129 lines
4.1 KiB
JavaScript
129 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const root = process.cwd();
|
|
const sourceDir = path.join(root, "dist", "docs-site");
|
|
const outputDir = path.join(root, "dist", "docs-r2");
|
|
const manifestPath = path.join(root, "dist", "docs-r2-manifest.json");
|
|
|
|
if (!fs.existsSync(sourceDir)) throw new Error("dist/docs-site does not exist; run docs:build first");
|
|
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
copyTree(sourceDir, outputDir);
|
|
|
|
const entries = [];
|
|
for (const file of walk(outputDir)) {
|
|
const key = toKey(path.relative(outputDir, file));
|
|
if (key === "_headers") continue;
|
|
entries.push(entryFor(key, file, key));
|
|
}
|
|
|
|
for (const file of walk(outputDir)) {
|
|
const rel = toKey(path.relative(outputDir, file));
|
|
if (rel === "_headers") continue;
|
|
if (!rel.endsWith("/index.html") || rel === "index.html") continue;
|
|
const slashlessKey = rel.slice(0, -"/index.html".length);
|
|
entries.push(entryFor(slashlessKey, file, rel));
|
|
}
|
|
|
|
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
const manifest = {
|
|
version: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
sourceDir: "dist/docs-site",
|
|
outputDir: "dist/docs-r2",
|
|
objectCount: entries.length,
|
|
entries,
|
|
};
|
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
const physicalFiles = countFiles(outputDir);
|
|
const virtualFiles = entries.length - physicalFiles;
|
|
console.log(`r2 prepare ok: ${physicalFiles} files, ${virtualFiles} slashless html aliases, ${entries.length} objects`);
|
|
|
|
function copyTree(from, to) {
|
|
fs.mkdirSync(to, { recursive: true });
|
|
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
const source = path.join(from, entry.name);
|
|
const target = path.join(to, entry.name);
|
|
if (entry.isDirectory()) {
|
|
copyTree(source, target);
|
|
} else if (entry.isFile()) {
|
|
fs.copyFileSync(source, target);
|
|
}
|
|
}
|
|
}
|
|
|
|
function entryFor(key, file, sourceKey) {
|
|
const data = fs.readFileSync(file);
|
|
return {
|
|
key,
|
|
sourceKey,
|
|
file: toKey(path.relative(root, file)),
|
|
size: data.byteLength,
|
|
sha256: crypto.createHash("sha256").update(data).digest("hex"),
|
|
contentType: contentTypeFor(key),
|
|
cacheControl: cacheControlFor(key),
|
|
};
|
|
}
|
|
|
|
function contentTypeFor(key) {
|
|
const ext = path.extname(key).toLowerCase();
|
|
if (!ext || key.endsWith("/index.html")) return "text/html; charset=utf-8";
|
|
switch (ext) {
|
|
case ".avif": return "image/avif";
|
|
case ".css": return "text/css; charset=utf-8";
|
|
case ".gif": return "image/gif";
|
|
case ".html": return "text/html; charset=utf-8";
|
|
case ".ico": return "image/x-icon";
|
|
case ".jpeg":
|
|
case ".jpg": return "image/jpeg";
|
|
case ".js": return "text/javascript; charset=utf-8";
|
|
case ".json": return "application/json; charset=utf-8";
|
|
case ".jsonl": return "application/x-ndjson; charset=utf-8";
|
|
case ".md": return "text/markdown; charset=utf-8";
|
|
case ".png": return "image/png";
|
|
case ".svg": return "image/svg+xml";
|
|
case ".txt": return "text/plain; charset=utf-8";
|
|
case ".webp": return "image/webp";
|
|
case ".wasm": return "application/wasm";
|
|
case ".xml": return "application/xml; charset=utf-8";
|
|
default: return "application/octet-stream";
|
|
}
|
|
}
|
|
|
|
function cacheControlFor(key) {
|
|
if (key === "CNAME") return "public, max-age=300, s-maxage=300";
|
|
if (key.endsWith(".html") || !path.extname(key)) {
|
|
return "public, max-age=60, s-maxage=86400, stale-while-revalidate=604800";
|
|
}
|
|
if (key.endsWith(".md") || key.endsWith(".txt") || key.endsWith(".json") || key.endsWith(".jsonl")) {
|
|
return "public, max-age=300, s-maxage=3600, stale-while-revalidate=86400";
|
|
}
|
|
return "public, max-age=31536000, immutable";
|
|
}
|
|
|
|
function countFiles(dir) {
|
|
let count = 0;
|
|
for (const _file of walk(dir)) count += 1;
|
|
return count;
|
|
}
|
|
|
|
function* walk(dir) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
yield* walk(fullPath);
|
|
} else if (entry.isFile()) {
|
|
yield fullPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toKey(value) {
|
|
return value.split(path.sep).join("/");
|
|
}
|