import { createHash } from "node:crypto"; import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import prettier from "prettier"; const sourceExtensions = new Set([".ts", ".tsx", ".js", ".mjs", ".json", ".css", ".md"]); const scriptExtensions = new Set([".js", ".mjs"]); const rootExtensions = new Set([".ts", ".json", ".mjs", ".md", ".css"]); const args = parseArgs(process.argv.slice(2)); const shouldCheck = args.check; const shouldUseCache = args.cache; const cacheLocation = args.cacheLocation ?? "node_modules/.cache/prettier/.prettier-cache"; const root = process.cwd(); const files = await formatFiles(); const cache = shouldCheck && shouldUseCache ? await readCache(cacheLocation) : new Map(); const nextCache = new Map(cache); const unformattedFiles = []; let hadError = false; for (const file of files) { try { const info = await prettier.getFileInfo(file, { ignorePath: path.join(root, ".prettierignore") }); if (info.ignored || info.inferredParser === null) continue; const input = await readFile(file, "utf8"); const config = (await prettier.resolveConfig(file)) ?? {}; const cacheKey = path.relative(root, file); const cacheHash = hashFormatInput(input, config, info.inferredParser); if (shouldCheck && cache.get(cacheKey) === cacheHash) continue; const output = await prettier.format(input, { ...config, filepath: file }); if (output === input) { nextCache.set(cacheKey, cacheHash); continue; } if (shouldCheck) { nextCache.delete(cacheKey); unformattedFiles.push(cacheKey); continue; } await writeFile(file, output); console.log(path.relative(root, file)); } catch (error) { hadError = true; const message = error instanceof Error ? error.message : String(error); console.error(`${path.relative(root, file)}: ${message}`); } } if (shouldCheck && shouldUseCache) await writeCache(cacheLocation, nextCache); if (unformattedFiles.length > 0) { console.error("Unformatted files:"); for (const file of unformattedFiles) console.error(file); process.exit(1); } if (hadError) process.exit(1); function parseArgs(argv) { const parsed = { cache: false, cacheLocation: null, check: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--cache") { parsed.cache = true; } else if (arg === "--check") { parsed.check = true; } else if (arg === "--cache-location") { const value = argv[index + 1]; if (!value || value.startsWith("--")) usage(); parsed.cacheLocation = value; index += 1; } else { usage(); } } return parsed; } function usage() { console.error("Usage: node scripts/format.mjs [--check] [--cache] [--cache-location PATH]"); process.exit(1); } async function readCache(file) { try { const value = JSON.parse(await readFile(path.join(root, file), "utf8")); return new Map(Object.entries(value.files ?? {})); } catch { return new Map(); } } async function writeCache(file, cacheEntries) { const absoluteFile = path.join(root, file); await mkdir(path.dirname(absoluteFile), { recursive: true }); const files = Object.fromEntries([...cacheEntries.entries()].sort(([left], [right]) => left.localeCompare(right))); await writeFile(absoluteFile, `${JSON.stringify({ version: 1, files })}\n`); } function hashFormatInput(input, config, inferredParser) { return createHash("sha256") .update(prettier.version) .update("\0") .update(inferredParser ?? "") .update("\0") .update(JSON.stringify(config)) .update("\0") .update(input) .digest("hex"); } async function formatFiles() { const result = [ ...(await filesInTree("src", sourceExtensions, { excludedPrefixes: ["src/generated"] })), ...(await filesInTree("tests", sourceExtensions)), ...(await filesInTree("scripts", scriptExtensions)), ...(await rootFiles(rootExtensions)), ]; return [...new Set(result)].sort((a, b) => path.relative(root, a).localeCompare(path.relative(root, b))); } async function rootFiles(extensions) { const entries = await readdir(root, { withFileTypes: true }); return entries.filter((entry) => entry.isFile() && extensions.has(path.extname(entry.name))).map((entry) => path.join(root, entry.name)); } async function filesInTree(relativeDirectory, extensions, options = {}) { const directory = path.join(root, relativeDirectory); const excludedPrefixes = options.excludedPrefixes?.map((prefix) => path.join(root, prefix)) ?? []; const result = []; await collectFiles(directory, extensions, excludedPrefixes, result); return result; } async function collectFiles(directory, extensions, excludedPrefixes, result) { if (excludedPrefixes.some((prefix) => directory === prefix || directory.startsWith(`${prefix}${path.sep}`))) return; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { const file = path.join(directory, entry.name); if (entry.isDirectory()) { await collectFiles(file, extensions, excludedPrefixes, result); } else if (entry.isFile() && extensions.has(path.extname(entry.name))) { result.push(file); } } }