#!/usr/bin/env node import { lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs"; import { isAbsolute, join, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { ROOT } from "./lib/common.mjs"; export const CONFIG_PATH = "scripts/codegen/targets.json"; export const EXTENSION_SLUG = "{{extension-slug}}"; export const GENERATED_MARKER = "// Generated by scripts/codegen.mjs. DO NOT EDIT.\n"; function generatedWarning(template) { return [ "// ============================================================================", "// THIS FILE IS GENERATED. DO NOT EDIT IT DIRECTLY.", `// Instead, edit ${template}.`, "// Then run: mise run //:codegen", "// ============================================================================", "", ].join("\n"); } function pathError(label, message) { throw new Error(`${label}: ${message}`); } function validatePlaceholders(value, label) { for (const token of value.match(/{{[^{}]*}}|{{|}}/g) ?? []) if (token !== EXTENSION_SLUG) pathError(label, `unsupported placeholder ${token}`); } function safeRelativePath(root, value, label) { if (typeof value !== "string" || !value) pathError(label, "must be a path"); if (value.includes("\\") || value.includes("\0") || isAbsolute(value)) pathError(label, "must be a relative slash-separated path"); const parts = value.split("/"); if ( parts.some( (part) => !part || part === "." || part === ".." || part.includes(":"), ) ) pathError( label, "must not contain traversal or platform-specific segments", ); const path = resolve(root, ...parts); const rel = relative(root, path); if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) pathError(label, "must stay inside repository root"); return path; } function assertNoSymlink(root, path, label) { let current = root; for (const part of relative(root, path).split(/[\\/]/)) { current = join(current, part); try { if (lstatSync(current).isSymbolicLink()) pathError(label, `must not cross symlink ${relative(root, current)}`); } catch (error) { if (error.code !== "ENOENT") throw error; break; } } } function readUtf8(path, label) { let bytes; try { bytes = readFileSync(path); } catch (error) { pathError(label, error.message); } try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { pathError(label, "must be valid UTF-8 text"); } } function extensionDirectories(root) { const extensions = safeRelativePath(root, "extensions", "extensions root"); assertNoSymlink(root, extensions, "extensions root"); try { return readdirSync(extensions, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .filter((slug) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) .filter((slug) => { const pkg = join(extensions, slug, "package.json"); assertNoSymlink(root, pkg, `extension ${slug}`); try { return lstatSync(pkg).isFile(); } catch (error) { if (error.code === "ENOENT") return false; throw error; } }) .sort(); } catch (error) { if (error.code === "ENOENT") return []; throw error; } } function readTargets(root) { const path = safeRelativePath(root, CONFIG_PATH, "codegen config"); assertNoSymlink(root, path, "codegen config"); try { if (!lstatSync(path).isFile()) pathError("codegen config", "must be a file"); } catch (error) { if (error.code === "ENOENT") return []; throw error; } let config; try { config = JSON.parse(readUtf8(path, "codegen config")); } catch (error) { pathError("codegen config", `must contain valid JSON: ${error.message}`); } if (!config || typeof config !== "object" || Array.isArray(config)) pathError("codegen config", "must be an object with a targets array"); if (!Array.isArray(config.targets)) pathError("codegen config", "targets must be an array"); return config.targets; } function targetExtensions(target, available, label) { if (target.extensions === "all") return available; if (!Array.isArray(target.extensions) || !target.extensions.length) pathError(label, "extensions must be 'all' or a non-empty slug list"); const selected = new Set(); for (const slug of target.extensions) { if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) pathError(label, "extension selectors must be directory slugs"); if (!available.includes(slug)) pathError(label, `unknown extension ${slug}`); if (selected.has(slug)) pathError(label, `duplicate extension ${slug}`); selected.add(slug); } return [...selected].sort(); } function validateTarget(root, target, index, extensions) { const label = `target ${index + 1}`; if (!target || typeof target !== "object" || Array.isArray(target)) pathError(label, "must be an object"); if (typeof target.name !== "string" || !target.name) pathError(label, "name must be a non-empty string"); const template = safeRelativePath(root, target.template, `${label} template`); assertNoSymlink(root, template, `${label} template`); try { if (!lstatSync(template).isFile()) pathError(`${label} template`, "must be a regular file"); } catch (error) { if (error.code === "ENOENT") pathError(`${label} template`, "does not exist"); throw error; } if (typeof target.output !== "string") pathError(label, "output must be a path"); validatePlaceholders(target.output, `${label} output`); const matches = target.output.split(EXTENSION_SLUG).length - 1; if ( matches !== 1 || !target.output.startsWith(`extensions/${EXTENSION_SLUG}/`) ) pathError(label, `output must be inside extensions/${EXTENSION_SLUG}/`); const selected = targetExtensions(target, extensions, label); return { label, template, output: target.output, selected }; } export function expectedGenerated(root = ROOT) { const resolvedRoot = resolve(root); const extensions = extensionDirectories(resolvedRoot); const expected = []; const outputs = new Set(); for (const [index, target] of readTargets(resolvedRoot).entries()) { const validated = validateTarget(resolvedRoot, target, index, extensions); const template = readUtf8( validated.template, `${validated.label} template`, ); validatePlaceholders(template, `${validated.label} template`); for (const slug of validated.selected) { const output = safeRelativePath( resolvedRoot, validated.output.replace(EXTENSION_SLUG, slug), `${validated.label} output`, ); assertNoSymlink(resolvedRoot, output, `${validated.label} output`); if (outputs.has(output)) pathError( validated.label, `output collision at ${relative(resolvedRoot, output)}`, ); outputs.add(output); expected.push({ slug, output, bytes: Buffer.from( `${GENERATED_MARKER}${generatedWarning(target.template)}${template.replaceAll(EXTENSION_SLUG, slug)}`, "utf8", ), }); } } return expected; } function outputBytes(path, label) { try { const stat = lstatSync(path); if (stat.isSymbolicLink() || !stat.isFile()) pathError(label, "must be a regular file"); return readFileSync(path); } catch (error) { if (error.code === "ENOENT") return undefined; throw error; } } function extensionSlug(ext, root) { const path = resolve(ext); const extensions = join(root, "extensions"); if (resolve(path, "..") !== extensions) throw new Error("extension must be a direct child of extensions"); const slug = path.split(/[\\/]/).at(-1); if (!slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) throw new Error("extension must have a valid directory slug"); return slug; } export function checkGenerated(ext, root = ROOT) { let slug; try { const resolvedRoot = resolve(root); const expected = expectedGenerated(resolvedRoot); if (!expected.length) return []; slug = extensionSlug(ext, resolvedRoot); const errors = []; for (const generated of expected) { if (generated.slug !== slug) continue; const actual = outputBytes(generated.output, "generated output"); if (!actual?.equals(generated.bytes)) errors.push( `${slug}: stale generated ${relative(resolvedRoot, generated.output)}`, ); } return errors; } catch (error) { return [`${slug ?? "codegen"}: ${error.message}`]; } } export function generate(root = ROOT) { const resolvedRoot = resolve(root); const expected = expectedGenerated(resolvedRoot); for (const generated of expected) { const actual = outputBytes(generated.output, "generated output"); if (actual && !actual.toString("utf8").startsWith(GENERATED_MARKER)) throw new Error( `refusing to overwrite unmarked file ${relative(resolvedRoot, generated.output)}`, ); } for (const generated of expected) { if ( outputBytes(generated.output, "generated output")?.equals(generated.bytes) ) continue; mkdirSync(resolve(generated.output, ".."), { recursive: true }); assertNoSymlink(resolvedRoot, generated.output, "generated output"); writeFileSync(generated.output, generated.bytes); console.log(`OK generated ${relative(resolvedRoot, generated.output)}`); } return expected; } export function main() { generate(); return 0; } if (import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { try { process.exitCode = main(); } catch (error) { console.error(error.message); process.exitCode = 1; } }