Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/codegen.test.ts

Raw
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	symlinkSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import {
	checkGenerated,
	expectedGenerated,
	GENERATED_MARKER,
	generate,
} from "./codegen.mjs";

function withTemp(fn: (root: string) => void) {
	const root = mkdtempSync(join(tmpdir(), "pi-ext-codegen-"));
	try {
		fn(root);
	} finally {
		rmSync(root, { recursive: true, force: true });
	}
}

function extension(root: string, slug: string) {
	const dir = join(root, "extensions", slug);
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, "package.json"), "{}\n", "utf8");
	return dir;
}

function targets(root: string, value: object) {
	const dir = join(root, "scripts", "codegen");
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, "targets.json"), JSON.stringify(value), "utf8");
}

function template(
	root: string,
	text = "export const slug = '{{extension-slug}}';\n",
) {
	const path = join(root, "scripts", "codegen", "fixture.ts");
	mkdirSync(join(path, ".."), { recursive: true });
	writeFileSync(path, text, "utf8");
	return "scripts/codegen/fixture.ts";
}

it("selects every package extension for all", () => {
	withTemp((root) => {
		extension(root, "alpha");
		extension(root, "beta");
		const source = template(root);
		targets(root, {
			targets: [
				{
					name: "fixture",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
			],
		});

		generate(root);
		for (const slug of ["alpha", "beta"]) {
			const output = readFileSync(
				join(root, "extensions", slug, "generated.ts"),
				"utf8",
			);
			expect(output).toMatch(
				/^\/\/ Generated by scripts\/codegen\.mjs\. DO NOT EDIT\.\n\/\/ ={10,}\n\/\/ THIS FILE IS GENERATED\. DO NOT EDIT IT DIRECTLY\./,
			);
			expect(output).toContain(`// Instead, edit ${source}.`);
			expect(output).toContain("// Then run: mise run //:codegen");
			expect(output).toMatch(new RegExp(`export const slug = '${slug}';\\n$`));
		}
	});
});

it("selects listed extension slugs only", () => {
	withTemp((root) => {
		const alpha = extension(root, "alpha");
		extension(root, "beta");
		const source = template(root);
		targets(root, {
			targets: [
				{
					name: "fixture",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: ["alpha"],
				},
			],
		});

		generate(root);
		expect(checkGenerated(alpha, root)).toEqual([]);
		expect(expectedGenerated(root)).toHaveLength(1);
		expect(() =>
			readFileSync(join(root, "extensions", "beta", "generated.ts")),
		).toThrow();
	});
});

it("rejects traversal, symlinks, and output collisions", () => {
	withTemp((root) => {
		const alpha = extension(root, "alpha");
		const source = template(root);
		targets(root, {
			targets: [
				{
					name: "traversal",
					template: source,
					output: "../{{extension-slug}}.ts",
					extensions: "all",
				},
			],
		});
		expect(checkGenerated(alpha, root)[0]).toMatch(/extensions/);

		targets(root, {
			targets: [
				{
					name: "wrong-owner",
					template: source,
					output: "extensions/beta/generated-{{extension-slug}}.ts",
					extensions: "all",
				},
			],
		});
		expect(checkGenerated(alpha, root)[0]).toMatch(/extensions/);

		targets(root, {
			targets: [
				{
					name: "symlink",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
			],
		});
		symlinkSync(join(root, "extensions", "alpha"), join(root, "linked"));
		writeFileSync(
			join(root, "scripts", "codegen", "targets.json"),
			JSON.stringify({
				targets: [
					{
						name: "symlink",
						template: "linked/fixture.ts",
						output: "extensions/{{extension-slug}}/generated.ts",
						extensions: "all",
					},
				],
			}),
			"utf8",
		);
		expect(checkGenerated(alpha, root)[0]).toMatch(/symlink/);

		targets(root, {
			targets: [
				{
					name: "one",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
				{
					name: "two",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
			],
		});
		expect(checkGenerated(alpha, root)[0]).toMatch(/collision/);
	});
});

it("rejects unsupported or incomplete placeholders", () => {
	withTemp((root) => {
		const alpha = extension(root, "alpha");
		const source = template(root, 'export const value = "{{unknown}}";\n');
		targets(root, {
			targets: [
				{
					name: "fixture",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
			],
		});
		expect(checkGenerated(alpha, root)[0]).toMatch(/unsupported placeholder/);
		template(root, 'export const value = "{{extension-slug";\n');
		expect(checkGenerated(alpha, root)[0]).toMatch(/unsupported placeholder/);
		template(root);
		targets(root, {
			targets: [
				{
					name: "fixture",
					template: source,
					output: "extensions/{{extension-slug}}/{{unknown}}.ts",
					extensions: "all",
				},
			],
		});
		expect(checkGenerated(alpha, root)[0]).toMatch(/unsupported placeholder/);
	});
});

it("reports stale output without writing", () => {
	withTemp((root) => {
		const alpha = extension(root, "alpha");
		const source = template(root);
		targets(root, {
			targets: [
				{
					name: "fixture",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
			],
		});
		generate(root);
		const output = join(root, "extensions", "alpha", "generated.ts");
		writeFileSync(output, `${GENERATED_MARKER}stale\n`, "utf8");

		expect(checkGenerated(alpha, root)).toEqual([
			"alpha: stale generated extensions/alpha/generated.ts",
		]);
		expect(readFileSync(output, "utf8")).toBe(`${GENERATED_MARKER}stale\n`);
	});
});

it("refuses to overwrite unmarked output", () => {
	withTemp((root) => {
		extension(root, "alpha");
		const source = template(root);
		targets(root, {
			targets: [
				{
					name: "fixture",
					template: source,
					output: "extensions/{{extension-slug}}/generated.ts",
					extensions: "all",
				},
			],
		});
		const output = join(root, "extensions", "alpha", "generated.ts");
		writeFileSync(output, "manual\n", "utf8");

		expect(() => generate(root)).toThrow(/unmarked/);
		expect(readFileSync(output, "utf8")).toBe("manual\n");
	});
});

it("does nothing without codegen config", () => {
	withTemp((root) => {
		const alpha = extension(root, "alpha");
		expect(expectedGenerated(root)).toEqual([]);
		expect(checkGenerated(alpha, root)).toEqual([]);
		expect(generate(root)).toEqual([]);
	});
});