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:
+8
-2
@@ -3,16 +3,22 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
import { generateTypeScriptBindings } from "./index.js";
|
||||
import path from "node:path";
|
||||
import { reactPlatformTypes } from "./react-platform.js";
|
||||
import { generateReactBindings } from "./react.js";
|
||||
|
||||
const main = async () => {
|
||||
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
|
||||
if (!schema || !revision || !output || rest.length)
|
||||
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 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);
|
||||
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), "react-props.gen.ts"),
|
||||
generateReactBindings(contracts, revision, config.react.propsExports, config.react.components),
|
||||
);
|
||||
}
|
||||
};
|
||||
main().catch((error: unknown) => {
|
||||
|
||||
@@ -124,7 +124,7 @@ export const genericImplementationType = (
|
||||
};
|
||||
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
|
||||
member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.filter((op) => op.mode === "call" && op.scope !== "class")
|
||||
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
|
||||
);
|
||||
return object([
|
||||
@@ -146,11 +146,21 @@ export const genericImplementationType = (
|
||||
.join(",");
|
||||
const receiver = definition.receiverRequirement;
|
||||
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())],
|
||||
["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 handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
|
||||
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.
|
||||
const operations = contract.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((operation) => operation.mode === "call")
|
||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
|
||||
);
|
||||
return {
|
||||
@@ -245,13 +245,15 @@ export const generateTypeScriptBindings = (
|
||||
? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>`
|
||||
: "QxObjectRef<string>";
|
||||
const contextShape = object([
|
||||
["objectId", receiver],
|
||||
...(entry.kind === "function" ? [] : [["objectId", receiver] as [string, string]]),
|
||||
["input", type(entry.inputType)],
|
||||
["ports", object(ports.map((port) => [port.name, port.type]))],
|
||||
]);
|
||||
contexts.push([
|
||||
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 contextType = `Contexts[${q(entry.displayName)}]`;
|
||||
@@ -266,6 +268,7 @@ export const generateTypeScriptBindings = (
|
||||
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
|
||||
]);
|
||||
specs[entry.displayName] = {
|
||||
...(entry.kind === "function" ? { receiver: "none" } : {}),
|
||||
inputType: entry.inputType,
|
||||
outputType: entry.outputType,
|
||||
...(event ? { eventType: event } : {}),
|
||||
|
||||
@@ -8,21 +8,13 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
export type ObjectRef<AtomId extends string> = string & {
|
||||
readonly $quixosAtom: AtomId;
|
||||
};
|
||||
export type LiveFieldProp<T> = {
|
||||
value: T;
|
||||
source: {
|
||||
objectId: string;
|
||||
slotId: string;
|
||||
valueType?: string;
|
||||
storagePolicy?: string;
|
||||
revision?: string | number | bigint;
|
||||
crdtSnapshot?: {
|
||||
type: string;
|
||||
encoding: string;
|
||||
payload: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type FieldCapability = {objectId: string; interfaceRevisionId: string; getOperationId: string; watchOperationId?: string; setOperationId?: string; inputKind?: "fields" | "value"};
|
||||
export type ReadableField<T> = {readonly value?: T; readonly capability: FieldCapability};
|
||||
/** set(T) uses the resolved capability: native CRDT fields send incremental edits;
|
||||
* custom/manual setters receive semantic values. Storage provenance grants no authority. */
|
||||
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}};
|
||||
export type LiveFieldProp<T> = ReadableField<T>;
|
||||
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
|
||||
export type ReactComponentHostProps<Action> = {
|
||||
onAction?: (action: Action) => void;
|
||||
fallback?: React.ReactNode;
|
||||
@@ -49,15 +41,8 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||
) => Promise<Result>;
|
||||
export const h: typeof React.createElement;
|
||||
export const useLiveField: <T>(
|
||||
field: LiveFieldProp<T>,
|
||||
options?: {
|
||||
reconcileRegister?: (state: {
|
||||
confirmed: T;
|
||||
optimistic: T;
|
||||
pending: boolean;
|
||||
}) => T;
|
||||
},
|
||||
) => readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: ReadableField<T>, options: {write: (value: T) => Promise<void>}): readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: WritableField<T>): readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: ReadableField<T>): readonly [T];
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
AS=22
|
||||
BIND=23
|
||||
TO=24
|
||||
PRIVATE=25
|
||||
SHARED=26
|
||||
STATE=27
|
||||
EDGE=28
|
||||
PROJECTION=29
|
||||
WITH=30
|
||||
USING=31
|
||||
VIA=32
|
||||
MATERIALIZE=33
|
||||
IF=34
|
||||
ABSENT=35
|
||||
ON=36
|
||||
POLICY=37
|
||||
DEFAULT=38
|
||||
SOURCE=39
|
||||
REPOSITORY=40
|
||||
COMMIT=41
|
||||
REVISION=42
|
||||
SEMANTIC_MAJOR=43
|
||||
ON_DELETE=44
|
||||
RETAIN_OTHER=45
|
||||
KEYED=46
|
||||
PUBLIC_TRAVERSAL=47
|
||||
ID=48
|
||||
DOC=49
|
||||
MODE=50
|
||||
EMITS=51
|
||||
RECEIVER=52
|
||||
REQUIRES=53
|
||||
ANY=54
|
||||
GET=55
|
||||
SET=56
|
||||
WATCH=57
|
||||
START=58
|
||||
STOP=59
|
||||
READ=60
|
||||
WRITE=61
|
||||
RESOLVE=62
|
||||
CONNECT=63
|
||||
DISCONNECT=64
|
||||
CALL=65
|
||||
WATCH_START=66
|
||||
WATCH_STOP=67
|
||||
SUBSCRIBE=68
|
||||
UNSUBSCRIBE=69
|
||||
OPTIMISTIC_REGISTER=70
|
||||
CRDT=71
|
||||
OPTIONAL_ONE=72
|
||||
EXACTLY_ONE=73
|
||||
MANY_UNIQUE=74
|
||||
MANY=75
|
||||
ORDERED=76
|
||||
UNIT=77
|
||||
WATCH_HANDLE=78
|
||||
MESSAGE=79
|
||||
ATOM_REF=80
|
||||
INTERFACE_REF=81
|
||||
OPTIONAL=82
|
||||
LIST=83
|
||||
RECORD=84
|
||||
BOOL=85
|
||||
BYTES=86
|
||||
DOUBLE=87
|
||||
INT32=88
|
||||
INT64=89
|
||||
STRING=90
|
||||
UINT32=91
|
||||
UINT64=92
|
||||
TRUE=93
|
||||
FALSE=94
|
||||
NULL=95
|
||||
ARROW=96
|
||||
COLON=97
|
||||
SEMI=98
|
||||
COMMA=99
|
||||
DOT=100
|
||||
LBRACE=101
|
||||
RBRACE=102
|
||||
LBRACK=103
|
||||
RBRACK=104
|
||||
LPAREN=105
|
||||
RPAREN=106
|
||||
LT=107
|
||||
GT=108
|
||||
AMP=109
|
||||
EQUAL=110
|
||||
INTEGER=111
|
||||
JSON_NUMBER=112
|
||||
IDENTIFIER=113
|
||||
STRING_LITERAL=114
|
||||
LINE_COMMENT=115
|
||||
BLOCK_COMMENT=116
|
||||
WS=117
|
||||
STATIC=24
|
||||
TO=25
|
||||
PRIVATE=26
|
||||
SHARED=27
|
||||
STATE=28
|
||||
EDGE=29
|
||||
PROJECTION=30
|
||||
WITH=31
|
||||
USING=32
|
||||
VIA=33
|
||||
MATERIALIZE=34
|
||||
IF=35
|
||||
ABSENT=36
|
||||
ON=37
|
||||
POLICY=38
|
||||
DEFAULT=39
|
||||
SOURCE=40
|
||||
REPOSITORY=41
|
||||
COMMIT=42
|
||||
REVISION=43
|
||||
SEMANTIC_MAJOR=44
|
||||
ON_DELETE=45
|
||||
RETAIN_OTHER=46
|
||||
KEYED=47
|
||||
PUBLIC_TRAVERSAL=48
|
||||
ID=49
|
||||
DOC=50
|
||||
MODE=51
|
||||
EMITS=52
|
||||
RECEIVER=53
|
||||
REQUIRES=54
|
||||
ANY=55
|
||||
GET=56
|
||||
SET=57
|
||||
WATCH=58
|
||||
START=59
|
||||
STOP=60
|
||||
READ=61
|
||||
WRITE=62
|
||||
RESOLVE=63
|
||||
CONNECT=64
|
||||
DISCONNECT=65
|
||||
CALL=66
|
||||
WATCH_START=67
|
||||
WATCH_STOP=68
|
||||
SUBSCRIBE=69
|
||||
UNSUBSCRIBE=70
|
||||
OPTIMISTIC_REGISTER=71
|
||||
CRDT=72
|
||||
OPTIONAL_ONE=73
|
||||
EXACTLY_ONE=74
|
||||
MANY_UNIQUE=75
|
||||
MANY=76
|
||||
ORDERED=77
|
||||
UNIT=78
|
||||
WATCH_HANDLE=79
|
||||
MESSAGE=80
|
||||
ATOM_REF=81
|
||||
INTERFACE_REF=82
|
||||
OPTIONAL=83
|
||||
LIST=84
|
||||
RECORD=85
|
||||
BOOL=86
|
||||
BYTES=87
|
||||
DOUBLE=88
|
||||
INT32=89
|
||||
INT64=90
|
||||
STRING=91
|
||||
UINT32=92
|
||||
UINT64=93
|
||||
TRUE=94
|
||||
FALSE=95
|
||||
NULL=96
|
||||
ARROW=97
|
||||
COLON=98
|
||||
SEMI=99
|
||||
COMMA=100
|
||||
DOT=101
|
||||
LBRACE=102
|
||||
RBRACE=103
|
||||
LBRACK=104
|
||||
RBRACK=105
|
||||
LPAREN=106
|
||||
RPAREN=107
|
||||
LT=108
|
||||
GT=109
|
||||
AMP=110
|
||||
EQUAL=111
|
||||
INTEGER=112
|
||||
JSON_NUMBER=113
|
||||
IDENTIFIER=114
|
||||
STRING_LITERAL=115
|
||||
LINE_COMMENT=116
|
||||
BLOCK_COMMENT=117
|
||||
WS=118
|
||||
'workspace'=1
|
||||
'type'=2
|
||||
'object'=3
|
||||
@@ -138,90 +139,91 @@ WS=117
|
||||
'conform'=21
|
||||
'as'=22
|
||||
'bind'=23
|
||||
'to'=24
|
||||
'private'=25
|
||||
'shared'=26
|
||||
'state'=27
|
||||
'edge'=28
|
||||
'projection'=29
|
||||
'with'=30
|
||||
'using'=31
|
||||
'via'=32
|
||||
'materialize'=33
|
||||
'if'=34
|
||||
'absent'=35
|
||||
'on'=36
|
||||
'policy'=37
|
||||
'default'=38
|
||||
'source'=39
|
||||
'repository'=40
|
||||
'commit'=41
|
||||
'revision'=42
|
||||
'semantic-major'=43
|
||||
'on-delete'=44
|
||||
'retain-other'=45
|
||||
'keyed'=46
|
||||
'public-traversal'=47
|
||||
'id'=48
|
||||
'doc'=49
|
||||
'mode'=50
|
||||
'emits'=51
|
||||
'receiver'=52
|
||||
'requires'=53
|
||||
'any'=54
|
||||
'get'=55
|
||||
'set'=56
|
||||
'watch'=57
|
||||
'start'=58
|
||||
'stop'=59
|
||||
'read'=60
|
||||
'write'=61
|
||||
'resolve'=62
|
||||
'connect'=63
|
||||
'disconnect'=64
|
||||
'call'=65
|
||||
'watch-start'=66
|
||||
'watch-stop'=67
|
||||
'subscribe'=68
|
||||
'unsubscribe'=69
|
||||
'optimistic-register'=70
|
||||
'crdt'=71
|
||||
'optional-one'=72
|
||||
'exactly-one'=73
|
||||
'many-unique'=74
|
||||
'many'=75
|
||||
'ordered'=76
|
||||
'unit'=77
|
||||
'watch-handle'=78
|
||||
'message'=79
|
||||
'atom-ref'=80
|
||||
'interface-ref'=81
|
||||
'optional'=82
|
||||
'list'=83
|
||||
'record'=84
|
||||
'bool'=85
|
||||
'bytes'=86
|
||||
'double'=87
|
||||
'int32'=88
|
||||
'int64'=89
|
||||
'string'=90
|
||||
'uint32'=91
|
||||
'uint64'=92
|
||||
'true'=93
|
||||
'false'=94
|
||||
'null'=95
|
||||
'->'=96
|
||||
':'=97
|
||||
';'=98
|
||||
','=99
|
||||
'.'=100
|
||||
'{'=101
|
||||
'}'=102
|
||||
'['=103
|
||||
']'=104
|
||||
'('=105
|
||||
')'=106
|
||||
'<'=107
|
||||
'>'=108
|
||||
'&'=109
|
||||
'='=110
|
||||
'static'=24
|
||||
'to'=25
|
||||
'private'=26
|
||||
'shared'=27
|
||||
'state'=28
|
||||
'edge'=29
|
||||
'projection'=30
|
||||
'with'=31
|
||||
'using'=32
|
||||
'via'=33
|
||||
'materialize'=34
|
||||
'if'=35
|
||||
'absent'=36
|
||||
'on'=37
|
||||
'policy'=38
|
||||
'default'=39
|
||||
'source'=40
|
||||
'repository'=41
|
||||
'commit'=42
|
||||
'revision'=43
|
||||
'semantic-major'=44
|
||||
'on-delete'=45
|
||||
'retain-other'=46
|
||||
'keyed'=47
|
||||
'public-traversal'=48
|
||||
'id'=49
|
||||
'doc'=50
|
||||
'mode'=51
|
||||
'emits'=52
|
||||
'receiver'=53
|
||||
'requires'=54
|
||||
'any'=55
|
||||
'get'=56
|
||||
'set'=57
|
||||
'watch'=58
|
||||
'start'=59
|
||||
'stop'=60
|
||||
'read'=61
|
||||
'write'=62
|
||||
'resolve'=63
|
||||
'connect'=64
|
||||
'disconnect'=65
|
||||
'call'=66
|
||||
'watch-start'=67
|
||||
'watch-stop'=68
|
||||
'subscribe'=69
|
||||
'unsubscribe'=70
|
||||
'optimistic-register'=71
|
||||
'crdt'=72
|
||||
'optional-one'=73
|
||||
'exactly-one'=74
|
||||
'many-unique'=75
|
||||
'many'=76
|
||||
'ordered'=77
|
||||
'unit'=78
|
||||
'watch-handle'=79
|
||||
'message'=80
|
||||
'atom-ref'=81
|
||||
'interface-ref'=82
|
||||
'optional'=83
|
||||
'list'=84
|
||||
'record'=85
|
||||
'bool'=86
|
||||
'bytes'=87
|
||||
'double'=88
|
||||
'int32'=89
|
||||
'int64'=90
|
||||
'string'=91
|
||||
'uint32'=92
|
||||
'uint64'=93
|
||||
'true'=94
|
||||
'false'=95
|
||||
'null'=96
|
||||
'->'=97
|
||||
':'=98
|
||||
';'=99
|
||||
','=100
|
||||
'.'=101
|
||||
'{'=102
|
||||
'}'=103
|
||||
'['=104
|
||||
']'=105
|
||||
'('=106
|
||||
')'=107
|
||||
'<'=108
|
||||
'>'=109
|
||||
'&'=110
|
||||
'='=111
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,100 +21,101 @@ INPUT=20
|
||||
CONFORM=21
|
||||
AS=22
|
||||
BIND=23
|
||||
TO=24
|
||||
PRIVATE=25
|
||||
SHARED=26
|
||||
STATE=27
|
||||
EDGE=28
|
||||
PROJECTION=29
|
||||
WITH=30
|
||||
USING=31
|
||||
VIA=32
|
||||
MATERIALIZE=33
|
||||
IF=34
|
||||
ABSENT=35
|
||||
ON=36
|
||||
POLICY=37
|
||||
DEFAULT=38
|
||||
SOURCE=39
|
||||
REPOSITORY=40
|
||||
COMMIT=41
|
||||
REVISION=42
|
||||
SEMANTIC_MAJOR=43
|
||||
ON_DELETE=44
|
||||
RETAIN_OTHER=45
|
||||
KEYED=46
|
||||
PUBLIC_TRAVERSAL=47
|
||||
ID=48
|
||||
DOC=49
|
||||
MODE=50
|
||||
EMITS=51
|
||||
RECEIVER=52
|
||||
REQUIRES=53
|
||||
ANY=54
|
||||
GET=55
|
||||
SET=56
|
||||
WATCH=57
|
||||
START=58
|
||||
STOP=59
|
||||
READ=60
|
||||
WRITE=61
|
||||
RESOLVE=62
|
||||
CONNECT=63
|
||||
DISCONNECT=64
|
||||
CALL=65
|
||||
WATCH_START=66
|
||||
WATCH_STOP=67
|
||||
SUBSCRIBE=68
|
||||
UNSUBSCRIBE=69
|
||||
OPTIMISTIC_REGISTER=70
|
||||
CRDT=71
|
||||
OPTIONAL_ONE=72
|
||||
EXACTLY_ONE=73
|
||||
MANY_UNIQUE=74
|
||||
MANY=75
|
||||
ORDERED=76
|
||||
UNIT=77
|
||||
WATCH_HANDLE=78
|
||||
MESSAGE=79
|
||||
ATOM_REF=80
|
||||
INTERFACE_REF=81
|
||||
OPTIONAL=82
|
||||
LIST=83
|
||||
RECORD=84
|
||||
BOOL=85
|
||||
BYTES=86
|
||||
DOUBLE=87
|
||||
INT32=88
|
||||
INT64=89
|
||||
STRING=90
|
||||
UINT32=91
|
||||
UINT64=92
|
||||
TRUE=93
|
||||
FALSE=94
|
||||
NULL=95
|
||||
ARROW=96
|
||||
COLON=97
|
||||
SEMI=98
|
||||
COMMA=99
|
||||
DOT=100
|
||||
LBRACE=101
|
||||
RBRACE=102
|
||||
LBRACK=103
|
||||
RBRACK=104
|
||||
LPAREN=105
|
||||
RPAREN=106
|
||||
LT=107
|
||||
GT=108
|
||||
AMP=109
|
||||
EQUAL=110
|
||||
INTEGER=111
|
||||
JSON_NUMBER=112
|
||||
IDENTIFIER=113
|
||||
STRING_LITERAL=114
|
||||
LINE_COMMENT=115
|
||||
BLOCK_COMMENT=116
|
||||
WS=117
|
||||
STATIC=24
|
||||
TO=25
|
||||
PRIVATE=26
|
||||
SHARED=27
|
||||
STATE=28
|
||||
EDGE=29
|
||||
PROJECTION=30
|
||||
WITH=31
|
||||
USING=32
|
||||
VIA=33
|
||||
MATERIALIZE=34
|
||||
IF=35
|
||||
ABSENT=36
|
||||
ON=37
|
||||
POLICY=38
|
||||
DEFAULT=39
|
||||
SOURCE=40
|
||||
REPOSITORY=41
|
||||
COMMIT=42
|
||||
REVISION=43
|
||||
SEMANTIC_MAJOR=44
|
||||
ON_DELETE=45
|
||||
RETAIN_OTHER=46
|
||||
KEYED=47
|
||||
PUBLIC_TRAVERSAL=48
|
||||
ID=49
|
||||
DOC=50
|
||||
MODE=51
|
||||
EMITS=52
|
||||
RECEIVER=53
|
||||
REQUIRES=54
|
||||
ANY=55
|
||||
GET=56
|
||||
SET=57
|
||||
WATCH=58
|
||||
START=59
|
||||
STOP=60
|
||||
READ=61
|
||||
WRITE=62
|
||||
RESOLVE=63
|
||||
CONNECT=64
|
||||
DISCONNECT=65
|
||||
CALL=66
|
||||
WATCH_START=67
|
||||
WATCH_STOP=68
|
||||
SUBSCRIBE=69
|
||||
UNSUBSCRIBE=70
|
||||
OPTIMISTIC_REGISTER=71
|
||||
CRDT=72
|
||||
OPTIONAL_ONE=73
|
||||
EXACTLY_ONE=74
|
||||
MANY_UNIQUE=75
|
||||
MANY=76
|
||||
ORDERED=77
|
||||
UNIT=78
|
||||
WATCH_HANDLE=79
|
||||
MESSAGE=80
|
||||
ATOM_REF=81
|
||||
INTERFACE_REF=82
|
||||
OPTIONAL=83
|
||||
LIST=84
|
||||
RECORD=85
|
||||
BOOL=86
|
||||
BYTES=87
|
||||
DOUBLE=88
|
||||
INT32=89
|
||||
INT64=90
|
||||
STRING=91
|
||||
UINT32=92
|
||||
UINT64=93
|
||||
TRUE=94
|
||||
FALSE=95
|
||||
NULL=96
|
||||
ARROW=97
|
||||
COLON=98
|
||||
SEMI=99
|
||||
COMMA=100
|
||||
DOT=101
|
||||
LBRACE=102
|
||||
RBRACE=103
|
||||
LBRACK=104
|
||||
RBRACK=105
|
||||
LPAREN=106
|
||||
RPAREN=107
|
||||
LT=108
|
||||
GT=109
|
||||
AMP=110
|
||||
EQUAL=111
|
||||
INTEGER=112
|
||||
JSON_NUMBER=113
|
||||
IDENTIFIER=114
|
||||
STRING_LITERAL=115
|
||||
LINE_COMMENT=116
|
||||
BLOCK_COMMENT=117
|
||||
WS=118
|
||||
'workspace'=1
|
||||
'type'=2
|
||||
'object'=3
|
||||
@@ -138,90 +139,91 @@ WS=117
|
||||
'conform'=21
|
||||
'as'=22
|
||||
'bind'=23
|
||||
'to'=24
|
||||
'private'=25
|
||||
'shared'=26
|
||||
'state'=27
|
||||
'edge'=28
|
||||
'projection'=29
|
||||
'with'=30
|
||||
'using'=31
|
||||
'via'=32
|
||||
'materialize'=33
|
||||
'if'=34
|
||||
'absent'=35
|
||||
'on'=36
|
||||
'policy'=37
|
||||
'default'=38
|
||||
'source'=39
|
||||
'repository'=40
|
||||
'commit'=41
|
||||
'revision'=42
|
||||
'semantic-major'=43
|
||||
'on-delete'=44
|
||||
'retain-other'=45
|
||||
'keyed'=46
|
||||
'public-traversal'=47
|
||||
'id'=48
|
||||
'doc'=49
|
||||
'mode'=50
|
||||
'emits'=51
|
||||
'receiver'=52
|
||||
'requires'=53
|
||||
'any'=54
|
||||
'get'=55
|
||||
'set'=56
|
||||
'watch'=57
|
||||
'start'=58
|
||||
'stop'=59
|
||||
'read'=60
|
||||
'write'=61
|
||||
'resolve'=62
|
||||
'connect'=63
|
||||
'disconnect'=64
|
||||
'call'=65
|
||||
'watch-start'=66
|
||||
'watch-stop'=67
|
||||
'subscribe'=68
|
||||
'unsubscribe'=69
|
||||
'optimistic-register'=70
|
||||
'crdt'=71
|
||||
'optional-one'=72
|
||||
'exactly-one'=73
|
||||
'many-unique'=74
|
||||
'many'=75
|
||||
'ordered'=76
|
||||
'unit'=77
|
||||
'watch-handle'=78
|
||||
'message'=79
|
||||
'atom-ref'=80
|
||||
'interface-ref'=81
|
||||
'optional'=82
|
||||
'list'=83
|
||||
'record'=84
|
||||
'bool'=85
|
||||
'bytes'=86
|
||||
'double'=87
|
||||
'int32'=88
|
||||
'int64'=89
|
||||
'string'=90
|
||||
'uint32'=91
|
||||
'uint64'=92
|
||||
'true'=93
|
||||
'false'=94
|
||||
'null'=95
|
||||
'->'=96
|
||||
':'=97
|
||||
';'=98
|
||||
','=99
|
||||
'.'=100
|
||||
'{'=101
|
||||
'}'=102
|
||||
'['=103
|
||||
']'=104
|
||||
'('=105
|
||||
')'=106
|
||||
'<'=107
|
||||
'>'=108
|
||||
'&'=109
|
||||
'='=110
|
||||
'static'=24
|
||||
'to'=25
|
||||
'private'=26
|
||||
'shared'=27
|
||||
'state'=28
|
||||
'edge'=29
|
||||
'projection'=30
|
||||
'with'=31
|
||||
'using'=32
|
||||
'via'=33
|
||||
'materialize'=34
|
||||
'if'=35
|
||||
'absent'=36
|
||||
'on'=37
|
||||
'policy'=38
|
||||
'default'=39
|
||||
'source'=40
|
||||
'repository'=41
|
||||
'commit'=42
|
||||
'revision'=43
|
||||
'semantic-major'=44
|
||||
'on-delete'=45
|
||||
'retain-other'=46
|
||||
'keyed'=47
|
||||
'public-traversal'=48
|
||||
'id'=49
|
||||
'doc'=50
|
||||
'mode'=51
|
||||
'emits'=52
|
||||
'receiver'=53
|
||||
'requires'=54
|
||||
'any'=55
|
||||
'get'=56
|
||||
'set'=57
|
||||
'watch'=58
|
||||
'start'=59
|
||||
'stop'=60
|
||||
'read'=61
|
||||
'write'=62
|
||||
'resolve'=63
|
||||
'connect'=64
|
||||
'disconnect'=65
|
||||
'call'=66
|
||||
'watch-start'=67
|
||||
'watch-stop'=68
|
||||
'subscribe'=69
|
||||
'unsubscribe'=70
|
||||
'optimistic-register'=71
|
||||
'crdt'=72
|
||||
'optional-one'=73
|
||||
'exactly-one'=74
|
||||
'many-unique'=75
|
||||
'many'=76
|
||||
'ordered'=77
|
||||
'unit'=78
|
||||
'watch-handle'=79
|
||||
'message'=80
|
||||
'atom-ref'=81
|
||||
'interface-ref'=82
|
||||
'optional'=83
|
||||
'list'=84
|
||||
'record'=85
|
||||
'bool'=86
|
||||
'bytes'=87
|
||||
'double'=88
|
||||
'int32'=89
|
||||
'int64'=90
|
||||
'string'=91
|
||||
'uint32'=92
|
||||
'uint64'=93
|
||||
'true'=94
|
||||
'false'=95
|
||||
'null'=96
|
||||
'->'=97
|
||||
':'=98
|
||||
';'=99
|
||||
','=100
|
||||
'.'=101
|
||||
'{'=102
|
||||
'}'=103
|
||||
'['=104
|
||||
']'=105
|
||||
'('=106
|
||||
')'=107
|
||||
'<'=108
|
||||
'>'=109
|
||||
'&'=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 { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
|
||||
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
|
||||
@@ -347,6 +348,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
|
||||
* @return the visitor 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`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
@@ -552,6 +552,7 @@ const lowerOperationMember = (state: LoweringState, context: OperationMemberCont
|
||||
inputType,
|
||||
outputType,
|
||||
mode: "call",
|
||||
...(context.STATIC() ? { scope: "class" as const } : {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -702,7 +703,15 @@ const lowerInterfaceTemplate = (
|
||||
inputType,
|
||||
outputType,
|
||||
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
|
||||
.conformanceItem()
|
||||
.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();
|
||||
if (!bindingContext) {
|
||||
return [];
|
||||
|
||||
@@ -142,7 +142,7 @@ export const scaffoldRecipe = async (
|
||||
if (react) {
|
||||
create(
|
||||
"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(
|
||||
"src/impl/sourceGet.ts",
|
||||
@@ -180,14 +180,7 @@ export const scaffoldRecipe = async (
|
||||
bindingOutput: "src/gen/qx.ts",
|
||||
...(react
|
||||
? {
|
||||
options: {
|
||||
messages: {
|
||||
"org.quixos.web-studio.ReactProps": {
|
||||
module: "@quixos/camino-package-runtime",
|
||||
export: "opaqueReactPropsBinding",
|
||||
},
|
||||
},
|
||||
},
|
||||
options: { react: { propsExports: [] } },
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
@@ -394,9 +387,20 @@ export const scaffoldRecipe = async (
|
||||
for (const [file, content] of Object.entries(spec.initialFiles)) {
|
||||
if (
|
||||
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}`);
|
||||
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);
|
||||
if (existing >= 0) files.splice(existing, 1);
|
||||
create(file, content);
|
||||
|
||||
@@ -310,6 +310,9 @@ const main = async () => {
|
||||
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
||||
const request: StructuralRequest = {
|
||||
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,
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -110,6 +110,8 @@ export interface InterfaceOperation<Type = ValueType> {
|
||||
inputType: Type;
|
||||
outputType: Type;
|
||||
mode: InterfaceOperationMode;
|
||||
/** Class capabilities have no object receiver and bind free functions. */
|
||||
scope?: "class";
|
||||
/** Required for watch-start/subscribe and absent for other modes. */
|
||||
eventType?: Type;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
OwnedAttachment,
|
||||
PackageExport,
|
||||
PackageOperationExport,
|
||||
PackageFunctionExport,
|
||||
PackageRevision,
|
||||
PackageRevisionId,
|
||||
PersistentAttachment,
|
||||
@@ -1423,6 +1424,24 @@ const validateConformances = (
|
||||
}
|
||||
const operation = operationEntry.operation;
|
||||
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") {
|
||||
const attachment = findAttachment(indexes, "state", binding.slotId);
|
||||
if (!attachment || attachment.attachment.kind !== "state") {
|
||||
@@ -1562,31 +1581,49 @@ const validateConformances = (
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (packageExport.kind !== "operation") {
|
||||
if (operation.scope === "class" ? packageExport.kind !== "function" : packageExport.kind !== "operation") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-package-binding",
|
||||
`${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;
|
||||
}
|
||||
if (!signaturesMatch(operation, packageExport)) {
|
||||
if (
|
||||
!signaturesMatch(operation, {
|
||||
...packageExport,
|
||||
mode: packageExport.kind === "operation" ? packageExport.mode : "call",
|
||||
})
|
||||
) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-package-binding",
|
||||
`${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, {
|
||||
dependencies: binding.dependencies,
|
||||
dependencyPorts: packageExport.dependencyPorts,
|
||||
@@ -1919,7 +1956,7 @@ export type ResolvedOperationPlan =
|
||||
kind: "package";
|
||||
binding: Extract<Binding, { kind: "package" }>;
|
||||
packageRevision: PackageRevision;
|
||||
packageExport: PackageOperationExport;
|
||||
packageExport: PackageOperationExport | PackageFunctionExport;
|
||||
dependencies: Array<{
|
||||
port: DependencyPort;
|
||||
binding: DependencyBinding;
|
||||
@@ -1962,7 +1999,8 @@ export const resolveOperationPlan = (
|
||||
}
|
||||
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
||||
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) {
|
||||
return undefined;
|
||||
|
||||
+217
-21
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user