// 文件系统与会话基元。刻意不引任何第三方依赖:构建脚本要能在一台只有 Node 的机器上跑起来。 import { createHash } from "node:crypto"; import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { dirname, join, relative, resolve, sep } from "node:path"; /** 仓库根(tools/lib/fs.ts → ../..)。所有路径都相对它解析,脚本可从任意 cwd 调用。 */ export const repoRoot = resolve(import.meta.dirname, "..", ".."); /** 把仓库相对路径解析成绝对路径。 */ export function abs(rel: string): string { return join(repoRoot, rel); } /** 统一成以 / 分隔的仓库相对路径,便于打印与写进 dist-map.json。 */ export function rel(absPath: string): string { return relative(repoRoot, absPath).split(sep).join("/"); } export async function readJson(path: string): Promise { return JSON.parse(await readFile(path, "utf8")) as T; } export async function writeJson(path: string, value: unknown): Promise { await mkdir(dirname(path), { recursive: true }); await writeFile(path, JSON.stringify(value, null, "\t") + "\n", "utf8"); } export async function writeText(path: string, value: string): Promise { await mkdir(dirname(path), { recursive: true }); await writeFile(path, value.endsWith("\n") ? value : value + "\n", "utf8"); } export async function exists(path: string): Promise { return existsSync(path); } export async function isDir(path: string): Promise { try { return (await stat(path)).isDirectory(); } catch { return false; } } export async function isFile(path: string): Promise { try { return (await stat(path)).isFile(); } catch { return false; } } /** 列目录,只返回目录名,跳过以 . 开头的条目与显式排除的名字。 */ export async function listDirs(path: string, exclude: string[] = []): Promise { if (!(await isDir(path))) return []; const entries = await readdir(path, { withFileTypes: true }); return entries .filter((e) => e.isDirectory() && !e.name.startsWith(".") && !exclude.includes(e.name)) .map((e) => e.name) .sort(); } /** * 拷贝目录树。 * * 刻意用真拷贝而不是链接(hardlink / junction):分发目录要能被整体搬到别的机器上传, * 链接会让"目录可整体搬走"这条不变式在某些工具下失效(见 docs/adr/0005)。 */ export async function copyTree(src: string, dest: string): Promise { if (!(await exists(src))) return; await mkdir(dirname(dest), { recursive: true }); await cp(src, dest, { recursive: true, force: true }); } /** 拷贝单个文件,自动建父目录。 */ export async function copyFileTo(src: string, dest: string): Promise { await mkdir(dirname(dest), { recursive: true }); await cp(src, dest, { force: true }); } /** 目录内所有文件的相对路径(以 / 分隔)。 */ export async function walkFiles(root: string, base = root): Promise { if (!(await isDir(base))) return []; const out: string[] = []; for (const entry of await readdir(base, { withFileTypes: true })) { const full = join(base, entry.name); if (entry.isDirectory()) out.push(...(await walkFiles(root, full))); else if (entry.isFile()) out.push(relative(root, full).split(sep).join("/")); } return out.sort(); } /** 目录内所有文件的总字节数。 */ export async function dirBytes(root: string): Promise { if (!(await isDir(root))) return 0; let total = 0; for (const f of await walkFiles(root)) total += (await stat(join(root, f))).size; return total; } export async function sizeOf(path: string): Promise { try { return (await stat(path)).size; } catch { return 0; } } /** 一个长任务的耗时统计与人性化打印。 */ export function mb(bytes: number): string { return (bytes / 1024 / 1024).toFixed(1) + " MB"; } export function sha1(text: string): string { return createHash("sha1").update(text).digest("hex"); } /** 清空目录后重建(用于"分发目录绝不残留脏文件"这条保证)。 */ export async function resetDir(path: string): Promise { await rm(path, { recursive: true, force: true }); await mkdir(path, { recursive: true }); } /** 单行进度输出,避免几十个分发目录刷屏。 */ export function log(message: string): void { process.stdout.write(message + "\n"); }