163 lines
5.7 KiB
JavaScript
163 lines
5.7 KiB
JavaScript
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;
|
|
});
|