diff --git a/packages/farm/src/nitro/universal-build.ts b/packages/farm/src/nitro/universal-build.ts index e710f1f1..6bd889b6 100644 --- a/packages/farm/src/nitro/universal-build.ts +++ b/packages/farm/src/nitro/universal-build.ts @@ -5588,8 +5588,30 @@ export default defineEventHandler(async (event) => { logger.info(`📄 Pre-rendering ${prerenderRoutes.length} SSG page(s)`); await nitro.prerender(nitroInstance); } - await nitro.build(nitroInstance); + const sharpRuntimeStagePromise = + imageRuntime === "node" + ? stageSharpRuntime(config, root, distDir, fs) + : Promise.resolve(null); + const [nitroBuildResult, sharpRuntimeStageResult] = await Promise.allSettled([ + nitro.build(nitroInstance), + sharpRuntimeStagePromise, + ]); await nitroInstance.close(); + if (nitroBuildResult.status === "rejected") { + if (sharpRuntimeStageResult.status === "fulfilled" && sharpRuntimeStageResult.value) { + await fs.rm(sharpRuntimeStageResult.value.stageDir, { + recursive: true, + force: true, + }); + } + throw nitroBuildResult.reason; + } + if (sharpRuntimeStageResult.status === "rejected") { + throw sharpRuntimeStageResult.reason; + } + const stagedSharpRuntime = + sharpRuntimeStageResult.value ?? + (imageRuntime === "node" ? await stageSharpRuntime(config, root, distDir, fs) : null); const copyResults = await Promise.allSettled([ canReusePrebuiltSSR @@ -5601,8 +5623,8 @@ export default defineEventHandler(async (event) => { fs, ) : Promise.resolve(), - imageRuntime === "node" - ? copySharpRuntime(config, root, path.join(outputDir, "server"), fs) + stagedSharpRuntime + ? installStagedSharpRuntime(stagedSharpRuntime, path.join(outputDir, "server"), fs) : Promise.resolve(), ]); const failedCopy = copyResults.find( @@ -5829,14 +5851,17 @@ async function copyFarmDocsContentForVercel( logger.info(`📚 Bundled docs content for Vercel: ${path.relative(root, docsContentDir)}`); } -async function copySharpRuntime( +interface StagedSharpRuntime { + stageDir: string; + dependencies: Record; +} + +async function stageSharpRuntime( config: ResolvedFarmConfig, root: string, - nitroFuncDir: string, + distDir: string, fs: typeof import("fs/promises"), -): Promise { - if (config.images.provider === "none") return; - +): Promise { const projectRequire = createRequire(path.join(root, "package.json")); let sharpRequire = projectRequire; if (!resolvePackageJson(projectRequire, "sharp")) { @@ -5851,7 +5876,10 @@ async function copySharpRuntime( } const copiedPackages = new Map(); const packageCopies = new Map>(); - const targetNodeModules = path.join(nitroFuncDir, "node_modules"); + const stageRoot = path.join(root, distDir, "tmp"); + await fs.mkdir(stageRoot, { recursive: true }); + const stageDir = await fs.mkdtemp(path.join(stageRoot, "sharp-runtime-")); + const targetNodeModules = path.join(stageDir, "node_modules"); function copyPackage(packageName: string, parentRequire: NodeJS.Require): Promise { const activeCopy = packageCopies.get(packageName); @@ -5892,21 +5920,90 @@ async function copySharpRuntime( ]); } - await copyPackage("sharp", sharpRequire); - if (!copiedPackages.has("sharp")) { - throw new Error( - "Farm image optimization requires sharp. Reinstall dependencies without omitting optional packages.", - ); + try { + await copyPackage("sharp", sharpRequire); + if (!copiedPackages.has("sharp")) { + throw new Error( + "Farm image optimization requires sharp. Reinstall dependencies without omitting optional packages.", + ); + } + } catch (error) { + await fs.rm(stageDir, { recursive: true, force: true }); + throw error; } - const functionPackagePath = path.join(nitroFuncDir, "package.json"); - const functionPackage = JSON.parse(await fs.readFile(functionPackagePath, "utf8")); - functionPackage.dependencies = { - ...functionPackage.dependencies, - ...Object.fromEntries([...copiedPackages].sort(([left], [right]) => left.localeCompare(right))), + return { + stageDir, + dependencies: Object.fromEntries( + [...copiedPackages].sort(([left], [right]) => left.localeCompare(right)), + ), }; - await fs.writeFile(functionPackagePath, JSON.stringify(functionPackage, null, 2)); - logger.info(`🖼️ Bundled Sharp image runtime (${copiedPackages.size} packages)`); +} + +async function installStagedSharpRuntime( + staged: StagedSharpRuntime, + nitroFuncDir: string, + fs: typeof import("fs/promises"), +): Promise { + const stagedNodeModules = path.join(staged.stageDir, "node_modules"); + const targetNodeModules = path.join(nitroFuncDir, "node_modules"); + + try { + await mergeStagedDirectory(stagedNodeModules, targetNodeModules, fs); + + const functionPackagePath = path.join(nitroFuncDir, "package.json"); + const functionPackage = JSON.parse(await fs.readFile(functionPackagePath, "utf8")); + functionPackage.dependencies = { + ...functionPackage.dependencies, + ...staged.dependencies, + }; + await fs.writeFile(functionPackagePath, JSON.stringify(functionPackage, null, 2)); + } finally { + await fs.rm(staged.stageDir, { recursive: true, force: true }); + } + + logger.info( + `🖼️ Bundled Sharp image runtime (${Object.keys(staged.dependencies).length} packages)`, + ); +} + +async function mergeStagedDirectory( + sourceDir: string, + targetDir: string, + fs: typeof import("fs/promises"), +): Promise { + await fs.mkdir(targetDir, { recursive: true }); + const entries = await fs.readdir(sourceDir, { withFileTypes: true }); + + await Promise.all( + entries.map(async (entry) => { + const sourcePath = path.join(sourceDir, entry.name); + const targetPath = path.join(targetDir, entry.name); + + try { + await fs.rename(sourcePath, targetPath); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "EEXIST" && code !== "ENOTEMPTY" && code !== "EXDEV") { + throw error; + } + } + + if (entry.isDirectory()) { + await mergeStagedDirectory(sourcePath, targetPath, fs); + await fs.rm(sourcePath, { recursive: true, force: true }); + return; + } + + await fs.cp(sourcePath, targetPath, { + force: true, + dereference: true, + mode: fsConstants.COPYFILE_FICLONE, + }); + await fs.rm(sourcePath, { force: true }); + }), + ); } function resolvePackageJson(parentRequire: NodeJS.Require, packageName: string): string | null {