Add checked React field bindings and Web Studio factories
Replace opaque props with checked generic presentation contracts and lazy typed interface references. Generate readonly/writable field APIs and component checks. Preserve CRDT editing through explicit resolved getter/setter contracts, binding- fenced delta RPCs, native watches and replica-aware field adapters. Custom setters retain semantic writes; storage snapshots never grant write authority. Cover concurrent edits, lost acknowledgements, readonly contracts and authorization. Add receiver-free static factory dispatch, state-field binding shorthand, and conformance-based creation. Migrate TODO, editable scaffolds and authoring guides. Verify language/codegen, SDK, RPC, browser lifecycle, local scaffolds, production browser bundling and CRDT persistence with temporary PostgreSQL.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
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";
|
||||
|
||||
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" {
|
||||
value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; }
|
||||
value summary id "summary" : string { get id "summary:get"; }
|
||||
}`,
|
||||
{ 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;};
|
||||
}`,
|
||||
{ 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");
|
||||
const schema = {
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: [iface.resource.revision],
|
||||
packages: [pkg.resource.revision],
|
||||
} as const;
|
||||
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"),
|
||||
`import {useLiveField, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
|
||||
import type {ReactResults} from "./react-props.gen.js";
|
||||
declare const props: ReactResults["props"];
|
||||
const [title, setTitle] = useLiveField(props.fields.fields.title);
|
||||
setTitle("new");
|
||||
// @ts-expect-error wrong setter value
|
||||
setTitle(123);
|
||||
// @ts-expect-error read-only hook has no setter
|
||||
const [summary, setSummary] = useLiveField(props.fields.fields.summary);
|
||||
const [manual, write] = useLiveField(props.fields.fields.summary, {write: async (value: string) => {}});
|
||||
write("new");
|
||||
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
|
||||
useLiveField(props.fields.fields.summary, {write: async (value: number) => {}});
|
||||
`,
|
||||
);
|
||||
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"],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user