import { lstat, readFile, writeFile } from "node:fs/promises"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import { format } from "prettier"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const sourcePath = join(root, "src", "toolbox", "manifest.source.json"); const outputPath = join(root, "public", "toolbox-app.json"); const packagePath = join(root, "package.json"); const applicationVersionPath = join(root, "src", "version.ts"); const publicPath = join(root, "public"); const checkOnly = process.argv.includes("--check"); const source = JSON.parse(await readFile(sourcePath, "utf8")); const packageJson = JSON.parse(await readFile(packagePath, "utf8")); const applicationVersionSource = await readFile(applicationVersionPath, "utf8"); const applicationVersion = /^export const APPLICATION_VERSION = "([^"]+)";$/mu.exec( applicationVersionSource, )?.[1]; if ( source.version !== packageJson.version || applicationVersion !== packageJson.version ) { throw new Error( `Version drift: manifest ${source.version}, application ${String(applicationVersion)}, package ${packageJson.version}`, ); } if ( source.source?.repository !== "https://git.add-ideas.de/zemion/regex-tools" || source.source?.license !== "GPL-3.0-or-later" ) { throw new Error("Manifest source identity is incomplete or inconsistent"); } if (!Array.isArray(source.assets)) { throw new Error("Manifest assets must be an array"); } for (const asset of source.assets) { if ( typeof asset !== "string" || !asset.startsWith("./") || asset.includes("\\") || asset.split("/").includes("..") ) { throw new Error(`Unsafe manifest asset path: ${JSON.stringify(asset)}`); } const candidate = join(publicPath, asset.slice(2)); const details = await lstat(candidate).catch(() => null); if (!details?.isFile() || details.isSymbolicLink()) { throw new Error(`Manifest asset is missing or unsafe: ${asset}`); } } const serialized = await format(JSON.stringify(source), { filepath: outputPath, }); if (checkOnly) { const current = await readFile(outputPath, "utf8").catch(() => ""); if (current !== serialized) { throw new Error( `${relative(root, outputPath)} is stale; run npm run manifest:generate`, ); } console.log("Toolbox manifest is synchronized"); } else { await writeFile(outputPath, serialized); console.log(`Generated ${relative(root, outputPath)}`); }