2026-09-16 11:25:20 -07:00
import type { BindingSchema } from "./index.js" ;
import type { ValueType } from "../capability-model/types.js" ;
2026-09-18 01:10:48 -07:00
import { queryPresentation } from "../query/presentation.js" ;
2026-09-16 11:25:20 -07:00
/** 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"]>;` ;
});
2026-09-17 11:29:56 -07:00
// Emit optional discovery descriptors only when the browser has all codecs.
// Unrelated opaque-message imports must not break an otherwise checked UI.
const browserValue = ( value : ValueType , seen : Set < string >) : boolean => {
if ( value . kind === "message" ) return false ;
if ( value . kind === "builtin" ) return value . name === "unit" ;
if ( value . kind === "record" ) return Object . values ( value . fields ). every (( field ) => browserValue ( field , seen ));
if ( value . kind === "list" || value . kind === "optional" ) return browserValue ( value . value , seen );
if ( value . kind === "object-ref" && value . expectation . kind === "interface" ) {
const id = value . expectation . interfaceRevisionId ;
if ( seen . has ( id )) return true ;
const contract = schema . interfaces . find (( entry ) => entry . revisionId === id );
return !! contract && browserInterface ( contract , new Set ([... seen , id ]));
}
return true ;
};
const browserInterface = ( iface : ( typeof schema . interfaces )[ number ], seen : Set < string >) : boolean =>
iface . members . every (( member ) =>
member . operations
. filter (( op ) => op . mode === "call" && op . scope !== "class" )
. every (( op ) => browserValue ( op . inputType , seen ) && browserValue ( op . outputType , seen )),
);
const contracts = schema . interfaces
. filter (( iface ) => browserInterface ( iface , new Set ([ iface . revisionId ])))
. map (( iface ) => {
const reference = type ({
kind : "object-ref" ,
expectation : { kind : "interface" , interfaceRevisionId : iface.revisionId },
});
const calls = iface . members . flatMap (( member ) =>
member . operations
. filter (( operation ) => operation . mode === "call" && operation . scope !== "class" )
. map (
( operation ) =>
` ${ q ( ` ${ member . displayName } . ${ operation . displayName } ` ) } : ( ${ operation . inputType . kind === "builtin" && operation . inputType . name === "unit" ? "" : `input: ${ type ( operation . inputType ) } ` } ) => Promise< ${ type ( operation . outputType ) } >` ,
),
);
return { iface , view : ` ${ reference } & {call: { ${ calls . join ( ";" ) } }}` };
});
2026-09-17 14:34:16 -07:00
const queryType = ( value : ValueType ) : string => {
if ( value . kind === "object-ref" )
return `QueryReference< ${ q ( value . expectation . kind === "atom" ? value.expectation.atomId : value.expectation.interfaceRevisionId ) } >` ;
if ( value . kind === "optional" ) return `( ${ queryType ( value . value ) } | null)` ;
if ( value . kind === "list" ) return `Array< ${ queryType ( value . value ) } >` ;
if ( value . kind === "record" )
return `{ ${ Object . entries ( value . fields )
. map (([ name , field ]) => ` ${ q ( name ) } : ${ queryType ( field ) } ` )
. join ( ";" ) } }` ;
return type ( value );
};
const queries = pkg . checkedQueries ?? [];
2026-09-18 01:10:48 -07:00
const presentedType = (
value : ValueType ,
fields : ReturnType < typeof queryPresentation >,
path : string [] = [],
) : string => {
const field = fields . find (( field ) => JSON . stringify ( field . path ) === JSON . stringify ( path ));
if ( field )
return ` ${ field . conditions . length ? "(" : "" }${ field . setOperationId ? "WritableField" : "ReadableField" } < ${ type ( field . valueType ) } > ${ field . conditions . length ? " | null)" : "" } ` ;
if ( value . kind === "optional" ) return `( ${ presentedType ( value . value , fields , path ) } | null)` ;
if ( value . kind === "list" ) return `Array< ${ presentedType ( value . value , fields , [... path , "*" ]) } >` ;
if ( value . kind === "record" )
return `{ ${ Object . entries ( value . fields )
. map (([ key , child ]) => ` ${ q ( key ) } : ${ presentedType ( child , fields , [... path , key ]) } ` )
. join ( ";" ) } }` ;
return queryType ( value );
};
2026-09-17 14:34:16 -07:00
const queryCode =
`export type QueryVariables = { ${ queries . map (( query ) => ` ${ q ( query . declaration . displayName ) } : ${ queryType ( query . variables ) } ` ). join ( ";" ) } }; \ n` +
2026-09-18 01:10:48 -07:00
`export type QueryResults = { ${ queries . map (( query ) => ` ${ q ( query . declaration . displayName ) } : ${ presentedType ( query . output , queryPresentation ( query , schema . interfaces )) } ` ). join ( ";" ) } }; \ n` +
2026-09-17 14:34:16 -07:00
`export const queries: { ${ queries . map (( query ) => ` ${ q ( query . declaration . displayName ) } : QueryDescriptor<QueryVariables[ ${ q ( query . declaration . displayName ) } ], QueryResults[ ${ q ( query . declaration . displayName ) } ]>` ). join ( ";" ) } } = ${ JSON . stringify (
Object . fromEntries (
queries . map (( query ) => [
query . declaration . displayName ,
{
id : ` ${ pkg . revisionId } : ${ query . declaration . id } ` ,
definitionDigest : query.definitionDigest ,
rootInterfaceRevisionId : query.declaration.root ,
variables : query.variables ,
output : query.output ,
2026-09-18 01:10:48 -07:00
liveFields : queryPresentation ( query , schema . interfaces ),
2026-09-17 14:34:16 -07:00
watch : query.declaration.watch ,
} ,
]),
),
)}; \ n` ;
2026-09-16 11:25:20 -07:00
return (
2026-09-17 14:34:16 -07:00
`// Generated from checked QX contracts. Do not edit. \ nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime"; \ nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, QueryDescriptor, QueryReference} from "@quixos/web-studio-react-runtime"; \ n ${ declarations . join ( "\n" ) } \ nexport type ReactResults = { ${ results . join ( ";\n" ) } }; \ n` +
queryCode +
2026-09-17 11:29:56 -07:00
`const contractsData = ${ JSON . stringify ( schema . interfaces ) } ; \ n` +
`export const reactContracts = { ${ contracts . map (({ iface , view } ) => ` $ { q ( iface . revisionId )} : defineReactInterfaceContract < $ { view } > ( $ { q ( iface . revisionId )}, contractsData ) `).join(", \ n")}}; \ n` +
`export const reactInterfaces = { ${ contracts
. filter (({ iface } ) => contracts.filter((other) => other.iface.displayName === iface.displayName).length === 1)
.map(({ iface }) => ` $ { q ( iface . displayName )} : reactContracts [ $ { q ( iface . revisionId )}] `)
.join(", \ n")}}; \ n` +
2026-09-16 11:25:20 -07:00
( 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`
: "" )
);
}