repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/codegen.mjs
Raw#!/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;
}
}