53 lines
2.6 KiB
TypeScript
53 lines
2.6 KiB
TypeScript
import { execFile as callback } from "node:child_process";
|
|||
|
|
import { promisify } from "node:util";
|
||
|
|
const execFile = promisify(callback);
|
||
|
|
export const nixLiteral = (value: string): string => JSON.stringify(value).replace(/\$\{/g, "\\${");
|
||
|
|
|
||
|
|
/** Recognize the deliberately small scaffold edit surface, never evaluate Nix.
|
||
|
|
* Authors may replace it with arbitrary Nix; then scaffolding declines the edit. */
|
||
|
|
export async function componentEntries(source: string): Promise<Record<string, { entry: string; export: string }>> {
|
||
|
|
const { stdout } = await execFile("nix-instantiate", ["--parse", "--expr", source], { maxBuffer: 1024 * 1024 });
|
||
|
|
const tokens = stdout.match(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]/g) ?? [];
|
||
|
|
if (stdout.replace(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]|\s+/g, ""))
|
||
|
|
throw Error("components.nix is outside the supported literal-entry shape");
|
||
|
|
let cursor = 0;
|
||
|
|
const expect = (token: string) => {
|
||
|
|
if (tokens[cursor++] !== token) throw Error("components.nix is outside the supported literal-entry shape");
|
||
|
|
};
|
||
|
|
const string = () => {
|
||
|
|
const token = tokens[cursor++];
|
||
|
|
if (!token?.startsWith('"')) throw Error("Component entries must use literal strings");
|
||
|
|
const json = token.replace(/(\\*)\$\{/g, (_match, escapes: string) => {
|
||
|
|
if (escapes.length % 2 === 0) throw Error("Component entries must use literal strings");
|
||
|
|
return escapes.slice(1) + "${";
|
||
|
|
});
|
||
|
|
return JSON.parse(json) as string;
|
||
|
|
};
|
||
|
|
const entries: Record<string, { entry: string; export: string }> = Object.create(null);
|
||
|
|
expect("{");
|
||
|
|
while (tokens[cursor] !== "}") {
|
||
|
|
const id = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
|
||
|
|
if (!id) throw Error("Missing component identity");
|
||
|
|
if (Object.hasOwn(entries, id)) throw Error("Duplicate component entry");
|
||
|
|
expect("=");
|
||
|
|
expect("{");
|
||
|
|
const properties: Record<string, string> = Object.create(null);
|
||
|
|
while (tokens[cursor] !== "}") {
|
||
|
|
const key = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
|
||
|
|
if (!key || !["entry", "export"].includes(key) || Object.hasOwn(properties, key))
|
||
|
|
throw Error("Unsupported component entry property");
|
||
|
|
expect("=");
|
||
|
|
properties[key] = string();
|
||
|
|
expect(";");
|
||
|
|
}
|
||
|
|
expect("}");
|
||
|
|
expect(";");
|
||
|
|
if (!properties.entry || !properties.export) throw Error("Component entry needs entry and export");
|
||
|
|
entries[id] = { entry: properties.entry, export: properties.export };
|
||
|
|
}
|
||
|
|
expect("}");
|
||
|
|
if (cursor !== tokens.length || !/\n}\s*$/.test(source))
|
||
|
|
throw Error("components.nix is outside the supported scaffold shape");
|
||
|
|
return entries;
|
||
|
|
}
|