import type { InterfaceRevision, PackageRevision, ValueType } from "../capability-model/types.js"; import type { QueryBinding, QueryComposition, QueryCompositionStep } from "../query/composition.js"; /** This is an artifact of the explicitly selected React frontend, not QX IR. */ export const preparationArtifact = "org.quixos.react.preparation/1"; export interface ReactChildDeclaration { name: string; subject: string; root: QueryBinding; slot: string; with?: QueryBinding; each?: { source: QueryBinding; limit: number }; when?: QueryBinding; role?: { interface: string; member: string }; provider?: { package: string; module: string }; } export interface ReactModulePreparation { module: string; composition: string; /** Public input type before workspace query lowering adds private projections. */ input?: ValueType; children: ReactChildDeclaration[]; /** Binds roots/arguments only. The ordinary checked Query Tree owns every * acquisition path; the React declaration does not repeat its descendants. */ trees?: Pick[]; } export interface ReactPreparationArtifact { /** Frontend queries belong to installed preparation, not the authored client API. */ internalQueries?: string[]; modules: ReactModulePreparation[]; } /** Names are package imports/module declarations. All query bindings are passed * to Camino's ordinary composition checker after generated queries are checked. */ export function presentationDeclarations( text: string, pkg: PackageRevision, interfaces: readonly InterfaceRevision[], ): { compositions: QueryComposition[]; artifact: ReactPreparationArtifact } { const source: unknown = JSON.parse(text); const object = (value: unknown, label: string): Record => { if (!value || typeof value !== "object" || Array.isArray(value)) throw Error(`${label}: expected an object`); return value as Record; }; const top = object(source, "quixos.react.json"); if (Object.keys(top).some((key) => key !== "modules")) throw Error("quixos.react.json: unknown field"); const modules = object(top.modules, "modules"); const compositions: QueryComposition[] = []; const artifact: ReactPreparationArtifact = { modules: [] }; const imported = (name: string) => { const id = pkg.clientInterfaces?.[name]; if (!id) throw Error(`React declaration: unknown imported interface ${name}`); return id; }; for (const [name, value] of Object.entries(modules)) { const declaration = object(value, `module ${name}`); if (Object.keys(declaration).some((key) => !["inputs", "children", "trees"].includes(key))) throw Error(`${name}: unknown preparation field`); const module = pkg.modules?.find((entry) => entry.displayName === name); if (!module) throw Error(`React declaration: unknown module ${name}`); const contract = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId); const member = contract?.members.find((entry) => entry.id === module.memberId); if ( member?.kind !== "module" || member.subject?.kind !== "interface" || !["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes( member.contract, ) ) throw Error(`${name}: preparation requires a React module contract`); const fields: Record = { subject: { kind: "object-ref", expectation: { kind: "interface", interfaceRevisionId: member.subject.interfaceRevisionId }, }, }; if (member.contract === "org.quixos.react.component/2") { const state = contract?.application?.arguments[1]; if (state?.kind !== "object" || state.target.kind !== "interface") throw Error(`${name}: expected a closed state interface`); fields.navigation = { kind: "object-ref", expectation: { kind: "interface", interfaceRevisionId: state.target.interfaceRevisionId }, }; } if (!Array.isArray(declaration.inputs)) throw Error(`${name}: inputs must be an array of query bindings`); const steps = declaration.inputs.map((value) => { const step = object(value, `${name} input`) as unknown as QueryCompositionStep; if (step.name === "children") throw Error(`${name}: children is reserved for prepared child inputs`); const query = pkg.queries?.find((entry) => entry.displayName === step.query); if (!query) throw Error(`${name}.${step.name}: unknown query ${step.query}`); if (query.placement?.kind !== "local" && !query.fragment) throw Error(`${name}.${step.name}: requires a local projection or fragment`); return { ...step, query: query.id }; }); const composition = `react:inputs:${module.id}`; compositions.push({ id: composition, input: { kind: "record", fields }, steps, maxQueries: steps.reduce((count, step) => count + (step.each?.limit ?? 1), 0) || 1, }); const children = declaration.children ?? []; if (!Array.isArray(children)) throw Error(`${name}: children must be an array`); const names = new Set(); const parsed = children.map((value) => { const child = object(value, `${name} child`) as unknown as ReactChildDeclaration; if (!child.name || names.has(child.name) || !child.slot || !child.root) throw Error(`${name}: child requires a unique name, slot and root`); names.add(child.name); // A local module identifies a closed role, including its generic arguments. // This avoids inventing an import alias for a generic application solely // to select that role in the React preparation manifest. let role: ReactChildDeclaration["role"]; if (child.role) { const authored = object(child.role, `${name}.${child.name} role`); if (typeof authored.module === "string") { const target = pkg.modules?.find((entry) => entry.displayName === authored.module); if (!target || Object.keys(authored).length !== 1) throw Error(`${name}.${child.name}: expected a local module role`); role = { interface: target.interfaceRevisionId, member: target.memberId }; } else { role = { ...child.role, interface: imported(child.role.interface) }; } } if (child.each && child.with) throw Error(`${name}.${child.name}: cannot combine each and with`); if (child.each && (!Number.isSafeInteger(child.each.limit) || child.each.limit < 1)) throw Error(`${name}.${child.name}: finite positive collection limit required`); return { ...child, subject: imported(child.subject), ...(role ? { role } : {}), }; }); const trees = declaration.trees ?? []; if (!Array.isArray(trees) || trees.length > 16) throw Error(`${name}: trees must be a bounded array`); const treeNames = new Set(); const preparedTrees = trees.map((value) => { const tree = object(value, `${name} tree`) as unknown as NonNullable[number]; if ( Object.keys(tree).some((key) => !["name", "query", "root", "arguments"].includes(key)) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(tree.name) || treeNames.has(tree.name) ) throw Error(`${name}: tree requires a unique name and checked root/arguments`); treeNames.add(tree.name); const query = pkg.queries?.find((entry) => entry.displayName === tree.query); if (!query || query.placement?.kind !== "replica") throw Error(`${name}.${tree.name}: requires a replica Query Tree`); return { ...tree, query: query.id }; }); artifact.modules.push({ module: module.id, composition, children: parsed, ...(preparedTrees.length ? { trees: preparedTrees } : {}), }); } for (const module of pkg.modules ?? []) { const contract = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId); const member = contract?.members.find((entry) => entry.id === module.memberId); if ( member?.kind === "module" && member.contract.startsWith("org.quixos.react.") && !artifact.modules.some((entry) => entry.module === module.id) ) throw Error(`React module ${module.displayName} has no preparation declaration`); } return { compositions, artifact }; }