Package ordinary PresentationMount contracts and typed React hosting

This commit is contained in:
Timothy J. Aveni
2026-09-25 12:26:34 -07:00
parent e7778b3518
commit c482a99aca
4 changed files with 19 additions and 6 deletions
+2 -2
View File
@@ -37,8 +37,8 @@ export function validatePresentations(workspace: WorkspaceRevision): CapabilityV
?.modules?.find((entry) => entry.id === presentation.componentId); ?.modules?.find((entry) => entry.id === presentation.componentId);
if (module) { if (module) {
const contract = moduleContract(workspace.interfaceImports, module); const contract = moduleContract(workspace.interfaceImports, module);
if (contract?.contract !== "org.quixos.react.subject-only/1") if (!contract || !["org.quixos.react.subject-only/1", "org.quixos.react.component/2"].includes(contract.contract))
fail(`${path}.componentId`, "Standalone presentation requires a subject-only module contract"); fail(`${path}.componentId`, "Standalone presentation requires a supported React presentation contract");
if (contract?.subject?.kind !== "interface" || contract.subject.interfaceRevisionId !== presentation.subject) if (contract?.subject?.kind !== "interface" || contract.subject.interfaceRevisionId !== presentation.subject)
fail(`${path}.subject`, "Component requires its exact declared subject interface"); fail(`${path}.subject`, "Component requires its exact declared subject interface");
continue; continue;
+14 -1
View File
@@ -211,6 +211,19 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
const subjects = Object.fromEntries(components.map((component) => [component.displayName, find(component.subject)])); const subjects = Object.fromEntries(components.map((component) => [component.displayName, find(component.subject)]));
const props = (iface: string, member: string) => const props = (iface: string, member: string) =>
`ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`; `ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`;
// Preserve closed type arguments for the artifact consumer. Interpretation of
// a particular module contract belongs to that consumer, not this compiler.
const moduleArguments = (iface: InterfaceRevision) => `[${(iface.application?.arguments ?? []).map(argument => {
if (argument.kind === "value") return tsType(argument.type);
const target = argument.target;
const reference = target.kind === "atom" ? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>` : `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`;
const fields = target.kind === "interface" ? find(target.interfaceRevisionId).members.flatMap(member => {
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
const getter = member.operations.find(operation => operation.displayName === (member.kind === "value" ? "get" : "resolve"));
return getter ? [`readonly ${JSON.stringify(member.displayName)}: ${tsType(getter.outputType)}`] : [];
}) : [];
return `{readonly reference: ${reference}; readonly values: {${fields.join(";")}}}`;
}).join(",")}]`;
const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props); const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props);
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props) const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props)
.split("\n") .split("\n")
@@ -248,7 +261,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
.map((component) => { .map((component) => {
const module = modules.find((module) => module.id === component.id)!; const module = modules.find((module) => module.id === component.id)!;
const key = JSON.stringify([module.interfaceRevisionId, module.memberId]); const key = JSON.stringify([module.interfaceRevisionId, module.memberId]);
return `${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}> & ExtraModuleProps<${JSON.stringify(key)}>>;`; return `${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}, ${JSON.stringify(module.contract)}, ${moduleArguments(find(module.interfaceRevisionId))}> & ExtraModuleProps<${JSON.stringify(key)}>>;`;
}) })
.join("\n") + .join("\n") +
"\n}\n" "\n}\n"
+2 -2
View File
@@ -18,7 +18,7 @@ const meta = () => selection("_camino", { selection: [selection("ref")] });
export const inspectionPackage = "org.quixos.inspection/1"; export const inspectionPackage = "org.quixos.inspection/1";
export interface InspectionTemplate { export interface InspectionTemplate {
interface: string; interface: string;
members: { id: string; name: string; key: string; flag: string; lane: "local" | "server"; many: boolean }[]; members: { id: string; name: string; key: string; flag: string; lane: "local" | "server"; many: boolean; relationship: boolean }[];
inputs?: PublicQueryContract; inputs?: PublicQueryContract;
view?: PublicQueryContract; view?: PublicQueryContract;
server?: PublicQueryContract; server?: PublicQueryContract;
@@ -81,7 +81,7 @@ export function compileInspectionTemplates(plan: ExecutionPlan) {
// Only native edges promise the relationship traversal used by the interpreter. // Only native edges promise the relationship traversal used by the interpreter.
if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue; if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue;
if (!complete && (lane === "local" || !portable)) if (!complete && (lane === "local" || !portable))
template.members.push({ id: member.id, name: member.displayName, key, flag, lane, many }); template.members.push({ id: member.id, name: member.displayName, key, flag, lane, many, relationship: member.kind === "relationship" });
inputFields[flag] = scalar("bool"); inputFields[flag] = scalar("bool");
const field = selection(member.displayName, { const field = selection(member.displayName, {
key, key,
+1 -1
View File
@@ -82,7 +82,7 @@ function componentDeclarations(workspace: WorkspaceRevision, pkg: WorkspaceRevis
...(pkg.modules ?? []).flatMap((module) => { ...(pkg.modules ?? []).flatMap((module) => {
const contract = moduleContract(workspace.interfaceImports, module)!; const contract = moduleContract(workspace.interfaceImports, module)!;
if ( if (
!["org.quixos.react.component/1", "org.quixos.react.subject-only/1"].includes(contract.contract) || !["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(contract.contract) ||
contract.subject?.kind !== "interface" contract.subject?.kind !== "interface"
) )
return []; return [];