96 lines
4.0 KiB
JavaScript
96 lines
4.0 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
|
|
const resource = JSON.parse(await fs.readFile(config.interface, "utf8"));
|
|
if (resource.kind !== "interface") throw Error("React props belong to an interface resource");
|
|
const iface = resource.revision;
|
|
const output = process.env.out;
|
|
if (!output) throw Error("Missing Nix output");
|
|
const work = path.resolve("contracts-source");
|
|
const declarations = [];
|
|
await fs.cp(config.source, work, { recursive: true });
|
|
const writable = async (directory) => {
|
|
await fs.chmod(directory, 0o755);
|
|
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isSymbolicLink()) throw Error("Contract source cannot contain symlinks");
|
|
if (entry.isDirectory()) await writable(file);
|
|
else {
|
|
await fs.chmod(file, 0o644);
|
|
if (/\.d\.[cm]?ts$/.test(file)) declarations.push(path.relative(work, file));
|
|
}
|
|
}
|
|
};
|
|
await writable(work);
|
|
const packageFile = path.join(work, "package.json");
|
|
const metadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
|
|
if (error.code === "ENOENT") return {};
|
|
throw error;
|
|
});
|
|
await fs.writeFile(packageFile, JSON.stringify({ ...metadata, type: "module" }));
|
|
if (config.dependencies) await fs.symlink(config.dependencies, path.join(work, "node_modules"));
|
|
const members = Object.create(null);
|
|
let index = 0;
|
|
for (const [id, entry] of Object.entries(config.members)) {
|
|
const member = (iface.template?.members ?? iface.members).find((member) => member.id === id);
|
|
if (
|
|
member?.kind !== "module" ||
|
|
!["org.quixos.react.component/1", "org.quixos.react.subject-only/1"].includes(member.contract)
|
|
)
|
|
throw Error("Props entry is not a React module member: " + id);
|
|
if (
|
|
!/^[$A-Z_a-z][$\w]*$/.test(entry.export) ||
|
|
path.isAbsolute(entry.entry) ||
|
|
entry.entry.split("/").some((part) => part === ".." || !part)
|
|
)
|
|
throw Error("Invalid props type entry");
|
|
const filename = `props-${index++}.ts`;
|
|
const typeImport = JSON.stringify("./" + entry.entry.replace(/\.[cm]?tsx?$/, ".js"));
|
|
await fs.writeFile(
|
|
path.join(work, filename),
|
|
`import type {${entry.export} as Props} from ${typeImport};
|
|
type Assert<T extends true> = T;
|
|
type ObjectProps = Assert<Props extends object ? true : false>;
|
|
type ReservedSubject = Assert<"subject" extends keyof Props ? false : true>;
|
|
${member.contract === "org.quixos.react.subject-only/1" ? "type SubjectOnly = Assert<{} extends Props ? true : false>;" : ""}
|
|
export type {Props};\n`,
|
|
);
|
|
members[id] = { path: filename.replace(/\.ts$/, ".d.ts"), export: "Props" };
|
|
}
|
|
await fs.mkdir(path.join(output, "types"), { recursive: true });
|
|
if (!Object.keys(members).length) throw Error("React type companion declares no module members");
|
|
execFileSync(
|
|
config.node,
|
|
[
|
|
config.tsc,
|
|
"--declaration",
|
|
"--emitDeclarationOnly",
|
|
"--strict",
|
|
"--target",
|
|
"es2024",
|
|
"--module",
|
|
"nodenext",
|
|
"--moduleResolution",
|
|
"nodenext",
|
|
"--rootDir",
|
|
work,
|
|
"--outDir",
|
|
path.join(output, "types"),
|
|
...Object.values(members).map((entry) => path.join(work, entry.path.replace(/\.d\.ts$/, ".ts"))),
|
|
],
|
|
{ cwd: work, stdio: "inherit" },
|
|
);
|
|
// TypeScript emits declarations for implementation sources, but does not copy
|
|
// authored declarations which those emitted files may import.
|
|
for (const file of declarations) {
|
|
await fs.mkdir(path.dirname(path.join(output, "types", file)), { recursive: true });
|
|
await fs.copyFile(path.join(work, file), path.join(output, "types", file));
|
|
}
|
|
await fs.writeFile(path.join(output, "package.json"), JSON.stringify({ type: "module" }));
|
|
if (config.dependencies) await fs.symlink(config.dependencies, path.join(output, "node_modules"));
|
|
await fs.writeFile(
|
|
path.join(output, "react-contracts.json"),
|
|
JSON.stringify({ schemaVersion: 1, interfaceRevisionId: iface.revisionId, members }, null, 2),
|
|
);
|