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:
@@ -95,7 +95,7 @@ interfaceMember
|
|||||||
;
|
;
|
||||||
|
|
||||||
operationMember
|
operationMember
|
||||||
: OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
|
: STATIC? OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
|
||||||
LBRACE CALL ID stringLiteral SEMI RBRACE
|
LBRACE CALL ID stringLiteral SEMI RBRACE
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -239,9 +239,14 @@ conformanceDecl
|
|||||||
conformanceItem
|
conformanceItem
|
||||||
: PRIVATE attachmentDecl
|
: PRIVATE attachmentDecl
|
||||||
| operationBindingDecl
|
| operationBindingDecl
|
||||||
|
| stateFieldBindingDecl
|
||||||
| relationshipMaterializationDecl
|
| relationshipMaterializationDecl
|
||||||
;
|
;
|
||||||
|
|
||||||
|
stateFieldBindingDecl
|
||||||
|
: BIND identifier TO STATE identifier SEMI
|
||||||
|
;
|
||||||
|
|
||||||
relationshipMaterializationDecl
|
relationshipMaterializationDecl
|
||||||
: MATERIALIZE identifier IF ABSENT USING CONSTRUCTOR identifier VIA EDGE identifier DOT identifier SEMI
|
: MATERIALIZE identifier IF ABSENT USING CONSTRUCTOR identifier VIA EDGE identifier DOT identifier SEMI
|
||||||
;
|
;
|
||||||
@@ -395,6 +400,7 @@ INPUT: 'input';
|
|||||||
CONFORM: 'conform';
|
CONFORM: 'conform';
|
||||||
AS: 'as';
|
AS: 'as';
|
||||||
BIND: 'bind';
|
BIND: 'bind';
|
||||||
|
STATIC: 'static';
|
||||||
TO: 'to';
|
TO: 'to';
|
||||||
PRIVATE: 'private';
|
PRIVATE: 'private';
|
||||||
SHARED: 'shared';
|
SHARED: 'shared';
|
||||||
|
|||||||
+44
-3
@@ -9,6 +9,8 @@ import "quixos/runtime.proto";
|
|||||||
|
|
||||||
service OrchestratorRuntime {
|
service OrchestratorRuntime {
|
||||||
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
|
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||||
|
rpc EditCapabilityField(EditCapabilityFieldRequest) returns (InvokeCapabilityResponse);
|
||||||
|
rpc InvokeClassCapability(InvokeClassCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||||
rpc WatchCapability(WatchCapabilityRequest) returns (stream WatchCapabilityEvent);
|
rpc WatchCapability(WatchCapabilityRequest) returns (stream WatchCapabilityEvent);
|
||||||
rpc ConstructObject(ConstructObjectRequest) returns (ConstructObjectResponse);
|
rpc ConstructObject(ConstructObjectRequest) returns (ConstructObjectResponse);
|
||||||
rpc ResolveOrConstructRelatedObject(ResolveOrConstructRelatedObjectRequest)
|
rpc ResolveOrConstructRelatedObject(ResolveOrConstructRelatedObjectRequest)
|
||||||
@@ -46,6 +48,11 @@ message InvokeCapabilityRequest {
|
|||||||
// layers so a live-value controller can recognize its own confirmation.
|
// layers so a live-value controller can recognize its own confirmation.
|
||||||
string client_mutation_id = 4;
|
string client_mutation_id = 4;
|
||||||
}
|
}
|
||||||
|
message InvokeClassCapabilityRequest {
|
||||||
|
string conformance_id = 1;
|
||||||
|
string operation_id = 2;
|
||||||
|
map<string, camino.Value> input = 3;
|
||||||
|
}
|
||||||
message InvokeCapabilityResponse {
|
message InvokeCapabilityResponse {
|
||||||
string invocation_id = 1;
|
string invocation_id = 1;
|
||||||
Activation activation = 2;
|
Activation activation = 2;
|
||||||
@@ -53,6 +60,24 @@ message InvokeCapabilityResponse {
|
|||||||
camino.Value result = 4;
|
camino.Value result = 4;
|
||||||
string error = 5;
|
string error = 5;
|
||||||
repeated quixos.runtime.DerivedDependency dependencies = 6;
|
repeated quixos.runtime.DerivedDependency dependencies = 6;
|
||||||
|
FieldEditing field_editing = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolved from the checked native getter/setter binding, not Value.source.
|
||||||
|
message FieldEditing {
|
||||||
|
string getter_operation_id = 1;
|
||||||
|
string setter_operation_id = 2;
|
||||||
|
string document_type = 3;
|
||||||
|
string binding_digest = 4;
|
||||||
|
}
|
||||||
|
message EditCapabilityFieldRequest {
|
||||||
|
// The public getter; setter must belong to the same value member.
|
||||||
|
quixos.CapabilityRef capability = 1;
|
||||||
|
string object_id = 2;
|
||||||
|
string setter_operation_id = 3;
|
||||||
|
string binding_digest = 4;
|
||||||
|
camino.CrdtValue update = 5;
|
||||||
|
string client_mutation_id = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WatchCapabilityRequest {
|
message WatchCapabilityRequest {
|
||||||
@@ -68,18 +93,34 @@ message WatchCapabilityEvent {
|
|||||||
repeated quixos.runtime.DerivedDependency dependencies = 5;
|
repeated quixos.runtime.DerivedDependency dependencies = 5;
|
||||||
string error = 6;
|
string error = 6;
|
||||||
bool initial = 7;
|
bool initial = 7;
|
||||||
|
FieldEditing field_editing = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetWorkspaceRequest {}
|
message GetWorkspaceRequest {
|
||||||
|
// Revision polling must not download the entire interface graph.
|
||||||
|
bool include_interface_contracts = 1;
|
||||||
|
}
|
||||||
message GetWorkspaceResponse {
|
message GetWorkspaceResponse {
|
||||||
string workspace_id = 1;
|
string workspace_id = 1;
|
||||||
string workspace_revision_id = 2;
|
string workspace_revision_id = 2;
|
||||||
string source_root_commit = 3;
|
string source_root_commit = 3;
|
||||||
// Checked constructors whose wire input can be empty. Web Studio intersects
|
// Checked constructors whose wire input can be empty. The create panel uses
|
||||||
// this with its temporary Createable marker; the marker is not a factory.
|
// class factory conformances instead of this constructor inventory.
|
||||||
repeated string empty_input_constructible_atom_ids = 4;
|
repeated string empty_input_constructible_atom_ids = 4;
|
||||||
repeated CapabilityInputContract capability_inputs = 5;
|
repeated CapabilityInputContract capability_inputs = 5;
|
||||||
repeated ConstructorInputContract constructor_inputs = 6;
|
repeated ConstructorInputContract constructor_inputs = 6;
|
||||||
|
// Exact closed interface contracts used by checked presentation consumers.
|
||||||
|
string interfaces_json = 7;
|
||||||
|
repeated ClassCapability class_capabilities = 8;
|
||||||
|
}
|
||||||
|
message ClassCapability {
|
||||||
|
string conformance_id = 1;
|
||||||
|
string atom_id = 2;
|
||||||
|
string interface_revision_id = 3;
|
||||||
|
string definition_id = 4;
|
||||||
|
string operation_id = 5;
|
||||||
|
string input_type_json = 6;
|
||||||
|
string output_type_json = 7;
|
||||||
}
|
}
|
||||||
message CapabilityInputContract {
|
message CapabilityInputContract {
|
||||||
string interface_revision_id = 1;
|
string interface_revision_id = 1;
|
||||||
|
|||||||
+8
-2
@@ -3,16 +3,22 @@ import { readFile, writeFile } from "node:fs/promises";
|
|||||||
import { generateTypeScriptBindings } from "./index.js";
|
import { generateTypeScriptBindings } from "./index.js";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { reactPlatformTypes } from "./react-platform.js";
|
import { reactPlatformTypes } from "./react-platform.js";
|
||||||
|
import { generateReactBindings } from "./react.js";
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
|
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
|
||||||
if (!schema || !revision || !output || rest.length)
|
if (!schema || !revision || !output || rest.length)
|
||||||
throw new Error("usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
|
throw new Error("usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
|
||||||
const config = options ? JSON.parse(await readFile(options, "utf8")) : {};
|
const config = options ? JSON.parse(await readFile(options, "utf8")) : {};
|
||||||
const generated = generateTypeScriptBindings(JSON.parse(await readFile(schema, "utf8")), revision, config);
|
const contracts = JSON.parse(await readFile(schema, "utf8"));
|
||||||
|
const generated = generateTypeScriptBindings(contracts, revision, config);
|
||||||
await writeFile(output, generated);
|
await writeFile(output, generated);
|
||||||
if (config.messages?.["org.quixos.web-studio.ReactProps"]) {
|
if (config.react) {
|
||||||
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
|
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
|
||||||
|
await writeFile(
|
||||||
|
path.join(path.dirname(output), "react-props.gen.ts"),
|
||||||
|
generateReactBindings(contracts, revision, config.react.propsExports, config.react.components),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
main().catch((error: unknown) => {
|
main().catch((error: unknown) => {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const genericImplementationType = (
|
|||||||
};
|
};
|
||||||
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
|
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
|
||||||
member.operations
|
member.operations
|
||||||
.filter((op) => op.mode === "call")
|
.filter((op) => op.mode === "call" && op.scope !== "class")
|
||||||
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
|
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
|
||||||
);
|
);
|
||||||
return object([
|
return object([
|
||||||
@@ -146,11 +146,21 @@ export const genericImplementationType = (
|
|||||||
.join(",");
|
.join(",");
|
||||||
const receiver = definition.receiverRequirement;
|
const receiver = definition.receiverRequirement;
|
||||||
const context = object([
|
const context = object([
|
||||||
["objectId", `QxObjectRef<${receiver.kind === "target" ? target(receiver.target, rootScope()) : "string"}>`],
|
...(definition.kind === "function"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
[
|
||||||
|
"objectId",
|
||||||
|
`QxObjectRef<${receiver.kind === "target" ? target(receiver.target, rootScope()) : "string"}>`,
|
||||||
|
] as [string, string],
|
||||||
|
]),
|
||||||
["input", value(definition.inputType, rootScope())],
|
["input", value(definition.inputType, rootScope())],
|
||||||
["ports", object(definition.dependencyPorts.map((entry) => [entry.displayName, port(entry)]))],
|
["ports", object(definition.dependencyPorts.map((entry) => [entry.displayName, port(entry)]))],
|
||||||
]);
|
]);
|
||||||
const contextWithLifecycle = `${context} & QxContextLifecycle<${context} & {signal?: AbortSignal}>`;
|
const contextWithLifecycle =
|
||||||
|
definition.kind === "function"
|
||||||
|
? `${context} & {signal?: AbortSignal}`
|
||||||
|
: `${context} & QxContextLifecycle<${context} & {signal?: AbortSignal}>`;
|
||||||
const result = value(definition.eventType ?? definition.outputType, rootScope());
|
const result = value(definition.eventType ?? definition.outputType, rootScope());
|
||||||
const handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
|
const handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
|
||||||
const derived = `{kind:"derived";get:${handler}}`;
|
const derived = `{kind:"derived";get:${handler}}`;
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export const generateTypeScriptBindings = (
|
|||||||
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
|
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
|
||||||
const operations = contract.members.flatMap((member) =>
|
const operations = contract.members.flatMap((member) =>
|
||||||
member.operations
|
member.operations
|
||||||
.filter((operation) => operation.mode === "call")
|
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||||
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
|
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -245,13 +245,15 @@ export const generateTypeScriptBindings = (
|
|||||||
? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>`
|
? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>`
|
||||||
: "QxObjectRef<string>";
|
: "QxObjectRef<string>";
|
||||||
const contextShape = object([
|
const contextShape = object([
|
||||||
["objectId", receiver],
|
...(entry.kind === "function" ? [] : [["objectId", receiver] as [string, string]]),
|
||||||
["input", type(entry.inputType)],
|
["input", type(entry.inputType)],
|
||||||
["ports", object(ports.map((port) => [port.name, port.type]))],
|
["ports", object(ports.map((port) => [port.name, port.type]))],
|
||||||
]);
|
]);
|
||||||
contexts.push([
|
contexts.push([
|
||||||
entry.displayName,
|
entry.displayName,
|
||||||
`${contextShape} & QxContextLifecycle<${contextShape} & {signal?: AbortSignal}>`,
|
entry.kind === "function"
|
||||||
|
? `${contextShape} & {signal?: AbortSignal}`
|
||||||
|
: `${contextShape} & QxContextLifecycle<${contextShape} & {signal?: AbortSignal}>`,
|
||||||
]);
|
]);
|
||||||
const event = entry.kind === "operation" ? entry.eventType : undefined;
|
const event = entry.kind === "operation" ? entry.eventType : undefined;
|
||||||
const contextType = `Contexts[${q(entry.displayName)}]`;
|
const contextType = `Contexts[${q(entry.displayName)}]`;
|
||||||
@@ -266,6 +268,7 @@ export const generateTypeScriptBindings = (
|
|||||||
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
|
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
|
||||||
]);
|
]);
|
||||||
specs[entry.displayName] = {
|
specs[entry.displayName] = {
|
||||||
|
...(entry.kind === "function" ? { receiver: "none" } : {}),
|
||||||
inputType: entry.inputType,
|
inputType: entry.inputType,
|
||||||
outputType: entry.outputType,
|
outputType: entry.outputType,
|
||||||
...(event ? { eventType: event } : {}),
|
...(event ? { eventType: event } : {}),
|
||||||
|
|||||||
@@ -8,21 +8,13 @@ declare module "@quixos/web-studio-react-runtime" {
|
|||||||
export type ObjectRef<AtomId extends string> = string & {
|
export type ObjectRef<AtomId extends string> = string & {
|
||||||
readonly $quixosAtom: AtomId;
|
readonly $quixosAtom: AtomId;
|
||||||
};
|
};
|
||||||
export type LiveFieldProp<T> = {
|
export type FieldCapability = {objectId: string; interfaceRevisionId: string; getOperationId: string; watchOperationId?: string; setOperationId?: string; inputKind?: "fields" | "value"};
|
||||||
value: T;
|
export type ReadableField<T> = {readonly value?: T; readonly capability: FieldCapability};
|
||||||
source: {
|
/** set(T) uses the resolved capability: native CRDT fields send incremental edits;
|
||||||
objectId: string;
|
* custom/manual setters receive semantic values. Storage provenance grants no authority. */
|
||||||
slotId: string;
|
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}};
|
||||||
valueType?: string;
|
export type LiveFieldProp<T> = ReadableField<T>;
|
||||||
storagePolicy?: string;
|
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
|
||||||
revision?: string | number | bigint;
|
|
||||||
crdtSnapshot?: {
|
|
||||||
type: string;
|
|
||||||
encoding: string;
|
|
||||||
payload: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type ReactComponentHostProps<Action> = {
|
export type ReactComponentHostProps<Action> = {
|
||||||
onAction?: (action: Action) => void;
|
onAction?: (action: Action) => void;
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
@@ -49,15 +41,8 @@ declare module "@quixos/web-studio-react-runtime" {
|
|||||||
options?: {clientMutationId?: string; signal?: AbortSignal},
|
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||||
) => Promise<Result>;
|
) => Promise<Result>;
|
||||||
export const h: typeof React.createElement;
|
export const h: typeof React.createElement;
|
||||||
export const useLiveField: <T>(
|
export function useLiveField<T>(field: ReadableField<T>, options: {write: (value: T) => Promise<void>}): readonly [T, (value: T) => Promise<void>];
|
||||||
field: LiveFieldProp<T>,
|
export function useLiveField<T>(field: WritableField<T>): readonly [T, (value: T) => Promise<void>];
|
||||||
options?: {
|
export function useLiveField<T>(field: ReadableField<T>): readonly [T];
|
||||||
reconcileRegister?: (state: {
|
|
||||||
confirmed: T;
|
|
||||||
optimistic: T;
|
|
||||||
pending: boolean;
|
|
||||||
}) => T;
|
|
||||||
},
|
|
||||||
) => readonly [T, (value: T) => Promise<void>];
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type { BindingSchema } from "./index.js";
|
||||||
|
import type { ValueType } from "../capability-model/types.js";
|
||||||
|
|
||||||
|
/** 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"]>;`;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
`// Generated from checked QX contracts. Do not edit.\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
||||||
|
(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`
|
||||||
|
: "")
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -21,100 +21,101 @@ INPUT=20
|
|||||||
CONFORM=21
|
CONFORM=21
|
||||||
AS=22
|
AS=22
|
||||||
BIND=23
|
BIND=23
|
||||||
TO=24
|
STATIC=24
|
||||||
PRIVATE=25
|
TO=25
|
||||||
SHARED=26
|
PRIVATE=26
|
||||||
STATE=27
|
SHARED=27
|
||||||
EDGE=28
|
STATE=28
|
||||||
PROJECTION=29
|
EDGE=29
|
||||||
WITH=30
|
PROJECTION=30
|
||||||
USING=31
|
WITH=31
|
||||||
VIA=32
|
USING=32
|
||||||
MATERIALIZE=33
|
VIA=33
|
||||||
IF=34
|
MATERIALIZE=34
|
||||||
ABSENT=35
|
IF=35
|
||||||
ON=36
|
ABSENT=36
|
||||||
POLICY=37
|
ON=37
|
||||||
DEFAULT=38
|
POLICY=38
|
||||||
SOURCE=39
|
DEFAULT=39
|
||||||
REPOSITORY=40
|
SOURCE=40
|
||||||
COMMIT=41
|
REPOSITORY=41
|
||||||
REVISION=42
|
COMMIT=42
|
||||||
SEMANTIC_MAJOR=43
|
REVISION=43
|
||||||
ON_DELETE=44
|
SEMANTIC_MAJOR=44
|
||||||
RETAIN_OTHER=45
|
ON_DELETE=45
|
||||||
KEYED=46
|
RETAIN_OTHER=46
|
||||||
PUBLIC_TRAVERSAL=47
|
KEYED=47
|
||||||
ID=48
|
PUBLIC_TRAVERSAL=48
|
||||||
DOC=49
|
ID=49
|
||||||
MODE=50
|
DOC=50
|
||||||
EMITS=51
|
MODE=51
|
||||||
RECEIVER=52
|
EMITS=52
|
||||||
REQUIRES=53
|
RECEIVER=53
|
||||||
ANY=54
|
REQUIRES=54
|
||||||
GET=55
|
ANY=55
|
||||||
SET=56
|
GET=56
|
||||||
WATCH=57
|
SET=57
|
||||||
START=58
|
WATCH=58
|
||||||
STOP=59
|
START=59
|
||||||
READ=60
|
STOP=60
|
||||||
WRITE=61
|
READ=61
|
||||||
RESOLVE=62
|
WRITE=62
|
||||||
CONNECT=63
|
RESOLVE=63
|
||||||
DISCONNECT=64
|
CONNECT=64
|
||||||
CALL=65
|
DISCONNECT=65
|
||||||
WATCH_START=66
|
CALL=66
|
||||||
WATCH_STOP=67
|
WATCH_START=67
|
||||||
SUBSCRIBE=68
|
WATCH_STOP=68
|
||||||
UNSUBSCRIBE=69
|
SUBSCRIBE=69
|
||||||
OPTIMISTIC_REGISTER=70
|
UNSUBSCRIBE=70
|
||||||
CRDT=71
|
OPTIMISTIC_REGISTER=71
|
||||||
OPTIONAL_ONE=72
|
CRDT=72
|
||||||
EXACTLY_ONE=73
|
OPTIONAL_ONE=73
|
||||||
MANY_UNIQUE=74
|
EXACTLY_ONE=74
|
||||||
MANY=75
|
MANY_UNIQUE=75
|
||||||
ORDERED=76
|
MANY=76
|
||||||
UNIT=77
|
ORDERED=77
|
||||||
WATCH_HANDLE=78
|
UNIT=78
|
||||||
MESSAGE=79
|
WATCH_HANDLE=79
|
||||||
ATOM_REF=80
|
MESSAGE=80
|
||||||
INTERFACE_REF=81
|
ATOM_REF=81
|
||||||
OPTIONAL=82
|
INTERFACE_REF=82
|
||||||
LIST=83
|
OPTIONAL=83
|
||||||
RECORD=84
|
LIST=84
|
||||||
BOOL=85
|
RECORD=85
|
||||||
BYTES=86
|
BOOL=86
|
||||||
DOUBLE=87
|
BYTES=87
|
||||||
INT32=88
|
DOUBLE=88
|
||||||
INT64=89
|
INT32=89
|
||||||
STRING=90
|
INT64=90
|
||||||
UINT32=91
|
STRING=91
|
||||||
UINT64=92
|
UINT32=92
|
||||||
TRUE=93
|
UINT64=93
|
||||||
FALSE=94
|
TRUE=94
|
||||||
NULL=95
|
FALSE=95
|
||||||
ARROW=96
|
NULL=96
|
||||||
COLON=97
|
ARROW=97
|
||||||
SEMI=98
|
COLON=98
|
||||||
COMMA=99
|
SEMI=99
|
||||||
DOT=100
|
COMMA=100
|
||||||
LBRACE=101
|
DOT=101
|
||||||
RBRACE=102
|
LBRACE=102
|
||||||
LBRACK=103
|
RBRACE=103
|
||||||
RBRACK=104
|
LBRACK=104
|
||||||
LPAREN=105
|
RBRACK=105
|
||||||
RPAREN=106
|
LPAREN=106
|
||||||
LT=107
|
RPAREN=107
|
||||||
GT=108
|
LT=108
|
||||||
AMP=109
|
GT=109
|
||||||
EQUAL=110
|
AMP=110
|
||||||
INTEGER=111
|
EQUAL=111
|
||||||
JSON_NUMBER=112
|
INTEGER=112
|
||||||
IDENTIFIER=113
|
JSON_NUMBER=113
|
||||||
STRING_LITERAL=114
|
IDENTIFIER=114
|
||||||
LINE_COMMENT=115
|
STRING_LITERAL=115
|
||||||
BLOCK_COMMENT=116
|
LINE_COMMENT=116
|
||||||
WS=117
|
BLOCK_COMMENT=117
|
||||||
|
WS=118
|
||||||
'workspace'=1
|
'workspace'=1
|
||||||
'type'=2
|
'type'=2
|
||||||
'object'=3
|
'object'=3
|
||||||
@@ -138,90 +139,91 @@ WS=117
|
|||||||
'conform'=21
|
'conform'=21
|
||||||
'as'=22
|
'as'=22
|
||||||
'bind'=23
|
'bind'=23
|
||||||
'to'=24
|
'static'=24
|
||||||
'private'=25
|
'to'=25
|
||||||
'shared'=26
|
'private'=26
|
||||||
'state'=27
|
'shared'=27
|
||||||
'edge'=28
|
'state'=28
|
||||||
'projection'=29
|
'edge'=29
|
||||||
'with'=30
|
'projection'=30
|
||||||
'using'=31
|
'with'=31
|
||||||
'via'=32
|
'using'=32
|
||||||
'materialize'=33
|
'via'=33
|
||||||
'if'=34
|
'materialize'=34
|
||||||
'absent'=35
|
'if'=35
|
||||||
'on'=36
|
'absent'=36
|
||||||
'policy'=37
|
'on'=37
|
||||||
'default'=38
|
'policy'=38
|
||||||
'source'=39
|
'default'=39
|
||||||
'repository'=40
|
'source'=40
|
||||||
'commit'=41
|
'repository'=41
|
||||||
'revision'=42
|
'commit'=42
|
||||||
'semantic-major'=43
|
'revision'=43
|
||||||
'on-delete'=44
|
'semantic-major'=44
|
||||||
'retain-other'=45
|
'on-delete'=45
|
||||||
'keyed'=46
|
'retain-other'=46
|
||||||
'public-traversal'=47
|
'keyed'=47
|
||||||
'id'=48
|
'public-traversal'=48
|
||||||
'doc'=49
|
'id'=49
|
||||||
'mode'=50
|
'doc'=50
|
||||||
'emits'=51
|
'mode'=51
|
||||||
'receiver'=52
|
'emits'=52
|
||||||
'requires'=53
|
'receiver'=53
|
||||||
'any'=54
|
'requires'=54
|
||||||
'get'=55
|
'any'=55
|
||||||
'set'=56
|
'get'=56
|
||||||
'watch'=57
|
'set'=57
|
||||||
'start'=58
|
'watch'=58
|
||||||
'stop'=59
|
'start'=59
|
||||||
'read'=60
|
'stop'=60
|
||||||
'write'=61
|
'read'=61
|
||||||
'resolve'=62
|
'write'=62
|
||||||
'connect'=63
|
'resolve'=63
|
||||||
'disconnect'=64
|
'connect'=64
|
||||||
'call'=65
|
'disconnect'=65
|
||||||
'watch-start'=66
|
'call'=66
|
||||||
'watch-stop'=67
|
'watch-start'=67
|
||||||
'subscribe'=68
|
'watch-stop'=68
|
||||||
'unsubscribe'=69
|
'subscribe'=69
|
||||||
'optimistic-register'=70
|
'unsubscribe'=70
|
||||||
'crdt'=71
|
'optimistic-register'=71
|
||||||
'optional-one'=72
|
'crdt'=72
|
||||||
'exactly-one'=73
|
'optional-one'=73
|
||||||
'many-unique'=74
|
'exactly-one'=74
|
||||||
'many'=75
|
'many-unique'=75
|
||||||
'ordered'=76
|
'many'=76
|
||||||
'unit'=77
|
'ordered'=77
|
||||||
'watch-handle'=78
|
'unit'=78
|
||||||
'message'=79
|
'watch-handle'=79
|
||||||
'atom-ref'=80
|
'message'=80
|
||||||
'interface-ref'=81
|
'atom-ref'=81
|
||||||
'optional'=82
|
'interface-ref'=82
|
||||||
'list'=83
|
'optional'=83
|
||||||
'record'=84
|
'list'=84
|
||||||
'bool'=85
|
'record'=85
|
||||||
'bytes'=86
|
'bool'=86
|
||||||
'double'=87
|
'bytes'=87
|
||||||
'int32'=88
|
'double'=88
|
||||||
'int64'=89
|
'int32'=89
|
||||||
'string'=90
|
'int64'=90
|
||||||
'uint32'=91
|
'string'=91
|
||||||
'uint64'=92
|
'uint32'=92
|
||||||
'true'=93
|
'uint64'=93
|
||||||
'false'=94
|
'true'=94
|
||||||
'null'=95
|
'false'=95
|
||||||
'->'=96
|
'null'=96
|
||||||
':'=97
|
'->'=97
|
||||||
';'=98
|
':'=98
|
||||||
','=99
|
';'=99
|
||||||
'.'=100
|
','=100
|
||||||
'{'=101
|
'.'=101
|
||||||
'}'=102
|
'{'=102
|
||||||
'['=103
|
'}'=103
|
||||||
']'=104
|
'['=104
|
||||||
'('=105
|
']'=105
|
||||||
')'=106
|
'('=106
|
||||||
'<'=107
|
')'=107
|
||||||
'>'=108
|
'<'=108
|
||||||
'&'=109
|
'>'=109
|
||||||
'='=110
|
'&'=110
|
||||||
|
'='=111
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -21,100 +21,101 @@ INPUT=20
|
|||||||
CONFORM=21
|
CONFORM=21
|
||||||
AS=22
|
AS=22
|
||||||
BIND=23
|
BIND=23
|
||||||
TO=24
|
STATIC=24
|
||||||
PRIVATE=25
|
TO=25
|
||||||
SHARED=26
|
PRIVATE=26
|
||||||
STATE=27
|
SHARED=27
|
||||||
EDGE=28
|
STATE=28
|
||||||
PROJECTION=29
|
EDGE=29
|
||||||
WITH=30
|
PROJECTION=30
|
||||||
USING=31
|
WITH=31
|
||||||
VIA=32
|
USING=32
|
||||||
MATERIALIZE=33
|
VIA=33
|
||||||
IF=34
|
MATERIALIZE=34
|
||||||
ABSENT=35
|
IF=35
|
||||||
ON=36
|
ABSENT=36
|
||||||
POLICY=37
|
ON=37
|
||||||
DEFAULT=38
|
POLICY=38
|
||||||
SOURCE=39
|
DEFAULT=39
|
||||||
REPOSITORY=40
|
SOURCE=40
|
||||||
COMMIT=41
|
REPOSITORY=41
|
||||||
REVISION=42
|
COMMIT=42
|
||||||
SEMANTIC_MAJOR=43
|
REVISION=43
|
||||||
ON_DELETE=44
|
SEMANTIC_MAJOR=44
|
||||||
RETAIN_OTHER=45
|
ON_DELETE=45
|
||||||
KEYED=46
|
RETAIN_OTHER=46
|
||||||
PUBLIC_TRAVERSAL=47
|
KEYED=47
|
||||||
ID=48
|
PUBLIC_TRAVERSAL=48
|
||||||
DOC=49
|
ID=49
|
||||||
MODE=50
|
DOC=50
|
||||||
EMITS=51
|
MODE=51
|
||||||
RECEIVER=52
|
EMITS=52
|
||||||
REQUIRES=53
|
RECEIVER=53
|
||||||
ANY=54
|
REQUIRES=54
|
||||||
GET=55
|
ANY=55
|
||||||
SET=56
|
GET=56
|
||||||
WATCH=57
|
SET=57
|
||||||
START=58
|
WATCH=58
|
||||||
STOP=59
|
START=59
|
||||||
READ=60
|
STOP=60
|
||||||
WRITE=61
|
READ=61
|
||||||
RESOLVE=62
|
WRITE=62
|
||||||
CONNECT=63
|
RESOLVE=63
|
||||||
DISCONNECT=64
|
CONNECT=64
|
||||||
CALL=65
|
DISCONNECT=65
|
||||||
WATCH_START=66
|
CALL=66
|
||||||
WATCH_STOP=67
|
WATCH_START=67
|
||||||
SUBSCRIBE=68
|
WATCH_STOP=68
|
||||||
UNSUBSCRIBE=69
|
SUBSCRIBE=69
|
||||||
OPTIMISTIC_REGISTER=70
|
UNSUBSCRIBE=70
|
||||||
CRDT=71
|
OPTIMISTIC_REGISTER=71
|
||||||
OPTIONAL_ONE=72
|
CRDT=72
|
||||||
EXACTLY_ONE=73
|
OPTIONAL_ONE=73
|
||||||
MANY_UNIQUE=74
|
EXACTLY_ONE=74
|
||||||
MANY=75
|
MANY_UNIQUE=75
|
||||||
ORDERED=76
|
MANY=76
|
||||||
UNIT=77
|
ORDERED=77
|
||||||
WATCH_HANDLE=78
|
UNIT=78
|
||||||
MESSAGE=79
|
WATCH_HANDLE=79
|
||||||
ATOM_REF=80
|
MESSAGE=80
|
||||||
INTERFACE_REF=81
|
ATOM_REF=81
|
||||||
OPTIONAL=82
|
INTERFACE_REF=82
|
||||||
LIST=83
|
OPTIONAL=83
|
||||||
RECORD=84
|
LIST=84
|
||||||
BOOL=85
|
RECORD=85
|
||||||
BYTES=86
|
BOOL=86
|
||||||
DOUBLE=87
|
BYTES=87
|
||||||
INT32=88
|
DOUBLE=88
|
||||||
INT64=89
|
INT32=89
|
||||||
STRING=90
|
INT64=90
|
||||||
UINT32=91
|
STRING=91
|
||||||
UINT64=92
|
UINT32=92
|
||||||
TRUE=93
|
UINT64=93
|
||||||
FALSE=94
|
TRUE=94
|
||||||
NULL=95
|
FALSE=95
|
||||||
ARROW=96
|
NULL=96
|
||||||
COLON=97
|
ARROW=97
|
||||||
SEMI=98
|
COLON=98
|
||||||
COMMA=99
|
SEMI=99
|
||||||
DOT=100
|
COMMA=100
|
||||||
LBRACE=101
|
DOT=101
|
||||||
RBRACE=102
|
LBRACE=102
|
||||||
LBRACK=103
|
RBRACE=103
|
||||||
RBRACK=104
|
LBRACK=104
|
||||||
LPAREN=105
|
RBRACK=105
|
||||||
RPAREN=106
|
LPAREN=106
|
||||||
LT=107
|
RPAREN=107
|
||||||
GT=108
|
LT=108
|
||||||
AMP=109
|
GT=109
|
||||||
EQUAL=110
|
AMP=110
|
||||||
INTEGER=111
|
EQUAL=111
|
||||||
JSON_NUMBER=112
|
INTEGER=112
|
||||||
IDENTIFIER=113
|
JSON_NUMBER=113
|
||||||
STRING_LITERAL=114
|
IDENTIFIER=114
|
||||||
LINE_COMMENT=115
|
STRING_LITERAL=115
|
||||||
BLOCK_COMMENT=116
|
LINE_COMMENT=116
|
||||||
WS=117
|
BLOCK_COMMENT=117
|
||||||
|
WS=118
|
||||||
'workspace'=1
|
'workspace'=1
|
||||||
'type'=2
|
'type'=2
|
||||||
'object'=3
|
'object'=3
|
||||||
@@ -138,90 +139,91 @@ WS=117
|
|||||||
'conform'=21
|
'conform'=21
|
||||||
'as'=22
|
'as'=22
|
||||||
'bind'=23
|
'bind'=23
|
||||||
'to'=24
|
'static'=24
|
||||||
'private'=25
|
'to'=25
|
||||||
'shared'=26
|
'private'=26
|
||||||
'state'=27
|
'shared'=27
|
||||||
'edge'=28
|
'state'=28
|
||||||
'projection'=29
|
'edge'=29
|
||||||
'with'=30
|
'projection'=30
|
||||||
'using'=31
|
'with'=31
|
||||||
'via'=32
|
'using'=32
|
||||||
'materialize'=33
|
'via'=33
|
||||||
'if'=34
|
'materialize'=34
|
||||||
'absent'=35
|
'if'=35
|
||||||
'on'=36
|
'absent'=36
|
||||||
'policy'=37
|
'on'=37
|
||||||
'default'=38
|
'policy'=38
|
||||||
'source'=39
|
'default'=39
|
||||||
'repository'=40
|
'source'=40
|
||||||
'commit'=41
|
'repository'=41
|
||||||
'revision'=42
|
'commit'=42
|
||||||
'semantic-major'=43
|
'revision'=43
|
||||||
'on-delete'=44
|
'semantic-major'=44
|
||||||
'retain-other'=45
|
'on-delete'=45
|
||||||
'keyed'=46
|
'retain-other'=46
|
||||||
'public-traversal'=47
|
'keyed'=47
|
||||||
'id'=48
|
'public-traversal'=48
|
||||||
'doc'=49
|
'id'=49
|
||||||
'mode'=50
|
'doc'=50
|
||||||
'emits'=51
|
'mode'=51
|
||||||
'receiver'=52
|
'emits'=52
|
||||||
'requires'=53
|
'receiver'=53
|
||||||
'any'=54
|
'requires'=54
|
||||||
'get'=55
|
'any'=55
|
||||||
'set'=56
|
'get'=56
|
||||||
'watch'=57
|
'set'=57
|
||||||
'start'=58
|
'watch'=58
|
||||||
'stop'=59
|
'start'=59
|
||||||
'read'=60
|
'stop'=60
|
||||||
'write'=61
|
'read'=61
|
||||||
'resolve'=62
|
'write'=62
|
||||||
'connect'=63
|
'resolve'=63
|
||||||
'disconnect'=64
|
'connect'=64
|
||||||
'call'=65
|
'disconnect'=65
|
||||||
'watch-start'=66
|
'call'=66
|
||||||
'watch-stop'=67
|
'watch-start'=67
|
||||||
'subscribe'=68
|
'watch-stop'=68
|
||||||
'unsubscribe'=69
|
'subscribe'=69
|
||||||
'optimistic-register'=70
|
'unsubscribe'=70
|
||||||
'crdt'=71
|
'optimistic-register'=71
|
||||||
'optional-one'=72
|
'crdt'=72
|
||||||
'exactly-one'=73
|
'optional-one'=73
|
||||||
'many-unique'=74
|
'exactly-one'=74
|
||||||
'many'=75
|
'many-unique'=75
|
||||||
'ordered'=76
|
'many'=76
|
||||||
'unit'=77
|
'ordered'=77
|
||||||
'watch-handle'=78
|
'unit'=78
|
||||||
'message'=79
|
'watch-handle'=79
|
||||||
'atom-ref'=80
|
'message'=80
|
||||||
'interface-ref'=81
|
'atom-ref'=81
|
||||||
'optional'=82
|
'interface-ref'=82
|
||||||
'list'=83
|
'optional'=83
|
||||||
'record'=84
|
'list'=84
|
||||||
'bool'=85
|
'record'=85
|
||||||
'bytes'=86
|
'bool'=86
|
||||||
'double'=87
|
'bytes'=87
|
||||||
'int32'=88
|
'double'=88
|
||||||
'int64'=89
|
'int32'=89
|
||||||
'string'=90
|
'int64'=90
|
||||||
'uint32'=91
|
'string'=91
|
||||||
'uint64'=92
|
'uint32'=92
|
||||||
'true'=93
|
'uint64'=93
|
||||||
'false'=94
|
'true'=94
|
||||||
'null'=95
|
'false'=95
|
||||||
'->'=96
|
'null'=96
|
||||||
':'=97
|
'->'=97
|
||||||
';'=98
|
':'=98
|
||||||
','=99
|
';'=99
|
||||||
'.'=100
|
','=100
|
||||||
'{'=101
|
'.'=101
|
||||||
'}'=102
|
'{'=102
|
||||||
'['=103
|
'}'=103
|
||||||
']'=104
|
'['=104
|
||||||
'('=105
|
']'=105
|
||||||
')'=106
|
'('=106
|
||||||
'<'=107
|
')'=107
|
||||||
'>'=108
|
'<'=108
|
||||||
'&'=109
|
'>'=109
|
||||||
'='=110
|
'&'=110
|
||||||
|
'='=111
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@ import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
|
|||||||
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
|
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
|
||||||
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
|
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
|
||||||
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
|
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
|
||||||
|
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||||
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
|
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
|
||||||
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
|
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||||
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
|
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
|
||||||
@@ -347,6 +348,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
|
|||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
|
visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitStateFieldBindingDecl?: (ctx: StateFieldBindingDeclContext) => Result;
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipMaterializationDecl`.
|
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipMaterializationDecl`.
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ const lowerOperationMember = (state: LoweringState, context: OperationMemberCont
|
|||||||
inputType,
|
inputType,
|
||||||
outputType,
|
outputType,
|
||||||
mode: "call",
|
mode: "call",
|
||||||
|
...(context.STATIC() ? { scope: "class" as const } : {}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -702,7 +703,15 @@ const lowerInterfaceTemplate = (
|
|||||||
inputType,
|
inputType,
|
||||||
outputType,
|
outputType,
|
||||||
operations: [
|
operations: [
|
||||||
signature("call", capabilityId.operation(stringValue(operation.stringLiteral(1))), inputType, outputType),
|
{
|
||||||
|
...signature(
|
||||||
|
"call",
|
||||||
|
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
||||||
|
inputType,
|
||||||
|
outputType,
|
||||||
|
),
|
||||||
|
...(operation.STATIC() ? { scope: "class" as const } : {}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1439,42 @@ const lowerConformance = (
|
|||||||
const operationBindings = context
|
const operationBindings = context
|
||||||
.conformanceItem()
|
.conformanceItem()
|
||||||
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
|
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
|
||||||
|
const field = item.stateFieldBindingDecl();
|
||||||
|
if (field) {
|
||||||
|
const memberName = identifier(field.identifier(0));
|
||||||
|
const member = interfaceSymbol.definition?.members.find((entry) => entry.displayName === memberName);
|
||||||
|
const attachment = requireSymbol(
|
||||||
|
state,
|
||||||
|
state.attachments,
|
||||||
|
identifier(field.identifier(1)),
|
||||||
|
field,
|
||||||
|
"attachment",
|
||||||
|
);
|
||||||
|
if (!member || member.kind !== "value" || attachment?.attachment.kind !== "state") {
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
field,
|
||||||
|
"invalid-state-field-binding",
|
||||||
|
"Field shorthand requires a value member and a state slot",
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const primitives = {
|
||||||
|
get: "read",
|
||||||
|
set: "write",
|
||||||
|
"watch-start": "watch-start",
|
||||||
|
"watch-stop": "watch-stop",
|
||||||
|
} as const;
|
||||||
|
const slotId = attachment.attachment.id;
|
||||||
|
return member.operations.map((operation) => ({
|
||||||
|
operationId: operation.id,
|
||||||
|
binding: {
|
||||||
|
kind: "state" as const,
|
||||||
|
slotId,
|
||||||
|
primitive: primitives[operation.displayName as keyof typeof primitives],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
const bindingContext = item.operationBindingDecl();
|
const bindingContext = item.operationBindingDecl();
|
||||||
if (!bindingContext) {
|
if (!bindingContext) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const scaffoldRecipe = async (
|
|||||||
if (react) {
|
if (react) {
|
||||||
create(
|
create(
|
||||||
"src/component.tsx",
|
"src/component.tsx",
|
||||||
`// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`,
|
`// Add a checked props export, select it in quixos.check.json options.react,\n// then use its generated ReactResults type. Scaffold bundle does this for you.\nexport default function Component(_props: {camino: Record<string, never>; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1></section>;\n}\n`,
|
||||||
);
|
);
|
||||||
create(
|
create(
|
||||||
"src/impl/sourceGet.ts",
|
"src/impl/sourceGet.ts",
|
||||||
@@ -180,14 +180,7 @@ export const scaffoldRecipe = async (
|
|||||||
bindingOutput: "src/gen/qx.ts",
|
bindingOutput: "src/gen/qx.ts",
|
||||||
...(react
|
...(react
|
||||||
? {
|
? {
|
||||||
options: {
|
options: { react: { propsExports: [] } },
|
||||||
messages: {
|
|
||||||
"org.quixos.web-studio.ReactProps": {
|
|
||||||
module: "@quixos/camino-package-runtime",
|
|
||||||
export: "opaqueReactPropsBinding",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}),
|
}),
|
||||||
@@ -394,9 +387,20 @@ export const scaffoldRecipe = async (
|
|||||||
for (const [file, content] of Object.entries(spec.initialFiles)) {
|
for (const [file, content] of Object.entries(spec.initialFiles)) {
|
||||||
if (
|
if (
|
||||||
typeof content !== "string" ||
|
typeof content !== "string" ||
|
||||||
["quixos.lock", "flake.nix", "quixos.toolchain.json", "quixos.check.json", "package.json"].includes(file)
|
["quixos.lock", "flake.nix", "quixos.toolchain.json", "package.json"].includes(file)
|
||||||
)
|
)
|
||||||
throw new Error(`Not an initial authored file: ${file}`);
|
throw new Error(`Not an initial authored file: ${file}`);
|
||||||
|
if (file === "quixos.check.json") {
|
||||||
|
const check = JSON.parse(content);
|
||||||
|
if (
|
||||||
|
check.backend !== "typescript" ||
|
||||||
|
check.bindingOutput !== "src/gen/qx.ts" ||
|
||||||
|
Object.keys(check).some((key) => !["backend", "bindingOutput", "options"].includes(key))
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
"Initial check configuration may customize binding options, not the scaffold verification backend or output",
|
||||||
|
);
|
||||||
|
}
|
||||||
const existing = files.findIndex((entry) => entry.file === prefix + file);
|
const existing = files.findIndex((entry) => entry.file === prefix + file);
|
||||||
if (existing >= 0) files.splice(existing, 1);
|
if (existing >= 0) files.splice(existing, 1);
|
||||||
create(file, content);
|
create(file, content);
|
||||||
|
|||||||
@@ -310,6 +310,9 @@ const main = async () => {
|
|||||||
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
||||||
const request: StructuralRequest = {
|
const request: StructuralRequest = {
|
||||||
kind: "interface",
|
kind: "interface",
|
||||||
|
// Like package scaffolding, declaration creation is provisional. Imports
|
||||||
|
// can be attached next; the normal check verifies the complete graph.
|
||||||
|
validation: "syntax",
|
||||||
source: spec.source,
|
source: spec.source,
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ export interface InterfaceOperation<Type = ValueType> {
|
|||||||
inputType: Type;
|
inputType: Type;
|
||||||
outputType: Type;
|
outputType: Type;
|
||||||
mode: InterfaceOperationMode;
|
mode: InterfaceOperationMode;
|
||||||
|
/** Class capabilities have no object receiver and bind free functions. */
|
||||||
|
scope?: "class";
|
||||||
/** Required for watch-start/subscribe and absent for other modes. */
|
/** Required for watch-start/subscribe and absent for other modes. */
|
||||||
eventType?: Type;
|
eventType?: Type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type {
|
|||||||
OwnedAttachment,
|
OwnedAttachment,
|
||||||
PackageExport,
|
PackageExport,
|
||||||
PackageOperationExport,
|
PackageOperationExport,
|
||||||
|
PackageFunctionExport,
|
||||||
PackageRevision,
|
PackageRevision,
|
||||||
PackageRevisionId,
|
PackageRevisionId,
|
||||||
PersistentAttachment,
|
PersistentAttachment,
|
||||||
@@ -1423,6 +1424,24 @@ const validateConformances = (
|
|||||||
}
|
}
|
||||||
const operation = operationEntry.operation;
|
const operation = operationEntry.operation;
|
||||||
const binding = entry.binding;
|
const binding = entry.binding;
|
||||||
|
if (operation.scope === "class" && (!conformance.id || operation.mode !== "call")) {
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-package-binding",
|
||||||
|
bindingPath,
|
||||||
|
"Class capabilities require an explicit conformance ID and call mode",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (operation.scope === "class" && binding.kind !== "package") {
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-package-binding",
|
||||||
|
bindingPath,
|
||||||
|
"Class capabilities must bind a free package function, not instance storage",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (binding.kind === "state") {
|
if (binding.kind === "state") {
|
||||||
const attachment = findAttachment(indexes, "state", binding.slotId);
|
const attachment = findAttachment(indexes, "state", binding.slotId);
|
||||||
if (!attachment || attachment.attachment.kind !== "state") {
|
if (!attachment || attachment.attachment.kind !== "state") {
|
||||||
@@ -1562,31 +1581,49 @@ const validateConformances = (
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (packageExport.kind !== "operation") {
|
if (operation.scope === "class" ? packageExport.kind !== "function" : packageExport.kind !== "operation") {
|
||||||
issue(
|
issue(
|
||||||
issues,
|
issues,
|
||||||
"invalid-package-binding",
|
"invalid-package-binding",
|
||||||
`${bindingPath}.binding.exportId`,
|
`${bindingPath}.binding.exportId`,
|
||||||
`Package export ${binding.exportId} is ${packageExport.kind}, not an operation`,
|
`Package export ${binding.exportId} must be ${operation.scope === "class" ? "a free function" : "an instance operation"}`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!signaturesMatch(operation, packageExport)) {
|
if (
|
||||||
|
!signaturesMatch(operation, {
|
||||||
|
...packageExport,
|
||||||
|
mode: packageExport.kind === "operation" ? packageExport.mode : "call",
|
||||||
|
})
|
||||||
|
) {
|
||||||
issue(
|
issue(
|
||||||
issues,
|
issues,
|
||||||
"invalid-package-binding",
|
"invalid-package-binding",
|
||||||
`${bindingPath}.binding.exportId`,
|
`${bindingPath}.binding.exportId`,
|
||||||
`Package export provides ${describeSignature(packageExport)}, but operation requires ${describeSignature(operation)}`,
|
`Package export signature does not match ${describeSignature(operation)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (packageExport.kind === "operation")
|
||||||
|
validatePackageReceiver(issues, {
|
||||||
|
entry: packageExport,
|
||||||
|
atomId: conformance.atomId,
|
||||||
|
path: `${bindingPath}.binding.exportId`,
|
||||||
|
indexes,
|
||||||
|
requirementGraph,
|
||||||
|
graphSourceKey: key,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
operation.scope === "class" &&
|
||||||
|
(binding.dependencies.some((entry) => entry.binding.kind !== "constructor") ||
|
||||||
|
packageExport.dependencyPorts.some((entry) => entry.requirement.kind !== "constructor"))
|
||||||
|
) {
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-package-binding",
|
||||||
|
bindingPath,
|
||||||
|
"Class functions may inject constructors, not instance-dependent ports",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
validatePackageReceiver(issues, {
|
|
||||||
entry: packageExport,
|
|
||||||
atomId: conformance.atomId,
|
|
||||||
path: `${bindingPath}.binding.exportId`,
|
|
||||||
indexes,
|
|
||||||
requirementGraph,
|
|
||||||
graphSourceKey: key,
|
|
||||||
});
|
|
||||||
validateBoundDependencies(issues, {
|
validateBoundDependencies(issues, {
|
||||||
dependencies: binding.dependencies,
|
dependencies: binding.dependencies,
|
||||||
dependencyPorts: packageExport.dependencyPorts,
|
dependencyPorts: packageExport.dependencyPorts,
|
||||||
@@ -1919,7 +1956,7 @@ export type ResolvedOperationPlan =
|
|||||||
kind: "package";
|
kind: "package";
|
||||||
binding: Extract<Binding, { kind: "package" }>;
|
binding: Extract<Binding, { kind: "package" }>;
|
||||||
packageRevision: PackageRevision;
|
packageRevision: PackageRevision;
|
||||||
packageExport: PackageOperationExport;
|
packageExport: PackageOperationExport | PackageFunctionExport;
|
||||||
dependencies: Array<{
|
dependencies: Array<{
|
||||||
port: DependencyPort;
|
port: DependencyPort;
|
||||||
binding: DependencyBinding;
|
binding: DependencyBinding;
|
||||||
@@ -1962,7 +1999,8 @@ export const resolveOperationPlan = (
|
|||||||
}
|
}
|
||||||
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
||||||
const packageExport = packageRevision?.exports.find(
|
const packageExport = packageRevision?.exports.find(
|
||||||
(entry): entry is PackageOperationExport => entry.id === binding.exportId && entry.kind === "operation",
|
(entry): entry is PackageOperationExport | PackageFunctionExport =>
|
||||||
|
entry.id === binding.exportId && (entry.kind === "operation" || entry.kind === "function"),
|
||||||
);
|
);
|
||||||
if (!packageRevision || !packageExport) {
|
if (!packageRevision || !packageExport) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
+217
-21
File diff suppressed because one or more lines are too long
@@ -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"],
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -30,11 +30,9 @@ test("React preset applies its browser build script and shared-platform imports"
|
|||||||
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /browserSources = true/);
|
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /browserSources = true/);
|
||||||
assert.equal(await fs.readFile(path.join(root, "src/gen/web-studio-react-runtime.d.ts"), "utf8"), reactPlatformTypes);
|
assert.equal(await fs.readFile(path.join(root, "src/gen/web-studio-react-runtime.d.ts"), "utf8"), reactPlatformTypes);
|
||||||
assert.match(await fs.readFile(path.join(root, "src/browser-assets.d.ts"), "utf8"), /declare module "\*\.css"/);
|
assert.match(await fs.readFile(path.join(root, "src/browser-assets.d.ts"), "utf8"), /declare module "\*\.css"/);
|
||||||
assert.equal(
|
assert.deepEqual(
|
||||||
JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.messages[
|
JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.react.propsExports,
|
||||||
"org.quixos.web-studio.ReactProps"
|
[],
|
||||||
].export,
|
|
||||||
"opaqueReactPropsBinding",
|
|
||||||
);
|
);
|
||||||
await assert.rejects(fs.access(path.join(root, "quixos.scaffold.json")));
|
await assert.rejects(fs.access(path.join(root, "quixos.scaffold.json")));
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
Reference in New Issue
Block a user