From 75628e0cfd9e0b6a2492c970720432128a05b237 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 16 Jan 2026 21:18:13 +0100 Subject: [PATCH] fix(coding-agent): use lazy-loaded photon wrapper for Node.js compatibility Previous commit broke Node.js/tsx by using the ESM entry point which doesn't work with Node. This creates a wrapper module that: - Uses require() for lazy loading (works in both Node and Bun) - Gracefully handles load failures (returns original image) - Works in Node.js, tsx, and Bun compiled binaries --- .../coding-agent/src/utils/image-convert.ts | 12 ++-- .../coding-agent/src/utils/image-resize.ts | 24 ++++++-- packages/coding-agent/src/utils/photon.d.ts | 5 -- packages/coding-agent/src/utils/photon.ts | 59 +++++++++++++++++++ packages/coding-agent/tsconfig.build.json | 2 +- 5 files changed, 86 insertions(+), 16 deletions(-) delete mode 100644 packages/coding-agent/src/utils/photon.d.ts create mode 100644 packages/coding-agent/src/utils/photon.ts diff --git a/packages/coding-agent/src/utils/image-convert.ts b/packages/coding-agent/src/utils/image-convert.ts index e6adfcae..a1e5a474 100644 --- a/packages/coding-agent/src/utils/image-convert.ts +++ b/packages/coding-agent/src/utils/image-convert.ts @@ -1,6 +1,4 @@ -// Use ESM entry point so Bun can embed the WASM in compiled binaries -// (the CJS entry uses fs.readFileSync which breaks in standalone binaries) -import { PhotonImage } from "@silvia-odwyer/photon-node/photon_rs_bg.js"; +import { getPhoton } from "./photon.js"; /** * Convert image to PNG format for terminal display. @@ -15,8 +13,14 @@ export async function convertToPng( return { data: base64Data, mimeType }; } + const photon = getPhoton(); + if (!photon) { + // Photon not available, can't convert + return null; + } + try { - const image = PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64"))); + const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64"))); try { const pngBuffer = image.get_bytes(); return { diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index 58f4d6f7..dbaf0b4c 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -1,7 +1,5 @@ import type { ImageContent } from "@mariozechner/pi-ai"; -// Use ESM entry point so Bun can embed the WASM in compiled binaries -// (the CJS entry uses fs.readFileSync which breaks in standalone binaries) -import { PhotonImage, resize, SamplingFilter } from "@silvia-odwyer/photon-node/photon_rs_bg.js"; +import { getPhoton } from "./photon.js"; export interface ImageResizeOptions { maxWidth?: number; // Default: 2000 @@ -55,9 +53,23 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption const opts = { ...DEFAULT_OPTIONS, ...options }; const inputBuffer = Buffer.from(img.data, "base64"); - let image: ReturnType | undefined; + const photon = getPhoton(); + if (!photon) { + // Photon not available, return original image + return { + data: img.data, + mimeType: img.mimeType, + originalWidth: 0, + originalHeight: 0, + width: 0, + height: 0, + wasResized: false, + }; + } + + let image: ReturnType | undefined; try { - image = PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer)); + image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer)); const originalWidth = image.get_width(); const originalHeight = image.get_height(); @@ -96,7 +108,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption height: number, jpegQuality: number, ): { buffer: Uint8Array; mimeType: string } { - const resized = resize(image!, width, height, SamplingFilter.Lanczos3); + const resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3); try { const pngBuffer = resized.get_bytes(); diff --git a/packages/coding-agent/src/utils/photon.d.ts b/packages/coding-agent/src/utils/photon.d.ts deleted file mode 100644 index c43c637f..00000000 --- a/packages/coding-agent/src/utils/photon.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Type declarations for the ESM entry point of @silvia-odwyer/photon-node -// The ESM entry exports the same API but uses WASM imports that bundlers can embed -declare module "@silvia-odwyer/photon-node/photon_rs_bg.js" { - export * from "@silvia-odwyer/photon-node"; -} diff --git a/packages/coding-agent/src/utils/photon.ts b/packages/coding-agent/src/utils/photon.ts new file mode 100644 index 00000000..0666460b --- /dev/null +++ b/packages/coding-agent/src/utils/photon.ts @@ -0,0 +1,59 @@ +/** + * Photon image processing wrapper. + * + * This module provides a unified interface to @silvia-odwyer/photon-node that works in: + * 1. Node.js (development, npm run build) + * 2. Bun compiled binaries (standalone distribution) + * + * 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 and gracefully handle failures. Image processing functions + * already have fallbacks that return original images when photon isn't available. + */ + +// Re-export types from the main package +export type { PhotonImage as PhotonImageType } from "@silvia-odwyer/photon-node"; + +// Lazy-loaded photon module +let photonModule: typeof import("@silvia-odwyer/photon-node") | null = null; +let loadAttempted = false; +let loadError: Error | null = null; + +/** + * Get the photon module, loading it lazily on first access. + * Returns null if loading fails (e.g., in broken Bun binary). + */ +export function getPhoton(): typeof import("@silvia-odwyer/photon-node") | null { + if (loadAttempted) { + return photonModule; + } + + loadAttempted = true; + + try { + // Dynamic require to defer loading until actually needed + // This also allows the error to be caught gracefully + photonModule = require("@silvia-odwyer/photon-node"); + } catch (e) { + loadError = e as Error; + photonModule = null; + } + + return photonModule; +} + +/** + * Check if photon is available and working. + */ +export function isPhotonAvailable(): boolean { + return getPhoton() !== null; +} + +/** + * Get the error that occurred during photon loading, if any. + */ +export function getPhotonLoadError(): Error | null { + getPhoton(); // Ensure load was attempted + return loadError; +} diff --git a/packages/coding-agent/tsconfig.build.json b/packages/coding-agent/tsconfig.build.json index db3f6d97..695dd9ad 100644 --- a/packages/coding-agent/tsconfig.build.json +++ b/packages/coding-agent/tsconfig.build.json @@ -5,5 +5,5 @@ "rootDir": "./src" }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] }