@procedures/compiler 0.0.0-stage → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/checks.d.ts +24 -0
  5. package/dist/checks.d.ts.map +1 -0
  6. package/dist/checks.js +123 -0
  7. package/dist/checks.js.map +1 -0
  8. package/dist/codegen-closure.d.ts +19 -0
  9. package/dist/codegen-closure.d.ts.map +1 -0
  10. package/dist/codegen-closure.js +200 -0
  11. package/dist/codegen-closure.js.map +1 -0
  12. package/dist/codegen-text.d.ts +44 -0
  13. package/dist/codegen-text.d.ts.map +1 -0
  14. package/dist/codegen-text.js +360 -0
  15. package/dist/codegen-text.js.map +1 -0
  16. package/dist/diagnostics.d.ts +67 -0
  17. package/dist/diagnostics.d.ts.map +1 -0
  18. package/dist/diagnostics.js +38 -0
  19. package/dist/diagnostics.js.map +1 -0
  20. package/dist/effects.d.ts +63 -0
  21. package/dist/effects.d.ts.map +1 -0
  22. package/dist/effects.js +185 -0
  23. package/dist/effects.js.map +1 -0
  24. package/dist/explain.d.ts +58 -0
  25. package/dist/explain.d.ts.map +1 -0
  26. package/dist/explain.js +144 -0
  27. package/dist/explain.js.map +1 -0
  28. package/dist/facts.d.ts +66 -0
  29. package/dist/facts.d.ts.map +1 -0
  30. package/dist/facts.js +102 -0
  31. package/dist/facts.js.map +1 -0
  32. package/dist/fold.d.ts +27 -0
  33. package/dist/fold.d.ts.map +1 -0
  34. package/dist/fold.js +128 -0
  35. package/dist/fold.js.map +1 -0
  36. package/dist/index.d.ts +35 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +22 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/interceptor.d.ts +51 -0
  41. package/dist/interceptor.d.ts.map +1 -0
  42. package/dist/interceptor.js +116 -0
  43. package/dist/interceptor.js.map +1 -0
  44. package/dist/ir.d.ts +240 -0
  45. package/dist/ir.d.ts.map +1 -0
  46. package/dist/ir.js +74 -0
  47. package/dist/ir.js.map +1 -0
  48. package/dist/mermaid.d.ts +53 -0
  49. package/dist/mermaid.d.ts.map +1 -0
  50. package/dist/mermaid.js +423 -0
  51. package/dist/mermaid.js.map +1 -0
  52. package/dist/module.d.ts +20 -0
  53. package/dist/module.d.ts.map +1 -0
  54. package/dist/module.js +124 -0
  55. package/dist/module.js.map +1 -0
  56. package/dist/normalize.d.ts +43 -0
  57. package/dist/normalize.d.ts.map +1 -0
  58. package/dist/normalize.js +529 -0
  59. package/dist/normalize.js.map +1 -0
  60. package/dist/openrpc.d.ts +63 -0
  61. package/dist/openrpc.d.ts.map +1 -0
  62. package/dist/openrpc.js +57 -0
  63. package/dist/openrpc.js.map +1 -0
  64. package/dist/print.d.ts +8 -0
  65. package/dist/print.d.ts.map +1 -0
  66. package/dist/print.js +148 -0
  67. package/dist/print.js.map +1 -0
  68. package/dist/procedures/program/check.d.ts +14 -0
  69. package/dist/procedures/program/check.d.ts.map +1 -0
  70. package/dist/procedures/program/check.js +30 -0
  71. package/dist/procedures/program/check.js.map +1 -0
  72. package/dist/procedures/program/compile.d.ts +14 -0
  73. package/dist/procedures/program/compile.d.ts.map +1 -0
  74. package/dist/procedures/program/compile.js +35 -0
  75. package/dist/procedures/program/compile.js.map +1 -0
  76. package/dist/procedures/program/diagram.d.ts +14 -0
  77. package/dist/procedures/program/diagram.d.ts.map +1 -0
  78. package/dist/procedures/program/diagram.js +23 -0
  79. package/dist/procedures/program/diagram.js.map +1 -0
  80. package/dist/procedures/program/effects-output.d.ts +8 -0
  81. package/dist/procedures/program/effects-output.d.ts.map +1 -0
  82. package/dist/procedures/program/effects-output.js +13 -0
  83. package/dist/procedures/program/effects-output.js.map +1 -0
  84. package/dist/procedures/program/explain.d.ts +14 -0
  85. package/dist/procedures/program/explain.d.ts.map +1 -0
  86. package/dist/procedures/program/explain.js +43 -0
  87. package/dist/procedures/program/explain.js.map +1 -0
  88. package/dist/procedures/program/index.d.ts +5 -0
  89. package/dist/procedures/program/index.d.ts.map +1 -0
  90. package/dist/procedures/program/index.js +5 -0
  91. package/dist/procedures/program/index.js.map +1 -0
  92. package/dist/procedures/sdk/index.d.ts +3 -0
  93. package/dist/procedures/sdk/index.d.ts.map +1 -0
  94. package/dist/procedures/sdk/index.js +3 -0
  95. package/dist/procedures/sdk/index.js.map +1 -0
  96. package/dist/procedures/sdk/manifest.d.ts +11 -0
  97. package/dist/procedures/sdk/manifest.d.ts.map +1 -0
  98. package/dist/procedures/sdk/manifest.js +15 -0
  99. package/dist/procedures/sdk/manifest.js.map +1 -0
  100. package/dist/procedures/sdk/openrpc.d.ts +11 -0
  101. package/dist/procedures/sdk/openrpc.d.ts.map +1 -0
  102. package/dist/procedures/sdk/openrpc.js +13 -0
  103. package/dist/procedures/sdk/openrpc.js.map +1 -0
  104. package/dist/procedures/sdk/typescript.d.ts +11 -0
  105. package/dist/procedures/sdk/typescript.d.ts.map +1 -0
  106. package/dist/procedures/sdk/typescript.js +14 -0
  107. package/dist/procedures/sdk/typescript.js.map +1 -0
  108. package/dist/program.d.ts +84 -0
  109. package/dist/program.d.ts.map +1 -0
  110. package/dist/program.js +108 -0
  111. package/dist/program.js.map +1 -0
  112. package/dist/refs.d.ts +9 -0
  113. package/dist/refs.d.ts.map +1 -0
  114. package/dist/refs.js +9 -0
  115. package/dist/refs.js.map +1 -0
  116. package/dist/register.d.ts +6 -0
  117. package/dist/register.d.ts.map +1 -0
  118. package/dist/register.js +8 -0
  119. package/dist/register.js.map +1 -0
  120. package/dist/runtime.d.ts +68 -0
  121. package/dist/runtime.d.ts.map +1 -0
  122. package/dist/runtime.js +138 -0
  123. package/dist/runtime.js.map +1 -0
  124. package/dist/schema.d.ts +41 -0
  125. package/dist/schema.d.ts.map +1 -0
  126. package/dist/schema.js +325 -0
  127. package/dist/schema.js.map +1 -0
  128. package/dist/scopes.d.ts +25 -0
  129. package/dist/scopes.d.ts.map +1 -0
  130. package/dist/scopes.js +202 -0
  131. package/dist/scopes.js.map +1 -0
  132. package/dist/types.d.ts +129 -0
  133. package/dist/types.d.ts.map +1 -0
  134. package/dist/types.js +157 -0
  135. package/dist/types.js.map +1 -0
  136. package/dist/typescript-sdk.d.ts +34 -0
  137. package/dist/typescript-sdk.d.ts.map +1 -0
  138. package/dist/typescript-sdk.js +231 -0
  139. package/dist/typescript-sdk.js.map +1 -0
  140. package/package.json +83 -3
  141. package/src/checks.ts +128 -0
  142. package/src/codegen-closure.ts +224 -0
  143. package/src/codegen-text.ts +396 -0
  144. package/src/diagnostics.ts +98 -0
  145. package/src/effects.ts +231 -0
  146. package/src/explain.ts +194 -0
  147. package/src/facts.ts +161 -0
  148. package/src/fold.ts +136 -0
  149. package/src/index.ts +35 -0
  150. package/src/interceptor.ts +149 -0
  151. package/src/ir.ts +330 -0
  152. package/src/mermaid.ts +460 -0
  153. package/src/module.ts +171 -0
  154. package/src/normalize.ts +694 -0
  155. package/src/openrpc.ts +104 -0
  156. package/src/print.ts +143 -0
  157. package/src/procedures/program/check.ts +31 -0
  158. package/src/procedures/program/compile.ts +36 -0
  159. package/src/procedures/program/diagram.ts +25 -0
  160. package/src/procedures/program/effects-output.ts +16 -0
  161. package/src/procedures/program/explain.ts +46 -0
  162. package/src/procedures/program/index.ts +4 -0
  163. package/src/procedures/sdk/index.ts +2 -0
  164. package/src/procedures/sdk/manifest.ts +17 -0
  165. package/src/procedures/sdk/openrpc.ts +16 -0
  166. package/src/procedures/sdk/typescript.ts +17 -0
  167. package/src/program.ts +181 -0
  168. package/src/refs.ts +9 -0
  169. package/src/register.ts +10 -0
  170. package/src/runtime.ts +188 -0
  171. package/src/schema.ts +318 -0
  172. package/src/scopes.ts +196 -0
  173. package/src/types.ts +318 -0
  174. package/src/typescript-sdk.ts +282 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * A typed TypeScript SDK from a manifest (roadmap R23).
3
+ *
4
+ * The manifest of a host has the JSON Schemas of each procedure. This module writes the source
5
+ * of a TypeScript module from them:
6
+ *
7
+ * - One input type and one output type for each procedure.
8
+ * - The interface `Sdk`, with one method for each procedure at `sdk.<namespace>.<name>`.
9
+ * - The function `createSdk(client)`, whose methods call `client.call(path, input)`.
10
+ *
11
+ * The converter knows a part of JSON Schema. It knows objects with the `required` list, arrays,
12
+ * strings, numbers, booleans, `null`, `enum`, `const`, `anyOf`, `oneOf` and `additionalProperties`.
13
+ * It writes `unknown` for each other schema. The source has no imports, and it type-checks with
14
+ * the strict options of this repository.
15
+ */
16
+
17
+ import { MANIFEST_FORMAT, type ProcedureEntry, type ProcedureManifest } from "@procedures/client/browser";
18
+
19
+ /** The options of `toTypeScriptSdk()`. */
20
+ export interface TypeScriptSdkOptions {
21
+ /** The text of the first comment of the module. The default names the manifest and its hash. */
22
+ readonly header?: string | undefined;
23
+ }
24
+
25
+ type Schema = Record<string, unknown>;
26
+
27
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
28
+
29
+ function isSchema(value: unknown): value is Schema {
30
+ return value !== null && typeof value === "object" && !Array.isArray(value);
31
+ }
32
+
33
+ /** The name of a property in a type: an identifier as it is, else a string literal. */
34
+ function propertyName(name: string): string {
35
+ return IDENTIFIER.test(name) ? name : JSON.stringify(name);
36
+ }
37
+
38
+ /** The key of a property in an object literal. The key `__proto__` sets the prototype, so it is a computed key. */
39
+ function literalKey(name: string): string {
40
+ return name === "__proto__" ? '["__proto__"]' : propertyName(name);
41
+ }
42
+
43
+ /** The text of a comment: no end of the comment, and no line breaks. */
44
+ function commentText(text: string): string {
45
+ return text.replace(/\*\//g, "*\\/").replace(/[\r\n\u2028\u2029]+/g, " ").trim();
46
+ }
47
+
48
+ /** The type of a JSON value in `const` or `enum`: a literal type, or `unknown` for an object. */
49
+ function literalType(value: unknown): string {
50
+ if (value === null) return "null";
51
+ if (typeof value === "string") return JSON.stringify(value);
52
+ if (typeof value === "boolean") return String(value);
53
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
54
+ return "unknown";
55
+ }
56
+
57
+ function union(types: readonly string[]): string {
58
+ const unique = [...new Set(types)];
59
+ if (unique.length === 0) return "never";
60
+ if (unique.includes("unknown")) return "unknown";
61
+ return unique.join(" | ");
62
+ }
63
+
64
+ /** True when an object schema has an empty `required` list, or none: then the input can be absent. */
65
+ function noRequired(schema: unknown): boolean {
66
+ if (!isSchema(schema)) return false;
67
+ if (schema["type"] !== "object") return false;
68
+ const required = schema["required"];
69
+ return !Array.isArray(required) || required.length === 0;
70
+ }
71
+
72
+ /**
73
+ * This function gives the TypeScript type of a JSON Schema (draft-07). It knows objects, arrays,
74
+ * strings, numbers, booleans, `null`, `enum`, `const`, `anyOf`, `oneOf` and `additionalProperties`.
75
+ * Each other schema gives `unknown`. `indent` is the indentation of the lines of an object type.
76
+ */
77
+ export function schemaToType(schema: unknown, indent: string = ""): string {
78
+ if (schema === false) return "never";
79
+ if (!isSchema(schema)) return "unknown";
80
+ if ("const" in schema) return literalType(schema["const"]);
81
+ const values = schema["enum"];
82
+ if (Array.isArray(values)) return union(values.map(literalType));
83
+ const variants = schema["anyOf"] ?? schema["oneOf"];
84
+ if (Array.isArray(variants)) return union(variants.map((variant) => schemaToType(variant, indent)));
85
+ const type = schema["type"];
86
+ if (Array.isArray(type)) return union(type.map((name) => schemaToType({ ...schema, type: name }, indent)));
87
+ switch (type) {
88
+ case "string":
89
+ return "string";
90
+ case "number":
91
+ case "integer":
92
+ return "number";
93
+ case "boolean":
94
+ return "boolean";
95
+ case "null":
96
+ return "null";
97
+ case "array": {
98
+ const items = schema["items"];
99
+ return `Array<${Array.isArray(items) ? "unknown" : schemaToType(items, indent)}>`;
100
+ }
101
+ case "object":
102
+ return objectType(schema, indent);
103
+ default:
104
+ return "unknown";
105
+ }
106
+ }
107
+
108
+ function objectType(schema: Schema, indent: string): string {
109
+ const properties = isSchema(schema["properties"]) ? Object.entries(schema["properties"]) : [];
110
+ const required = new Set(Array.isArray(schema["required"]) ? schema["required"].filter((name): name is string => typeof name === "string") : []);
111
+ const extra = schema["additionalProperties"];
112
+ const inner = `${indent} `;
113
+ const lines: string[] = [];
114
+ for (const [name, property] of properties) {
115
+ const description = isSchema(property) && typeof property["description"] === "string" ? commentText(property["description"]) : "";
116
+ if (description) lines.push(`${inner}/** ${description} */`);
117
+ const optional = !required.has(name);
118
+ lines.push(`${inner}${propertyName(name)}${optional ? "?" : ""}: ${schemaToType(property, inner)}${optional ? " | undefined" : ""};`);
119
+ }
120
+ // Absent additionalProperties: an object without properties is open, and an object with properties is closed
121
+ const open = extra === true || (isSchema(extra) && Object.keys(extra).length === 0) || (extra === undefined && properties.length === 0);
122
+ if (open) {
123
+ lines.push(`${inner}[key: string]: unknown;`);
124
+ } else if (isSchema(extra)) {
125
+ // An index signature must agree with each property, so a schema with properties gets unknown
126
+ lines.push(`${inner}[key: string]: ${properties.length === 0 ? schemaToType(extra, inner) : "unknown"};`);
127
+ }
128
+ if (lines.length === 0) return "Record<string, never>";
129
+ return `{\n${lines.join("\n")}\n${indent}}`;
130
+ }
131
+
132
+ /** The PascalCase name of a path: the letters and digits of each segment. */
133
+ function pascalName(path: readonly string[]): string {
134
+ const name = path
135
+ .flatMap((segment) => segment.split(/[^A-Za-z0-9]+/))
136
+ .filter((part) => part.length > 0)
137
+ .map((part) => part[0]!.toUpperCase() + part.slice(1))
138
+ .join("");
139
+ return name === "" || /^[0-9]/.test(name) ? `P${name}` : name;
140
+ }
141
+
142
+ /** A procedure of the SDK with the names of its types. */
143
+ interface SdkProcedure {
144
+ readonly entry: ProcedureEntry;
145
+ readonly input: string;
146
+ readonly output: string;
147
+ /** True: the input can be absent (an object schema with an empty `required` list, or a schema of any value). */
148
+ readonly optionalInput: boolean;
149
+ /** True: an absent input becomes `{}`. */
150
+ readonly emptyObject: boolean;
151
+ }
152
+
153
+ /** A node of the tree of the paths: a procedure, the children, or both. */
154
+ interface PathNode {
155
+ procedure: SdkProcedure | undefined;
156
+ readonly children: Map<string, PathNode>;
157
+ }
158
+
159
+ function signature(procedure: SdkProcedure): string {
160
+ return `(input${procedure.optionalInput ? "?" : ""}: ${procedure.input}) => Promise<${procedure.output}>`;
161
+ }
162
+
163
+ /** The members of a node in the interface `Sdk`. */
164
+ function interfaceMembers(node: PathNode, indent: string): string[] {
165
+ const lines: string[] = [];
166
+ for (const [segment, child] of node.children) {
167
+ const description = child.procedure?.entry.description;
168
+ if (description) lines.push(`${indent}/** ${commentText(description)} */`);
169
+ lines.push(`${indent}readonly ${propertyName(segment)}: ${memberType(child, indent)};`);
170
+ }
171
+ return lines;
172
+ }
173
+
174
+ function memberType(node: PathNode, indent: string): string {
175
+ const members = node.children.size > 0 ? `{\n${interfaceMembers(node, `${indent} `).join("\n")}\n${indent}}` : undefined;
176
+ if (!node.procedure) return members ?? "Record<string, never>";
177
+ const fn = signature(node.procedure);
178
+ return members ? `(${fn}) & ${members}` : fn;
179
+ }
180
+
181
+ /** The value of a node in `createSdk()`. */
182
+ function memberValue(node: PathNode, indent: string): string {
183
+ const members =
184
+ node.children.size > 0
185
+ ? `{\n${[...node.children].map(([segment, child]) => `${indent} ${literalKey(segment)}: ${memberValue(child, `${indent} `)},`).join("\n")}\n${indent}}`
186
+ : undefined;
187
+ if (!node.procedure) return members ?? "{}";
188
+ const procedure = node.procedure;
189
+ const path = `[${procedure.entry.path.map((segment) => JSON.stringify(segment)).join(", ")}]`;
190
+ const argument = procedure.emptyObject ? "input ?? {}" : "input";
191
+ const fn = `(input${procedure.optionalInput ? "?" : ""}: ${procedure.input}): Promise<${procedure.output}> => call(${path}, ${argument})`;
192
+ return members ? `withMembers(${fn}, ${members})` : fn;
193
+ }
194
+
195
+ function hasBoth(node: PathNode): boolean {
196
+ if (node.procedure && node.children.size > 0) return true;
197
+ return [...node.children.values()].some(hasBoth);
198
+ }
199
+
200
+ /**
201
+ * This function writes the source of a typed TypeScript SDK for the procedures of a manifest.
202
+ * The module exports an input type and an output type for each procedure, the interface `Sdk`,
203
+ * the interface `SdkClient` and the function `createSdk()`.
204
+ */
205
+ export function toTypeScriptSdk(manifest: ProcedureManifest, options: TypeScriptSdkOptions = {}): string {
206
+ const entries = [...manifest.procedures].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
207
+ const used = new Set<string>();
208
+ const procedures = entries.map((entry): SdkProcedure => {
209
+ let name = pascalName(entry.path);
210
+ for (let suffix = 2; used.has(name); suffix++) name = `${pascalName(entry.path)}${suffix}`;
211
+ used.add(name);
212
+ const emptyObject = noRequired(entry.input);
213
+ const anyInput = schemaToType(entry.input) === "unknown";
214
+ return { entry, input: `${name}Input`, output: `${name}Output`, optionalInput: anyInput || emptyObject, emptyObject };
215
+ });
216
+
217
+ const root: PathNode = { procedure: undefined, children: new Map() };
218
+ for (const procedure of procedures) {
219
+ let node = root;
220
+ for (const segment of procedure.entry.path) {
221
+ let child = node.children.get(segment);
222
+ if (!child) {
223
+ child = { procedure: undefined, children: new Map() };
224
+ node.children.set(segment, child);
225
+ }
226
+ node = child;
227
+ }
228
+ node.procedure = procedure;
229
+ }
230
+
231
+ const header =
232
+ options.header ??
233
+ `The typed SDK of ${procedures.length} procedures. @procedures/compiler wrote this file from the manifest ${manifest.hash} (${MANIFEST_FORMAT}). Do not edit it: write it again from the manifest.`;
234
+ const out: string[] = [`/**`, ` * ${commentText(header)}`, ` */`, ""];
235
+
236
+ for (const procedure of procedures) {
237
+ const { entry } = procedure;
238
+ out.push(`/** The input of \`${commentText(entry.key)}\`. */`);
239
+ out.push(`export type ${procedure.input} = ${schemaToType(entry.input)};`, "");
240
+ out.push(`/** The output of \`${commentText(entry.key)}\`. */`);
241
+ out.push(`export type ${procedure.output} = ${schemaToType(entry.output)};`, "");
242
+ }
243
+
244
+ out.push(
245
+ "/** A client that calls a procedure by its path: `ctx.client` of a procedure handler, or an adapter of a `Client`. */",
246
+ "export interface SdkClient {",
247
+ " call(path: string[], input: unknown): Promise<unknown>;",
248
+ "}",
249
+ "",
250
+ "/** The methods of the procedures, at `sdk.<namespace>.<name>`. */",
251
+ `export interface Sdk {`,
252
+ ...interfaceMembers(root, " "),
253
+ "}",
254
+ "",
255
+ );
256
+
257
+ if (hasBoth(root)) {
258
+ out.push(
259
+ "/** This function gives a function with the members of a namespace (a path that is a procedure and a namespace). */",
260
+ "function withMembers<F extends object, M extends object>(fn: F, members: M): F & M {",
261
+ " for (const [key, value] of Object.entries(members)) Object.defineProperty(fn, key, { value, enumerable: true });",
262
+ " return fn as F & M;",
263
+ "}",
264
+ "",
265
+ );
266
+ }
267
+
268
+ out.push("/** This function gives the SDK: each method calls `client.call(path, input)`. */");
269
+ if (procedures.length === 0) {
270
+ // No method uses the client, and the strict options refuse an unused parameter
271
+ out.push("export function createSdk(_client: SdkClient): Sdk {", " return {};", "}", "");
272
+ } else {
273
+ out.push(
274
+ "export function createSdk(client: SdkClient): Sdk {",
275
+ " const call = <T>(path: string[], input: unknown): Promise<T> => client.call(path, input) as Promise<T>;",
276
+ ` return ${memberValue(root, " ")};`,
277
+ "}",
278
+ "",
279
+ );
280
+ }
281
+ return out.join("\n");
282
+ }