Build composable Camino packages with explicit type and source dependencies
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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);
|
||||
await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ 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),
|
||||
);
|
||||
Reference in New Issue
Block a user