88 lines
4.2 KiB
TypeScript
88 lines
4.2 KiB
TypeScript
import type { BindingSchema } from "./index.js";
|
|||
|
|
import type { ValueType } from "../capability-model/types.js";
|
||
|
|
|
||
|
|
/** Browser projection of the SAME checked RPC types, not a second props schema. */
|
||
|
|
export function generateReactBindings(
|
||
|
|
schema: BindingSchema,
|
||
|
|
packageRevisionId: string,
|
||
|
|
propsExports: readonly string[],
|
||
|
|
components: readonly { module: string; propsExport: string }[] = [],
|
||
|
|
) {
|
||
|
|
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
|
||
|
|
if (!pkg) throw new Error(`Unknown package ${packageRevisionId}`);
|
||
|
|
const q = JSON.stringify;
|
||
|
|
const references = new Map<string, string>();
|
||
|
|
const declarations: string[] = [];
|
||
|
|
const type = (value: ValueType): string => {
|
||
|
|
switch (value.kind) {
|
||
|
|
case "builtin":
|
||
|
|
if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`);
|
||
|
|
return "null";
|
||
|
|
case "scalar":
|
||
|
|
return {
|
||
|
|
string: "string",
|
||
|
|
bool: "boolean",
|
||
|
|
bytes: "Uint8Array",
|
||
|
|
int64: "bigint",
|
||
|
|
uint64: "bigint",
|
||
|
|
int32: "number",
|
||
|
|
uint32: "number",
|
||
|
|
double: "number",
|
||
|
|
}[value.name];
|
||
|
|
case "optional":
|
||
|
|
return `(${type(value.value)} | null)`;
|
||
|
|
case "list":
|
||
|
|
return `Array<${type(value.value)}>`;
|
||
|
|
case "record":
|
||
|
|
return `{${Object.entries(value.fields)
|
||
|
|
.map(([name, field]) => `${q(name)}: ${type(field)}`)
|
||
|
|
.join(";")}}`;
|
||
|
|
case "message":
|
||
|
|
throw new Error(`React props require checked values, not opaque message ${value.descriptorId}`);
|
||
|
|
case "object-ref": {
|
||
|
|
if (value.expectation.kind === "atom") return `ObjectRef<${q(value.expectation.atomId)}>`;
|
||
|
|
const id = value.expectation.interfaceRevisionId;
|
||
|
|
const existing = references.get(id);
|
||
|
|
if (existing) return existing;
|
||
|
|
const name = `Interface${references.size}`;
|
||
|
|
references.set(id, name);
|
||
|
|
const iface = schema.interfaces.find((entry) => entry.revisionId === id);
|
||
|
|
if (!iface) throw new Error(`Missing React reference contract ${id}`);
|
||
|
|
const fields = iface.members.flatMap((member) => {
|
||
|
|
if (member.kind === "operation") return [];
|
||
|
|
const get = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
|
||
|
|
if (!get) return [];
|
||
|
|
const writable = member.kind === "value" && member.operations.some((op) => op.displayName === "set");
|
||
|
|
return [`${q(member.displayName)}: ${writable ? "WritableField" : "ReadableField"}<${type(get.outputType)}>`];
|
||
|
|
});
|
||
|
|
declarations.push(`export type ${name} = InterfaceReference<${q(id)}, {${fields.join(";")}}> ;`);
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
// Only exports explicitly selected as props are projected. Non-props exports
|
||
|
|
// may legitimately use opaque messages or types unrelated to the browser.
|
||
|
|
const selected = propsExports.map((name) => {
|
||
|
|
const entry = pkg.exports.find((candidate) => candidate.displayName === name);
|
||
|
|
if (!entry) throw new Error(`Unknown React props export ${name}`);
|
||
|
|
return entry;
|
||
|
|
});
|
||
|
|
const results = selected.map(
|
||
|
|
(entry) =>
|
||
|
|
`${q(entry.displayName)}: ${type(entry.kind === "operation" ? (entry.eventType ?? entry.outputType) : entry.outputType)}`,
|
||
|
|
);
|
||
|
|
const checks = components.map((component, i) => {
|
||
|
|
if (!propsExports.includes(component.propsExport))
|
||
|
|
throw new Error(`Component refers to unselected props export ${component.propsExport}`);
|
||
|
|
if (!component.module.startsWith("."))
|
||
|
|
throw new Error("React component check must name a local module relative to generated bindings");
|
||
|
|
return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
|
||
|
|
});
|
||
|
|
return (
|
||
|
|
`// Generated from checked QX contracts. Do not edit.\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
||
|
|
(checks.length
|
||
|
|
? `type CheckedComponent<K extends keyof ReactResults, C extends (props: {camino: ReactResults[K]; render: any; dispatch: (action: any) => void}) => unknown> = C;\n${checks.join("\n")}\n`
|
||
|
|
: "")
|
||
|
|
);
|
||
|
|
}
|