chore: initialize WanderQ MiniAPP 2.0 repository
This commit is contained in:
162
scripts/migrate-static-assets.mjs
Normal file
162
scripts/migrate-static-assets.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user