111 lines
3.5 KiB
JavaScript
111 lines
3.5 KiB
JavaScript
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const repoRoot = path.join(__dirname, "..");
|
||
const sourceDir = path.join(repoRoot, "原图");
|
||
const publicMeme = path.join(repoRoot, "public", "meme");
|
||
const outFile = path.join(repoRoot, "public", "memes.json");
|
||
|
||
const NUMBERED_ASSET_RE = /^(\d+)\.(webp|gif)$/i;
|
||
|
||
/**
|
||
* Mirror srcDir into destDir without fs.cpSync (avoids native crashes on huge trees / some Node+Win builds).
|
||
*/
|
||
function copyTreeIterative(srcDir, destDir) {
|
||
fs.mkdirSync(destDir, { recursive: true });
|
||
/** @type {Array<[string, string]>} */
|
||
const stack = [[srcDir, destDir]];
|
||
let files = 0;
|
||
while (stack.length) {
|
||
const [src, dest] = stack.pop();
|
||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||
for (const ent of entries) {
|
||
const s = path.join(src, ent.name);
|
||
const d = path.join(dest, ent.name);
|
||
if (ent.isDirectory()) {
|
||
fs.mkdirSync(d, { recursive: true });
|
||
stack.push([s, d]);
|
||
} else if (ent.isFile()) {
|
||
fs.copyFileSync(s, d);
|
||
files += 1;
|
||
}
|
||
}
|
||
}
|
||
return files;
|
||
}
|
||
|
||
function syncSourceToPublic() {
|
||
console.log("build: mirroring 原图/ -> public/meme/ ...");
|
||
if (!fs.existsSync(sourceDir)) {
|
||
console.warn("原图/ not found — 请先运行 convert_to_webp.py,或保留 public/meme");
|
||
return false;
|
||
}
|
||
fs.mkdirSync(path.dirname(publicMeme), { recursive: true });
|
||
if (fs.existsSync(publicMeme)) {
|
||
fs.rmSync(publicMeme, { recursive: true, force: true });
|
||
}
|
||
fs.mkdirSync(publicMeme, { recursive: true });
|
||
const n = copyTreeIterative(sourceDir, publicMeme);
|
||
console.log(`synced 原图/ -> public/meme/ (${n} files copied)`);
|
||
return true;
|
||
}
|
||
|
||
/** @param {string} dirPath */
|
||
function listNumberedAssets(dirPath) {
|
||
if (!fs.existsSync(dirPath)) return [];
|
||
return fs
|
||
.readdirSync(dirPath)
|
||
.filter((f) => NUMBERED_ASSET_RE.test(f))
|
||
.sort((a, b) => {
|
||
const na = parseInt(a.replace(NUMBERED_ASSET_RE, "$1"), 10);
|
||
const nb = parseInt(b.replace(NUMBERED_ASSET_RE, "$1"), 10);
|
||
return na - nb;
|
||
});
|
||
}
|
||
|
||
function main() {
|
||
syncSourceToPublic();
|
||
|
||
const scanRoot = fs.existsSync(publicMeme) ? publicMeme : sourceDir;
|
||
|
||
/** @type {{ generatedAt: string; categories: Array<{ id: string; name: string; items: Array<{ file: string; path: string }> }> }} */
|
||
const payload = {
|
||
generatedAt: new Date().toISOString(),
|
||
categories: [],
|
||
};
|
||
|
||
if (fs.existsSync(scanRoot)) {
|
||
const dirs = fs.readdirSync(scanRoot, { withFileTypes: true });
|
||
for (const ent of dirs) {
|
||
if (!ent.isDirectory()) continue;
|
||
const id = ent.name;
|
||
const dirPath = path.join(scanRoot, id);
|
||
const files = listNumberedAssets(dirPath);
|
||
if (!files.length) continue;
|
||
payload.categories.push({
|
||
id,
|
||
name: id,
|
||
items: files.map((file) => ({
|
||
file,
|
||
path: `${id}/${file}`,
|
||
})),
|
||
});
|
||
}
|
||
}
|
||
|
||
payload.categories.sort((a, b) => a.id.localeCompare(b.id, "zh-Hans-CN"));
|
||
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||
fs.writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||
const n = payload.categories.reduce((acc, c) => acc + c.items.length, 0);
|
||
console.log(`manifest: ${payload.categories.length} categories, ${n} assets (webp/gif)`);
|
||
}
|
||
|
||
try {
|
||
main();
|
||
} catch (err) {
|
||
console.error("generate-manifest failed:", err);
|
||
process.exitCode = 1;
|
||
}
|