2026-09-16 11:25:20 -07:00
import assert from "node:assert/strict" ;
import test from "node:test" ;
import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import { spawnSync } from "node:child_process" ;
import { compileCapabilityResourceSource , compileCapabilitySource } from "../src/capability-language/parser.js" ;
import { generateReactBindings } from "../src/bindings/react.js" ;
import { reactPlatformTypes } from "../src/bindings/react-platform.js" ;
2026-09-18 01:10:48 -07:00
import { compileQuery } from "../src/query/compile.js" ;
2026-09-16 11:25:20 -07:00
const source = { repository : "https://example.test/fields.git" , commit : "a" . repeat ( 40 ) };
test ( "React bindings preserve read-only, writable and nested reference contracts" , async ( t ) => {
const iface = compileCapabilityResourceSource (
`interface Fields id "fields" revision "fields@1" {
2026-09-18 01:10:48 -07:00
queryable value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; }
queryable value summary id "summary" : string { get id "summary:get"; }
2026-09-16 11:25:20 -07:00
}` ,
{ source },
);
assert . ok ( iface . ok && iface . resource . kind === "interface" );
if ( ! iface . ok || iface . resource . kind !== "interface" ) throw new Error ( "interface failed" );
const pkg = compileCapabilityResourceSource (
`import interface Fields; package P id "p" revision "p@1" {
function props id "props" : unit -> record {fields: interface-ref<Fields>; caption: string;};
2026-09-18 01:10:48 -07:00
query Editor id "editor" root Fields document "editor.graphql" operation "Editor" {max rows 1; watch;}
2026-09-16 11:25:20 -07:00
}` ,
{ source , environment : { interfaces : new Map ([[ "Fields" , iface . resource . revision ]]) } },
);
assert . ok ( pkg . ok && pkg . resource . kind === "package" );
if ( ! pkg . ok || pkg . resource . kind !== "package" ) throw new Error ( "package failed" );
2026-09-18 01:10:48 -07:00
pkg . resource . revision . checkedQueries = [
await compileQuery (
pkg . resource . revision . queries ! [ 0 ] ! ,
[ iface . resource . revision ],
async () => "query Editor {root {title @live summary @live plain: title}}" ,
),
];
2026-09-16 11:25:20 -07:00
const schema = {
format : "quixos-bindings" ,
version : 1 ,
interfaces : [ iface . resource . revision ],
packages : [ pkg . resource . revision ],
} as const ;
2026-09-17 11:29:56 -07:00
const opaque = structuredClone ( iface . resource . revision );
opaque . revisionId = "opaque@1" as typeof opaque . revisionId ;
opaque . displayName = "Opaque" ;
for ( const member of opaque . members )
for ( const operation of member . operations )
operation . outputType = { kind : "message" , descriptorId : "UnsupportedBrowserMessage" };
const withUnusedOpaque = generateReactBindings (
{ ... schema , interfaces : [... schema . interfaces , opaque ], packages : [... schema . packages ] },
"p@1" ,
[ "props" ],
);
assert . doesNotMatch ( withUnusedOpaque , /"opaque@1": defineReactInterfaceContract/ );
2026-09-16 11:25:20 -07:00
const generated = generateReactBindings (
{ ... schema , interfaces : [... schema . interfaces ], packages : [... schema . packages ] },
"p@1" ,
[ "props" ],
);
assert . match ( generated , /"title": WritableField<string>/ );
assert . match ( generated , /"summary": ReadableField<string>/ );
assert . throws (
() => generateReactBindings ({ ... schema , interfaces : [], packages : [... schema . packages ] }, "p@1" , [ "props" ]),
/Missing React reference/ ,
);
const root = await fs . mkdtemp ( path . join ( os . tmpdir (), "qx-react-types-" ));
t . after (() => fs . rm ( root , { recursive : true , force : true }));
await fs . writeFile ( path . join ( root , "react-props.gen.ts" ), generated );
await fs . writeFile (
path . join ( root , "platform.d.ts" ),
reactPlatformTypes +
'\ndeclare module "react" {export type ReactNode = unknown; export type CSSProperties = {}; export function createElement(...args: unknown[]): unknown;}\n' ,
);
await fs . writeFile (
path . join ( root , "consumer.ts" ),
2026-09-18 13:27:07 -07:00
`import {useLiveField, tryConform, detachField, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
2026-09-17 11:29:56 -07:00
import {reactInterfaces} from "./react-props.gen.js";
2026-09-18 01:10:48 -07:00
import type {ReactResults, QueryResults} from "./react-props.gen.js";
declare const query: QueryResults["Editor"];
useLiveField(query.root.title).set("new");
2026-09-18 13:27:07 -07:00
useLiveField(detachField(query.root.title)).set("new");
// @ts-expect-error detachment preserves readonly access
useLiveField(detachField(query.root.summary)).set("no");
// @ts-expect-error query handles do not expose transport metadata
query.root.title.capability;
2026-09-18 01:10:48 -07:00
// @ts-expect-error live query fields retain exact setter types
useLiveField(query.root.title).set(123);
// @ts-expect-error readonly query fields do not acquire a setter
useLiveField(query.root.summary).set("no");
const plain: string = query.root.plain;
2026-09-17 11:29:56 -07:00
async function lookup() {
const view = await tryConform("object", reactInterfaces.Fields);
if (!view) return;
await view.call["title.set"]("works");
// @ts-expect-error exact checked setter input
await view.call["title.set"](123);
// @ts-expect-error no invented operation
await view.call["summary.set"]("no setter");
}
2026-09-16 11:25:20 -07:00
declare const props: ReactResults["props"];
2026-09-18 01:10:48 -07:00
const title = useLiveField(props.fields.fields.title);
title.set("new");
2026-09-16 11:25:20 -07:00
// @ts-expect-error wrong setter value
2026-09-18 01:10:48 -07:00
title.set(123);
2026-09-16 11:25:20 -07:00
// @ts-expect-error read-only hook has no setter
2026-09-18 01:10:48 -07:00
useLiveField(props.fields.fields.summary).set("no");
const manual = useLiveField(props.fields.fields.summary, {id: "manual", write: async (value: string) => {}});
manual.set("new");
2026-09-16 11:25:20 -07:00
const readonly: ReadableField<string> = props.fields.fields.title;
// @ts-expect-error read-only does not satisfy writable
const writable: WritableField<string> = props.fields.fields.summary;
declare const narrow: WritableField<"only">;
// @ts-expect-error writable references are invariant
const widened: WritableField<string> = narrow;
// @ts-expect-error callbacks must accept the field's type
2026-09-18 01:10:48 -07:00
useLiveField(props.fields.fields.summary, {id: "manual", write: async (value: number) => {}});
2026-09-16 11:25:20 -07:00
` ,
);
const result = spawnSync (
process . execPath ,
[
path . resolve ( "node_modules/typescript/bin/tsc" ),
"--strict" ,
"--noEmit" ,
"--skipLibCheck" ,
"--target" ,
"ES2022" ,
path . join ( root , "platform.d.ts" ),
path . join ( root , "consumer.ts" ),
],
{ encoding : "utf8" , cwd : root },
);
assert . equal ( result . status , 0 , result . stdout + result . stderr );
await fs . writeFile (
path . join ( root , "react-props.gen.ts" ),
generateReactBindings (
{ ... schema , interfaces : [... schema . interfaces ], packages : [... schema . packages ] },
"p@1" ,
[ "props" ],
[{ module : "./component.js" , propsExport : "props" }],
),
);
const checkComponent = () =>
spawnSync (
process . execPath ,
[
path . resolve ( "node_modules/typescript/bin/tsc" ),
"--strict" ,
"--noEmit" ,
"--skipLibCheck" ,
"--target" ,
"ES2022" ,
path . join ( root , "platform.d.ts" ),
path . join ( root , "react-props.gen.ts" ),
path . join ( root , "component.ts" ),
],
{ encoding : "utf8" , cwd : root },
);
await fs . writeFile (
path . join ( root , "component.ts" ),
'import type {ReactResults} from "./react-props.gen.js"; export default function Component(props: {camino: ReactResults["props"]}) {return null;}' ,
);
const matching = checkComponent ();
assert . equal ( matching . status , 0 , matching . stdout + matching . stderr );
await fs . writeFile (
path . join ( root , "component.ts" ),
'import type {WritableField} from "@quixos/web-studio-react-runtime"; export default function Component(props: {camino: {fields: {fields: {summary: WritableField<string>}}}}) {return null;}' ,
);
const incompatible = checkComponent ();
assert . notEqual ( incompatible . status , 0 , "component cannot strengthen a read-only prop into a writable field" );
assert . match ( incompatible . stdout + incompatible . stderr , /writable|WritableField/ );
});
test ( "class factories specialize return types and reject instance implementations or mismatched inputs" , () => {
const factory = compileCapabilityResourceSource (
'interface Factory<object T> id "factory" revision "factory@1" {static operation create id "create" : unit -> ref<T> {call id "call";}}' ,
{ source },
);
assert . ok ( factory . ok && factory . resource . kind === "interface" );
if ( ! factory . ok || factory . resource . kind !== "interface" ) throw new Error ( "factory failed" );
const factoryRevision = factory . resource . revision ;
const compile = ( declaration : string ) => {
const pkg = compileCapabilityResourceSource (
`external atom Note id "note"; package P id "p" revision "p@1" { ${ declaration } }` ,
{ source },
);
assert . ok ( pkg . ok && pkg . resource . kind === "package" , JSON . stringify ( pkg . diagnostics ));
if ( ! pkg . ok || pkg . resource . kind !== "package" ) throw new Error ( "package failed" );
return compileCapabilitySource (
`workspace W id "w" revision "w@1" commit " ${ source . commit } " {
atom Note id "note"; import interface Factory; import package P;
conform Note as Factory<atom Note> id "factory-conformance" {bind create.call to package P.make;}
}` ,
"workspace.qx" ,
{ interfaces : new Map ([[ "Factory" , factoryRevision ]]), packages : new Map ([[ "P" , pkg . resource . revision ]]) },
);
};
const good = compile ( 'function make id "make" : unit -> atom-ref<Note>;' );
assert . ok ( good . ok , JSON . stringify ( good . diagnostics ));
assert . equal ( good . workspace . interfaceImports [ 0 ]. members [ 0 ]. operations [ 0 ]. scope , "class" );
const wrongInput = compile ( 'function make id "make" : string -> atom-ref<Note>;' );
assert . equal ( wrongInput . ok , false );
const wrongReceiver = compile ( 'operation make id "make" : unit -> atom-ref<Note> mode call receiver atom Note;' );
assert . equal ( wrongReceiver . ok , false );
assert . match ( JSON . stringify ( wrongReceiver . diagnostics ), /free function/ );
});
test ( "state-field shorthand binds exactly the declared accessors, never invents writes" , () => {
const iface = compileCapabilityResourceSource (
'interface Reader id "reader" revision "reader@1" {value title id "title" : string {get id "get"; watch start id "watch" stop id "stop";}}' ,
{ source },
);
assert . ok ( iface . ok && iface . resource . kind === "interface" );
if ( ! iface . ok || iface . resource . kind !== "interface" ) throw new Error ( "interface failed" );
const result = compileCapabilitySource (
`workspace W id "w" revision "w@1" commit " ${ source . commit } " {
atom Note id "note"; import interface Reader;
conform Note as Reader id "reader-conformance" {private state Title id "title-slot" on Note : string policy crdt(string) default ""; bind title to state Title;}
}` ,
"workspace.qx" ,
{ interfaces : new Map ([[ "Reader" , iface . resource . revision ]]) },
);
assert . ok ( result . ok , JSON . stringify ( result . diagnostics ));
assert . deepEqual (
result . workspace . conformances [ 0 ]. operationBindings . map (( binding ) => binding . operationId ),
[ "get" , "watch" , "stop" ],
);
});