import { config as loadEnv } from "dotenv"; import OSS from "ali-oss"; import { spawn } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(scriptDir, ".."); const pythonScript = join(scriptDir, "prepare-static-webp.py"); const targetFiles = [ "src/App.tsx", "src/content.ts", "src/generated-products.json", "src/search-page-defaults.ts", "src/train-activity.ts", "apps/admin/src/App.tsx", "apps/api/src/home-module-defaults.ts", "apps/api/src/routes/admin.ts", "apps/api/src/routes/memory.ts", "apps/api/src/search-page.ts", ]; loadEnv({ path: resolve(projectRoot, ".env") }); const defaultPublicBaseUrl = (() => { const endpoint = String(process.env.OSS_ENDPOINT ?? "") .replace(/^https?:\/\//i, "") .replace(/\/$/, "") .split("/")[0]; return process.env.OSS_BUCKET_NAME && endpoint ? `https://${process.env.OSS_BUCKET_NAME}.${endpoint}` : ""; })(); const configuredPublicBaseUrl = String(process.env.MEDIA_PUBLIC_BASE_URL ?? "").replace(/\/$/, ""); const publicBaseUrl = configuredPublicBaseUrl || defaultPublicBaseUrl; if (!publicBaseUrl) throw new Error("无法推导 OSS 公共地址,请配置 MEDIA_PUBLIC_BASE_URL 或 OSS_BUCKET_NAME/OSS_ENDPOINT"); function endpointHost(endpoint) { return endpoint.replace(/^https?:\/\//i, "").replace(/\/$/, "").split("/")[0]; } function urlForKey(key) { return `${publicBaseUrl}/${key.split("/").map((part) => encodeURIComponent(part)).join("/")}`; } function run(command, args) { return new Promise((resolvePromise, reject) => { const child = spawn(command, args, { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk; }); child.stderr.on("data", (chunk) => { stderr += chunk; }); child.on("error", reject); child.on("close", (code) => { if (code === 0) { resolvePromise(stdout.trim()); return; } reject(new Error(`${command} 退出码 ${code}: ${stderr || stdout}`)); }); }); } function buildClient() { const accessKeyId = process.env.OSS_ACCESS_KEY_ID; const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET; const endpoint = process.env.OSS_ENDPOINT; const bucket = process.env.OSS_BUCKET_NAME; if (!accessKeyId || !accessKeySecret || !endpoint || !bucket) { throw new Error("OSS 存储需要配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_ENDPOINT 和 OSS_BUCKET_NAME"); } const host = endpointHost(endpoint); return { bucket, client: new OSS({ region: process.env.OSS_REGION ?? host.split(".")[0], endpoint: /^https?:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`, accessKeyId, accessKeySecret, bucket, secure: true, }), }; } async function uploadAll(manifest, outputRoot) { const { client } = buildClient(); let cursor = 0; let uploaded = 0; const concurrency = 4; async function worker() { while (cursor < manifest.length) { const item = manifest[cursor++]; const buffer = await readFile(join(outputRoot, item.outputPath)); await client.put(item.objectKey, buffer, { headers: { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable", }, }); uploaded += 1; if (uploaded % 20 === 0 || uploaded === manifest.length) { console.log(`OSS 上传进度 ${uploaded}/${manifest.length}`); } } } await Promise.all(Array.from({ length: Math.min(concurrency, manifest.length) }, () => worker())); } async function rewriteReferences(manifest) { const replacements = new Map(); for (const item of manifest) { if (item.reference) replacements.set(item.reference, urlForKey(item.objectKey)); } for (const relativePath of targetFiles) { const absolutePath = resolve(projectRoot, relativePath); let source = await readFile(absolutePath, "utf8"); for (const [from, to] of replacements) source = source.split(from).join(to); await writeFile(absolutePath, source); } } async function main() { const workDir = await mkdtemp(join(tmpdir(), "miniapp-static-webp-")); const outputRoot = join(workDir, "webp"); const manifestPath = join(workDir, "manifest.json"); try { const python = process.env.MINIAPP_IMAGE_PYTHON || process.env.PYTHON || "python3"; const prepareSummary = await run(python, [pythonScript, "--root", projectRoot, "--output", outputRoot, "--manifest", manifestPath]); console.log(`WebP 转码完成:${prepareSummary}`); const manifest = JSON.parse(await readFile(manifestPath, "utf8")).assets; await uploadAll(manifest, outputRoot); await rewriteReferences(manifest); await writeFile(resolve(projectRoot, "scripts/static-asset-manifest.json"), JSON.stringify({ generatedAt: new Date().toISOString(), publicBaseUrl, assets: manifest.map((item) => ({ sourcePath: item.sourcePath, reference: item.reference, objectKey: item.objectKey, url: urlForKey(item.objectKey), lossless: item.lossless, sourceBytes: item.sourceBytes, outputBytes: item.outputBytes, sha256: item.sha256, })), }, null, 2) + "\n"); console.log(JSON.stringify({ message: "静态图片已转 WebP 并上传 OSS", assets: manifest.length, publicBaseUrl }, null, 2)); } finally { await rm(workDir, { recursive: true, force: true }); } } main().catch((error) => { console.error(error); process.exitCode = 1; });