Spaces:
Running
Running
| import { copyFile, cp, mkdir, readFile, readdir, rm, stat } from 'node:fs/promises'; | |
| import { createRequire } from 'node:module'; | |
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| const requiredFiles = [ | |
| 'app-route-turbo.runtime.prod.js', | |
| 'app-route-turbo.runtime.prod.js.map' | |
| ]; | |
| const sharpRuntimePackages = ['sharp', 'semver', 'detect-libc']; | |
| const root = process.cwd(); | |
| function isSharpBindingPackage(packageName) { | |
| return packageName.startsWith('sharp-') && !packageName.startsWith('sharp-libvips-'); | |
| } | |
| function isSharpPlatformRuntimePackage(packageName) { | |
| return packageName === 'colour' || packageName.startsWith('sharp-'); | |
| } | |
| async function assertFile(filepath) { | |
| let info; | |
| try { | |
| info = await stat(filepath); | |
| } catch (error) { | |
| if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { | |
| throw new Error(`文件不存在:${filepath}`); | |
| } | |
| const message = error instanceof Error ? error.message : String(error); | |
| throw new Error(`读取文件状态失败:${filepath}。${message}`); | |
| } | |
| if (!info.isFile()) { | |
| throw new Error(`预期是文件,但实际不是文件路径:${filepath}`); | |
| } | |
| } | |
| async function directoryExists(dirpath) { | |
| try { | |
| const info = await stat(dirpath); | |
| return info.isDirectory(); | |
| } catch (error) { | |
| if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return false; | |
| throw error; | |
| } | |
| } | |
| async function assertDirectory(dirpath, description) { | |
| if (await directoryExists(dirpath)) return; | |
| throw new Error(`${description}缺失:${dirpath}`); | |
| } | |
| async function copyDirectory(source, target) { | |
| await rm(target, { force: true, recursive: true }); | |
| await mkdir(path.dirname(target), { recursive: true }); | |
| await cp(source, target, { recursive: true }); | |
| } | |
| async function isSharpExternalModule(sourceDir) { | |
| try { | |
| const rawPackageJson = await readFile(path.join(sourceDir, 'package.json'), 'utf8'); | |
| const packageJson = JSON.parse(rawPackageJson); | |
| return packageJson && typeof packageJson === 'object' && packageJson.name === 'sharp'; | |
| } catch (error) { | |
| if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return false; | |
| const message = error instanceof Error ? error.message : String(error); | |
| throw new Error(`读取 Turbopack sharp 外部模块 package.json 失败:${sourceDir}。${message}`); | |
| } | |
| } | |
| export async function copySharpRuntime(rootDir = process.cwd()) { | |
| const standaloneDir = path.join(rootDir, '.next', 'standalone'); | |
| for (const packageName of sharpRuntimePackages) { | |
| const sourcePackageDir = path.join(rootDir, 'node_modules', packageName); | |
| await assertDirectory(sourcePackageDir, 'standalone sharp 运行依赖'); | |
| const targetPackageDir = path.join(standaloneDir, 'node_modules', packageName); | |
| console.info(`复制 standalone sharp 运行依赖:${sourcePackageDir} -> ${targetPackageDir}`); | |
| await copyDirectory(sourcePackageDir, targetPackageDir); | |
| } | |
| const sourceImgDir = path.join(rootDir, 'node_modules', '@img'); | |
| await assertDirectory(sourceImgDir, 'standalone sharp 平台依赖目录'); | |
| await assertDirectory(path.join(sourceImgDir, 'colour'), 'standalone sharp colour 依赖'); | |
| const targetImgDir = path.join(standaloneDir, 'node_modules', '@img'); | |
| const entries = await readdir(sourceImgDir, { withFileTypes: true }); | |
| let copiedSharpBindingPackages = 0; | |
| let copiedSharpLibvipsPackages = 0; | |
| for (const entry of entries) { | |
| if (!entry.isDirectory() || !isSharpPlatformRuntimePackage(entry.name)) continue; | |
| const source = path.join(sourceImgDir, entry.name); | |
| const target = path.join(targetImgDir, entry.name); | |
| console.info(`复制 standalone sharp 平台包:${source} -> ${target}`); | |
| await copyDirectory(source, target); | |
| if (isSharpBindingPackage(entry.name)) copiedSharpBindingPackages += 1; | |
| if (entry.name.startsWith('sharp-libvips-')) copiedSharpLibvipsPackages += 1; | |
| } | |
| if (copiedSharpBindingPackages === 0) { | |
| throw new Error(`standalone sharp native binding 依赖缺失:${sourceImgDir} 没有可复制的 @img/sharp-* binding 包`); | |
| } | |
| if (copiedSharpLibvipsPackages === 0) { | |
| throw new Error(`standalone sharp libvips 依赖缺失:${sourceImgDir} 没有可复制的 @img/sharp-libvips-* 包`); | |
| } | |
| const externalNodeModulesDir = path.join(rootDir, '.next', 'node_modules'); | |
| if (!(await directoryExists(externalNodeModulesDir))) return; | |
| const externalEntries = await readdir(externalNodeModulesDir, { withFileTypes: true }); | |
| const sourceSharpDir = path.join(rootDir, 'node_modules', 'sharp'); | |
| if (!(await directoryExists(sourceSharpDir))) return; | |
| for (const entry of externalEntries) { | |
| if (!entry.isDirectory() || !entry.name.startsWith('sharp-')) continue; | |
| const externalSource = path.join(externalNodeModulesDir, entry.name); | |
| if (!(await isSharpExternalModule(externalSource))) continue; | |
| const target = path.join(standaloneDir, '.next', 'node_modules', entry.name); | |
| console.info(`复制 standalone Turbopack sharp 外部模块:${sourceSharpDir} -> ${target}`); | |
| await copyDirectory(sourceSharpDir, target); | |
| } | |
| } | |
| export async function validateStandaloneSharpRuntime(rootDir = process.cwd()) { | |
| const standaloneDir = path.join(rootDir, '.next', 'standalone'); | |
| const requireFromStandalone = createRequire(path.join(standaloneDir, 'runtime-check.cjs')); | |
| let sharpModule; | |
| try { | |
| const resolvedSharp = requireFromStandalone.resolve('sharp'); | |
| const sharpPackageDir = path.join(standaloneDir, 'node_modules', 'sharp'); | |
| for (const cachedPath of Object.keys(requireFromStandalone.cache)) { | |
| if (cachedPath === resolvedSharp || cachedPath.startsWith(`${sharpPackageDir}${path.sep}`)) { | |
| delete requireFromStandalone.cache[cachedPath]; | |
| } | |
| } | |
| sharpModule = requireFromStandalone('sharp'); | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| throw new Error(`standalone sharp 运行时加载失败:${message}`); | |
| } | |
| const sharp = sharpModule?.default ?? sharpModule; | |
| if (typeof sharp !== 'function') { | |
| throw new Error('standalone sharp 运行时加载失败:sharp 模块没有导出函数。'); | |
| } | |
| try { | |
| const buffer = await sharp({ | |
| create: { | |
| width: 1, | |
| height: 1, | |
| channels: 3, | |
| background: { r: 0, g: 0, b: 0 } | |
| } | |
| }) | |
| .webp() | |
| .toBuffer(); | |
| if (!Buffer.isBuffer(buffer) || buffer.length === 0) { | |
| throw new Error('sharp self-check returned an empty result'); | |
| } | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| throw new Error(`standalone sharp 运行时自检失败:${message}`); | |
| } | |
| } | |
| export async function patchStandaloneRuntime(rootDir = process.cwd()) { | |
| const sourceDir = path.join(rootDir, 'node_modules', 'next', 'dist', 'compiled', 'next-server'); | |
| const targetDir = path.join( | |
| rootDir, | |
| '.next', | |
| 'standalone', | |
| 'node_modules', | |
| 'next', | |
| 'dist', | |
| 'compiled', | |
| 'next-server' | |
| ); | |
| console.info(`准备补齐 standalone runtime:${targetDir}`); | |
| await mkdir(targetDir, { recursive: true }); | |
| for (const filename of requiredFiles) { | |
| const source = path.join(sourceDir, filename); | |
| const target = path.join(targetDir, filename); | |
| console.info(`复制 standalone runtime 文件:${source} -> ${target}`); | |
| await assertFile(source); | |
| await copyFile(source, target); | |
| } | |
| await copySharpRuntime(rootDir); | |
| await validateStandaloneSharpRuntime(rootDir); | |
| console.info('standalone runtime 文件补齐完成。'); | |
| } | |
| async function main() { | |
| try { | |
| await patchStandaloneRuntime(root); | |
| } catch (error) { | |
| console.error('补齐 standalone runtime 文件失败。', error); | |
| process.exit(1); | |
| } | |
| } | |
| if (path.resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) { | |
| await main(); | |
| } | |