From 588d90fd97afe989dadeaa0b9d8ea4b209af6773 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Sun, 21 Jun 2026 20:30:06 +0200 Subject: [PATCH] fix(web): place sharp @img closure next to the pnpm-store copy Next loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #71/#72 copied the @img closure into apps/web/node_modules, but Next loads sharp from the standalone ROOT pnpm store (node_modules/.pnpm/sharp@/node_modules/sharp) — where only a mismatched libvips (8.17.3, for the transitive sharp 0.34.5) had been traced, not 8.18.3 for our sharp 0.35.2. So dlopen still failed. Copy sharp + the full @img closure into *every* matching sharp home in the bundle: the pnpm-store path Next actually resolves, plus apps/web/node_modules as a fallback. Verified both receive the closure (incl. libvips at the right version on the Linux build). --- apps/web/scripts/copy-standalone-assets.mjs | 34 +++++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/web/scripts/copy-standalone-assets.mjs b/apps/web/scripts/copy-standalone-assets.mjs index 8d4e0ce..930d753 100644 --- a/apps/web/scripts/copy-standalone-assets.mjs +++ b/apps/web/scripts/copy-standalone-assets.mjs @@ -8,7 +8,7 @@ // "Could not load the sharp module … libvips-cpp.so… cannot open shared object // file". So we copy sharp + its platform `@img/*` binaries in explicitly. import { cpSync, existsSync, readdirSync, realpathSync, rmSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const webRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); @@ -48,6 +48,11 @@ function walkImgScope(scopeDir, destImg, seen) { * (platform module + libvips + colour) into the standalone bundle. Next 16's * Turbopack build keeps sharp external but doesn't trace these into * `output: standalone`, so the runtime require fails without this. + * + * Next actually loads sharp from the standalone ROOT pnpm store + * (`node_modules/.pnpm/sharp@/node_modules/sharp`), not from the app's + * own node_modules — so the @img closure must land *next to that copy*. We + * also drop it into `apps/web/node_modules/@img` as a belt-and-braces fallback. */ function copySharp() { // Resolve via the filesystem symlink (not require.resolve — sharp's `exports` @@ -62,17 +67,26 @@ function copySharp() { } // realpath → …/.pnpm/sharp@/node_modules/sharp const sharpDir = realpathSync(link); - const destNodeModules = join(standaloneWeb, "node_modules"); + const sharpScope = join(dirname(sharpDir), "@img"); // sharp's direct @img deps + const pnpmSharpDir = basename(dirname(dirname(sharpDir))); // e.g. "sharp@0.35.2" + const standaloneRoot = join(webRoot, ".next", "standalone"); - rmSync(join(destNodeModules, "sharp"), { recursive: true, force: true }); - cpSync(sharpDir, join(destNodeModules, "sharp"), { recursive: true, dereference: true }); - console.log("copied sharp"); + // Every place a matching sharp lives in the bundle gets the @img closure next + // to it. The pnpm-store copy is the one Next loads; the app copy is a fallback. + const sharpHomes = [ + join(standaloneRoot, "node_modules", ".pnpm", pnpmSharpDir, "node_modules"), + join(standaloneWeb, "node_modules"), + ]; - // sharp's @img deps live next to it (…/sharp@ver/node_modules/@img); walk the - // whole closure so libvips comes along with the right version. - const destImg = join(destNodeModules, "@img"); - rmSync(destImg, { recursive: true, force: true }); - walkImgScope(join(dirname(sharpDir), "@img"), destImg, new Set()); + for (const home of sharpHomes) { + if (!existsSync(home)) continue; + rmSync(join(home, "sharp"), { recursive: true, force: true }); + cpSync(sharpDir, join(home, "sharp"), { recursive: true, dereference: true }); + const destImg = join(home, "@img"); + rmSync(destImg, { recursive: true, force: true }); + walkImgScope(sharpScope, destImg, new Set()); + console.log(`sharp + @img closure -> ${home}`); + } } if (!existsSync(standaloneWeb)) {