Build reusable React component hosting and typed fragment composition
This commit is contained in:
@@ -99,6 +99,7 @@ export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContr
|
||||
export function generateInterfaceClientBindings(
|
||||
imports: Readonly<Record<string, InterfaceRevision>>,
|
||||
runtimeModule = "@quixos/camino-replica-core",
|
||||
moduleProps?: (interfaceId: string, memberId: string) => string,
|
||||
): string {
|
||||
const entries = Object.entries(imports).map(([alias, iface]) => {
|
||||
if (iface.template)
|
||||
@@ -108,6 +109,10 @@ export function generateInterfaceClientBindings(
|
||||
"Specialize the interface before generating a client",
|
||||
);
|
||||
const members = iface.members.map((member) => {
|
||||
if (member.kind === "module" && member.subject?.kind === "interface") {
|
||||
const subject = member.subject.interfaceRevisionId;
|
||||
return `${JSON.stringify(member.displayName)}: defineModuleMember<${JSON.stringify(subject)}, ${moduleProps?.(iface.revisionId, member.id) ?? "{}"}>(${JSON.stringify({ interface: iface.revisionId, member: member.id, subject, contract: member.contract })})`;
|
||||
}
|
||||
const operations = member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.map(
|
||||
@@ -118,7 +123,7 @@ export function generateInterfaceClientBindings(
|
||||
});
|
||||
return `${JSON.stringify(alias)}: {${members.join(",\n")}}`;
|
||||
});
|
||||
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
|
||||
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineModuleMember, defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
|
||||
}
|
||||
|
||||
export function generatePackageQueryClientBindings(
|
||||
@@ -203,8 +208,10 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
Object.entries(pkg.clientInterfaces ?? {}).map(([alias, id]) => [alias, find(id)]),
|
||||
);
|
||||
const subjects = Object.fromEntries(components.map((component) => [component.displayName, find(component.subject)]));
|
||||
const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react");
|
||||
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react")
|
||||
const props = (iface: string, member: string) =>
|
||||
`ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`;
|
||||
const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props);
|
||||
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props)
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith("import "))
|
||||
.join("\n")
|
||||
@@ -219,10 +226,20 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
.join("\n");
|
||||
return (
|
||||
interfaces +
|
||||
'\nimport {definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' +
|
||||
'\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' +
|
||||
subjectBindings +
|
||||
"\n" +
|
||||
queries +
|
||||
`\nexport const providers = {${Object.entries(pkg.clientPackages ?? {})
|
||||
.map(([alias, id]) => {
|
||||
const imported = plan.definition.workspace.packageImports.find((entry) => entry.revisionId === id);
|
||||
if (!imported) throw new ExecutionPlanError("UNINSTALLED_PACKAGE", id, "Missing imported module provider");
|
||||
return `${JSON.stringify(alias)}:{${(imported.modules ?? []).map((module) => `${JSON.stringify(module.displayName)}:${JSON.stringify({ packageRevision: id, id: module.id })}`).join(",")}}`;
|
||||
})
|
||||
.join(",")}} as const;\n` +
|
||||
`\nexport const renderers = {${Object.entries(imports)
|
||||
.map(([name, iface]) => `${JSON.stringify(name)}:createObjectRenderer(${JSON.stringify(iface.revisionId)})`)
|
||||
.join(",\n")}} as const;\n` +
|
||||
"\nexport interface ModuleProps {}\n" +
|
||||
"export type ExtraModuleProps<K extends string> = K extends keyof ModuleProps ? ModuleProps[K] : {};\n" +
|
||||
"export interface Modules {\n" +
|
||||
|
||||
@@ -29,6 +29,13 @@ export interface ClientContractCatalog {
|
||||
readonly queries: readonly PublicQueryContract[];
|
||||
readonly components: readonly InstalledComponent[];
|
||||
readonly presentations: readonly { id: string; subject: string; packageRevision: string; component: string }[];
|
||||
readonly moduleBindings: readonly {
|
||||
atom: string;
|
||||
interface: string;
|
||||
member: string;
|
||||
packageRevision: string;
|
||||
id: string;
|
||||
}[];
|
||||
}
|
||||
/** Explicit projection prevents accidental spreading of filesystem roots or private bindings. */
|
||||
export function clientContractCatalog(
|
||||
@@ -68,6 +75,17 @@ export function clientContractCatalog(
|
||||
(pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id)),
|
||||
),
|
||||
components: installation.components.filter((c) => requestedPackages.includes(c.packageRevision)),
|
||||
moduleBindings: plan.definition.workspace.conformances.flatMap((c) =>
|
||||
(c.moduleBindings ?? [])
|
||||
.filter((b) => requestedPackages.includes(b.packageRevisionId))
|
||||
.map((b) => ({
|
||||
atom: c.atomId,
|
||||
interface: c.interfaceRevisionId,
|
||||
member: b.memberId,
|
||||
packageRevision: b.packageRevisionId,
|
||||
id: b.moduleId,
|
||||
})),
|
||||
),
|
||||
presentations: (plan.definition.presentations ?? [])
|
||||
.filter((p) => requestedPackages.includes(p.packageRevisionId))
|
||||
.map((p) => ({ id: p.id, subject: p.subject, packageRevision: p.packageRevisionId, component: p.componentId })),
|
||||
@@ -99,6 +117,12 @@ export interface ExecutionInstallation {
|
||||
}
|
||||
/** Safe to expose in a presentation catalog: no source paths, state slots, or host configuration. */
|
||||
export interface InstalledComponent {
|
||||
readonly role: {
|
||||
readonly interface: string;
|
||||
readonly member: string;
|
||||
readonly subject: string;
|
||||
readonly contract: string;
|
||||
};
|
||||
readonly packageRevision: string;
|
||||
readonly id: string;
|
||||
readonly subject: string;
|
||||
@@ -176,6 +200,17 @@ function assembleExecutionInstallation(
|
||||
routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType };
|
||||
}
|
||||
components.push({
|
||||
role: (() => {
|
||||
const module = plan.definition.packages
|
||||
.find((p) => p.revision === revision)!
|
||||
.modules!.find((m) => m.id === component.id)!;
|
||||
return {
|
||||
interface: module.interfaceRevisionId,
|
||||
member: module.memberId,
|
||||
subject: component.subject,
|
||||
contract: module.contract,
|
||||
};
|
||||
})(),
|
||||
packageRevision: revision,
|
||||
id: component.id,
|
||||
subject: component.subject,
|
||||
|
||||
Reference in New Issue
Block a user