@tsplc/compiler 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +740 -0
- package/dist/cli.js.map +13 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +423 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +105 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +282 -0
- package/project/index.ts +1 -0
- package/project/project.ts +298 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { runtimeSources } from "./module-graph.ts";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { error, type Diagnostic } from "@tsplc/compiler/ir";
|
|
5
|
+
import {
|
|
6
|
+
isIntegerType,
|
|
7
|
+
serviceContract,
|
|
8
|
+
servicePath,
|
|
9
|
+
wireLayout,
|
|
10
|
+
wireRecord,
|
|
11
|
+
type ServiceContract,
|
|
12
|
+
type WireField,
|
|
13
|
+
type WireScalar,
|
|
14
|
+
} from "@tsplc/server";
|
|
15
|
+
|
|
16
|
+
/** The server section's transport; each client's type argument names its router. */
|
|
17
|
+
export interface ServiceOptions {
|
|
18
|
+
transport: "http" | "opcua";
|
|
19
|
+
baseUrlEnv?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A procedure record read from its type, with the PLC enum each enum field names. */
|
|
23
|
+
interface RecordLayout {
|
|
24
|
+
fields: Record<string, WireField>;
|
|
25
|
+
enums: Record<string, ts.EnumDeclaration>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const property = (checker: ts.TypeChecker, type: ts.Type, name: string) => {
|
|
29
|
+
const symbol = type.getProperty(name);
|
|
30
|
+
return symbol && checker.getTypeOfSymbol(symbol);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** An `iec.*` schema's layout, which its type carries as an optional phantom property. */
|
|
34
|
+
const layoutNumber = (checker: ts.TypeChecker, type: ts.Type, name: string, label: string) => {
|
|
35
|
+
const brand = property(checker, type, wireLayout);
|
|
36
|
+
const value = brand && property(checker, checker.getNonNullableType(brand), name);
|
|
37
|
+
if (!value) return undefined;
|
|
38
|
+
if (!value.isNumberLiteral()) throw new Error(`${label}: ${name} must be a literal number.`);
|
|
39
|
+
return value.value;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const scalarLayout = (
|
|
43
|
+
checker: ts.TypeChecker,
|
|
44
|
+
type: ts.Type,
|
|
45
|
+
label: string,
|
|
46
|
+
): { field: WireScalar; enumeration?: ts.EnumDeclaration } => {
|
|
47
|
+
if (type.flags & ts.TypeFlags.Boolean) return { field: { kind: "boolean" } };
|
|
48
|
+
// An enum type is the union of its member literals.
|
|
49
|
+
const first = type.isUnion() ? type.types[0] : type;
|
|
50
|
+
const member = first?.symbol?.valueDeclaration;
|
|
51
|
+
if (first && first.flags & ts.TypeFlags.EnumLiteral && member && ts.isEnumMember(member)) {
|
|
52
|
+
const members: Record<string, number> = {};
|
|
53
|
+
for (const item of member.parent.members) {
|
|
54
|
+
const value = checker.getConstantValue(item);
|
|
55
|
+
if (typeof value !== "number")
|
|
56
|
+
throw new Error(`${label}: PLC enums require numeric members.`);
|
|
57
|
+
members[item.name.getText()] = value;
|
|
58
|
+
}
|
|
59
|
+
return { field: { kind: "enum", members }, enumeration: member.parent };
|
|
60
|
+
}
|
|
61
|
+
const capacity = layoutNumber(checker, type, "capacity", label);
|
|
62
|
+
if (capacity !== undefined) return { field: { kind: "string", capacity } };
|
|
63
|
+
const brand = property(checker, type, wireLayout);
|
|
64
|
+
const iec = brand && property(checker, checker.getNonNullableType(brand), "iec");
|
|
65
|
+
if (iec?.isStringLiteral() && isIntegerType(iec.value)) {
|
|
66
|
+
const minimum = layoutNumber(checker, type, "minimum", label);
|
|
67
|
+
const maximum = layoutNumber(checker, type, "maximum", label);
|
|
68
|
+
if (minimum !== undefined && maximum !== undefined)
|
|
69
|
+
return { field: { kind: "integer", iec: iec.value, minimum, maximum } };
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${label}: use z.boolean(), z.enum() of a numeric enum, iec.STRING(), or an integer such as iec.INT().`,
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const recordLayout = (
|
|
77
|
+
checker: ts.TypeChecker,
|
|
78
|
+
type: ts.Type,
|
|
79
|
+
label: string,
|
|
80
|
+
output: boolean,
|
|
81
|
+
): RecordLayout => {
|
|
82
|
+
const fields: Record<string, WireField> = {};
|
|
83
|
+
const enums: Record<string, ts.EnumDeclaration> = {};
|
|
84
|
+
// A procedure without `.input()` takes `void`.
|
|
85
|
+
if (!(type.flags & ts.TypeFlags.Void)) {
|
|
86
|
+
if (
|
|
87
|
+
!(type.flags & ts.TypeFlags.Object) ||
|
|
88
|
+
type.getStringIndexType() ||
|
|
89
|
+
type.getNumberIndexType() ||
|
|
90
|
+
type.getCallSignatures().length
|
|
91
|
+
)
|
|
92
|
+
throw new Error(`${label}: PLC procedures exchange one closed z.strictObject() record.`);
|
|
93
|
+
for (const field of type.getProperties()) {
|
|
94
|
+
const key = `${label}.${field.name}`;
|
|
95
|
+
if (field.flags & ts.SymbolFlags.Optional)
|
|
96
|
+
throw new Error(`${key}: optional wire fields are unsupported.`);
|
|
97
|
+
const value = checker.getTypeOfSymbol(field);
|
|
98
|
+
const length = layoutNumber(checker, value, "length", key);
|
|
99
|
+
const element = length === undefined ? undefined : value.getNumberIndexType();
|
|
100
|
+
const scalar = scalarLayout(checker, element ?? value, key);
|
|
101
|
+
fields[field.name] =
|
|
102
|
+
length === undefined ? scalar.field : { kind: "array", length, element: scalar.field };
|
|
103
|
+
if (scalar.enumeration) enums[field.name] = scalar.enumeration;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { fields: wireRecord(fields, label, output).fields, enums };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Identifies a record by layout, so every name for it lowers to one PLC struct. */
|
|
110
|
+
const recordKey = (layout: RecordLayout) =>
|
|
111
|
+
JSON.stringify([
|
|
112
|
+
layout.fields,
|
|
113
|
+
Object.entries(layout.enums)
|
|
114
|
+
.map(([field, value]) => [field, value.getSourceFile().fileName, value.name.text])
|
|
115
|
+
.sort(),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const procedureAt = (checker: ts.TypeChecker, router: ts.Type, path: readonly string[]) => {
|
|
119
|
+
let type = property(checker, router, "_def");
|
|
120
|
+
type = type && property(checker, type, "record");
|
|
121
|
+
for (const part of path) type = type && property(checker, type, part);
|
|
122
|
+
const definition = type && property(checker, type, "_def");
|
|
123
|
+
if (!definition || !property(checker, definition, "procedure")) return undefined;
|
|
124
|
+
const kind = property(checker, definition, "type");
|
|
125
|
+
const types = property(checker, definition, "$types");
|
|
126
|
+
const input = types && property(checker, types, "input");
|
|
127
|
+
const output = types && property(checker, types, "output");
|
|
128
|
+
if (!kind?.isStringLiteral() || !input || !output) return undefined;
|
|
129
|
+
return { kind: kind.value, input, output };
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const pascal = (path: string) =>
|
|
133
|
+
path
|
|
134
|
+
.split(".")
|
|
135
|
+
.map((part) => part[0]!.toUpperCase() + part.slice(1))
|
|
136
|
+
.join("");
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Replaces each `createServiceClient<Router>()` call with an in-memory module of the called
|
|
140
|
+
* procedures, read from the router's type: the server never runs and no file is written.
|
|
141
|
+
*/
|
|
142
|
+
export const lowerServices = (
|
|
143
|
+
program: ts.Program,
|
|
144
|
+
sdk: string,
|
|
145
|
+
options: ServiceOptions | undefined,
|
|
146
|
+
) => {
|
|
147
|
+
const sources = new Map<string, string>();
|
|
148
|
+
const diagnostics: Diagnostic[] = [];
|
|
149
|
+
const records = new Map<string, { name: string; layout: RecordLayout }>();
|
|
150
|
+
const calls = new Map<string, { contract: ServiceContract; input: string; output: string }>();
|
|
151
|
+
const checker = program.getTypeChecker();
|
|
152
|
+
const symbol = (node: ts.Node) => {
|
|
153
|
+
let value = checker.getSymbolAtLocation(node);
|
|
154
|
+
while (value && value.flags & ts.SymbolFlags.Alias) value = checker.getAliasedSymbol(value);
|
|
155
|
+
return value;
|
|
156
|
+
};
|
|
157
|
+
const fail = (node: ts.Node, code: string, message: string) => {
|
|
158
|
+
const source = node.getSourceFile(),
|
|
159
|
+
start = node.getStart(source),
|
|
160
|
+
position = source.getLineAndCharacterOfPosition(start);
|
|
161
|
+
diagnostics.push(
|
|
162
|
+
error(code, message, {
|
|
163
|
+
file: source.fileName,
|
|
164
|
+
start,
|
|
165
|
+
end: node.end,
|
|
166
|
+
line: position.line + 1,
|
|
167
|
+
column: position.character + 1,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
const record = (layout: RecordLayout, name: string) => {
|
|
172
|
+
const key = recordKey(layout);
|
|
173
|
+
const existing = records.get(key);
|
|
174
|
+
if (existing) return existing.name;
|
|
175
|
+
records.set(key, { name, layout });
|
|
176
|
+
return name;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const clients = new Map<ts.Symbol, ts.Type>();
|
|
180
|
+
let file: string | undefined;
|
|
181
|
+
const sourceFiles = runtimeSources(program).filter((source) => !source.isDeclarationFile);
|
|
182
|
+
for (const source of sourceFiles)
|
|
183
|
+
for (const statement of source.statements) {
|
|
184
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
185
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
186
|
+
const call = declaration.initializer;
|
|
187
|
+
if (
|
|
188
|
+
!call ||
|
|
189
|
+
!ts.isCallExpression(call) ||
|
|
190
|
+
symbol(call.expression)?.name !== "createServiceClient" ||
|
|
191
|
+
!symbol(call.expression)?.declarations?.some((value) =>
|
|
192
|
+
value.getSourceFile().fileName.startsWith(dirname(sdk) + "/"),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
continue;
|
|
196
|
+
const router = call.typeArguments?.[0];
|
|
197
|
+
const client = symbol(declaration.name);
|
|
198
|
+
if (!router || !client || !(statement.declarationList.flags & ts.NodeFlags.Const))
|
|
199
|
+
fail(
|
|
200
|
+
declaration,
|
|
201
|
+
"PLC1013",
|
|
202
|
+
"Create a service client as a module constant: const services = createServiceClient<AppRouter>().",
|
|
203
|
+
);
|
|
204
|
+
else if (!options)
|
|
205
|
+
fail(call, "PLC1013", "Configure server in tsplc.config.ts to call the server.");
|
|
206
|
+
else if (options.transport === "http" && !options.baseUrlEnv)
|
|
207
|
+
fail(call, "PLC1013", "HTTP service transport requires server.baseUrlEnv.");
|
|
208
|
+
else {
|
|
209
|
+
clients.set(client, checker.getTypeFromTypeNode(router));
|
|
210
|
+
file ??= join(dirname(source.fileName), ".tsplc-services.ts");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const source of sourceFiles) {
|
|
216
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
217
|
+
const imports = new Map<string, string>();
|
|
218
|
+
const visit = (node: ts.Node): void => {
|
|
219
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
220
|
+
const clause = node.importClause;
|
|
221
|
+
const bindings = clause?.namedBindings;
|
|
222
|
+
const typeOnly =
|
|
223
|
+
clause?.isTypeOnly ||
|
|
224
|
+
(!clause?.name &&
|
|
225
|
+
bindings &&
|
|
226
|
+
ts.isNamedImports(bindings) &&
|
|
227
|
+
bindings.elements.every((element) => element.isTypeOnly));
|
|
228
|
+
if (!typeOnly && clause && bindings && ts.isNamedImports(bindings)) {
|
|
229
|
+
const elements = bindings.elements.filter((element) => {
|
|
230
|
+
const imported = symbol(element.name);
|
|
231
|
+
return !imported || !clients.has(imported);
|
|
232
|
+
});
|
|
233
|
+
if (elements.length !== bindings.elements.length)
|
|
234
|
+
edits.push({
|
|
235
|
+
start: node.getStart(source),
|
|
236
|
+
end: node.end,
|
|
237
|
+
text:
|
|
238
|
+
elements.length || clause.name
|
|
239
|
+
? ts
|
|
240
|
+
.createPrinter()
|
|
241
|
+
.printNode(
|
|
242
|
+
ts.EmitHint.Unspecified,
|
|
243
|
+
ts.factory.updateImportDeclaration(
|
|
244
|
+
node,
|
|
245
|
+
node.modifiers,
|
|
246
|
+
ts.factory.updateImportClause(
|
|
247
|
+
clause,
|
|
248
|
+
clause.isTypeOnly,
|
|
249
|
+
clause.name,
|
|
250
|
+
ts.factory.updateNamedImports(bindings, elements),
|
|
251
|
+
),
|
|
252
|
+
node.moduleSpecifier,
|
|
253
|
+
node.attributes,
|
|
254
|
+
),
|
|
255
|
+
source,
|
|
256
|
+
)
|
|
257
|
+
: "",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (
|
|
261
|
+
!typeOnly &&
|
|
262
|
+
(node.moduleSpecifier.text.startsWith("node:") ||
|
|
263
|
+
["@tsplc/server", "@trpc/server", "zod", "fs", "http", "https", "net", "tls"].includes(
|
|
264
|
+
node.moduleSpecifier.text,
|
|
265
|
+
))
|
|
266
|
+
)
|
|
267
|
+
fail(
|
|
268
|
+
node,
|
|
269
|
+
"PLC1013",
|
|
270
|
+
`Server runtime import ${node.moduleSpecifier.text} cannot execute in PLC code. Expose a PLC procedure on the server router and call it through createServiceClient instead.`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (ts.isVariableStatement(node)) {
|
|
274
|
+
const clientStatement = node.declarationList.declarations.some((declaration) => {
|
|
275
|
+
const declared = symbol(declaration.name);
|
|
276
|
+
return declared && clients.has(declared);
|
|
277
|
+
});
|
|
278
|
+
if (clientStatement) {
|
|
279
|
+
edits.push({ start: node.getStart(source), end: node.end, text: "" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (clients.size && ts.isPropertyAccessExpression(node)) {
|
|
284
|
+
const parts: string[] = [];
|
|
285
|
+
let root: ts.Expression = node;
|
|
286
|
+
while (ts.isPropertyAccessExpression(root)) {
|
|
287
|
+
parts.unshift(root.name.text);
|
|
288
|
+
root = root.expression;
|
|
289
|
+
}
|
|
290
|
+
const client = symbol(root);
|
|
291
|
+
const router = client && clients.get(client);
|
|
292
|
+
const procedure = router && procedureAt(checker, router, parts);
|
|
293
|
+
if (router) {
|
|
294
|
+
const path = parts.join(".");
|
|
295
|
+
if (!procedure) {
|
|
296
|
+
fail(node, "PLC1013", `${path} is not a procedure of the client's router.`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
let call = calls.get(path);
|
|
300
|
+
if (!call) {
|
|
301
|
+
try {
|
|
302
|
+
if (procedure.kind !== "query" && procedure.kind !== "mutation")
|
|
303
|
+
throw new Error(
|
|
304
|
+
`${path}: subscriptions use a different transport; expose a query or mutation to the PLC.`,
|
|
305
|
+
);
|
|
306
|
+
const input = recordLayout(checker, procedure.input, `${path}.input`, false);
|
|
307
|
+
const output = recordLayout(checker, procedure.output, `${path}.output`, true);
|
|
308
|
+
call = {
|
|
309
|
+
contract: serviceContract(
|
|
310
|
+
path,
|
|
311
|
+
procedure.kind,
|
|
312
|
+
{ fields: input.fields },
|
|
313
|
+
{ fields: output.fields },
|
|
314
|
+
),
|
|
315
|
+
input: record(input, `${pascal(path)}Input`),
|
|
316
|
+
output: record(output, `${pascal(path)}Output`),
|
|
317
|
+
};
|
|
318
|
+
calls.set(path, call);
|
|
319
|
+
} catch (cause) {
|
|
320
|
+
fail(node, "PLC2001", cause instanceof Error ? cause.message : String(cause));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const name = `tsplcService_${path.replaceAll(".", "_")}`;
|
|
325
|
+
const local = imports.get(name) ?? `tsplcRemote${imports.size}`;
|
|
326
|
+
imports.set(name, local);
|
|
327
|
+
edits.push({ start: node.getStart(source), end: node.end, text: local });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
ts.forEachChild(node, visit);
|
|
332
|
+
};
|
|
333
|
+
visit(source);
|
|
334
|
+
if (!edits.length) continue;
|
|
335
|
+
let text = source.text;
|
|
336
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
337
|
+
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
338
|
+
if (imports.size && file) {
|
|
339
|
+
const path = relative(dirname(source.fileName), file).replaceAll("\\", "/");
|
|
340
|
+
text += `\nimport {${[...imports].map(([name, local]) => `${name} as ${local}`).join(",")}} from ${JSON.stringify(path.startsWith("../") ? path : `./${path}`)};\n`;
|
|
341
|
+
}
|
|
342
|
+
sources.set(source.fileName, text);
|
|
343
|
+
}
|
|
344
|
+
if (file && options) sources.set(file, serviceModule(records, calls, options));
|
|
345
|
+
return {
|
|
346
|
+
sources,
|
|
347
|
+
diagnostics,
|
|
348
|
+
file,
|
|
349
|
+
records: new Map([...records].map(([key, { name }]) => [key, name])),
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const serviceModule = (
|
|
354
|
+
records: ReadonlyMap<string, { name: string; layout: RecordLayout }>,
|
|
355
|
+
calls: ReadonlyMap<string, { contract: ServiceContract; input: string; output: string }>,
|
|
356
|
+
options: ServiceOptions,
|
|
357
|
+
) => {
|
|
358
|
+
const enums = new Map<ts.EnumDeclaration, string>();
|
|
359
|
+
const enumName = (declaration: ts.EnumDeclaration) => {
|
|
360
|
+
const name = enums.get(declaration) ?? `tsplcEnum${enums.size}`;
|
|
361
|
+
enums.set(declaration, name);
|
|
362
|
+
return name;
|
|
363
|
+
};
|
|
364
|
+
const scalar = (field: WireScalar, enumeration: ts.EnumDeclaration | undefined) => {
|
|
365
|
+
switch (field.kind) {
|
|
366
|
+
case "boolean":
|
|
367
|
+
return "BOOL";
|
|
368
|
+
case "integer":
|
|
369
|
+
return field.iec;
|
|
370
|
+
case "string":
|
|
371
|
+
return `STRING<${field.capacity}>`;
|
|
372
|
+
case "enum":
|
|
373
|
+
return enumName(enumeration!);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const interfaces = [...records.values()].map(({ name, layout }) => {
|
|
377
|
+
const fields = Object.entries(layout.fields).map(([key, field]) => {
|
|
378
|
+
const enumeration = layout.enums[key];
|
|
379
|
+
const type =
|
|
380
|
+
field.kind === "array"
|
|
381
|
+
? `FixedArray<${scalar(field.element, enumeration)},${field.length}>`
|
|
382
|
+
: scalar(field, enumeration);
|
|
383
|
+
return `${key}:${type}`;
|
|
384
|
+
});
|
|
385
|
+
return `export interface ${name}{${fields.join(";")}}`;
|
|
386
|
+
});
|
|
387
|
+
const functions = [...calls.values()]
|
|
388
|
+
.sort((left, right) => left.contract.path.localeCompare(right.contract.path))
|
|
389
|
+
.map(({ contract, input, output }) => {
|
|
390
|
+
const hasInput = Object.keys(contract.input.fields).length > 0;
|
|
391
|
+
const request = hasInput ? "input" : `defaultValue<${input}>()`;
|
|
392
|
+
const operation =
|
|
393
|
+
options.transport === "http"
|
|
394
|
+
? `tsplcServiceFetch<${output}>(\`\${tsplcServiceBaseUrl}${servicePath(contract)}\`,{method:"POST",json:${request}})`
|
|
395
|
+
: `tsplcServiceCall<${output},${input}>(${JSON.stringify(servicePath(contract))},${request})`;
|
|
396
|
+
return `export const tsplcService_${contract.path.replaceAll(".", "_")}=async(${hasInput ? `input:${input}` : ""}):Promise<${output}>=>{return await ${operation};};`;
|
|
397
|
+
});
|
|
398
|
+
const transport =
|
|
399
|
+
options.transport === "http"
|
|
400
|
+
? `import {fetch as tsplcServiceFetch} from "@tsplc/compiler/async";\nimport {env as tsplcServiceEnv} from "@tsplc/compiler/config";`
|
|
401
|
+
: `import {opcuaCall as tsplcServiceCall} from "@tsplc/compiler/async";`;
|
|
402
|
+
const enumImports = [...enums].map(
|
|
403
|
+
([declaration, name]) =>
|
|
404
|
+
`import type {${declaration.name.text} as ${name}} from ${JSON.stringify(declaration.getSourceFile().fileName)};`,
|
|
405
|
+
);
|
|
406
|
+
const baseUrl =
|
|
407
|
+
options.transport === "http"
|
|
408
|
+
? `const tsplcServiceBaseUrl:STRING<512>=tsplcServiceEnv<STRING<512>>(${JSON.stringify(options.baseUrlEnv)});`
|
|
409
|
+
: "";
|
|
410
|
+
return [transport, ...enumImports, baseUrl, ...interfaces, ...functions].join("\n") + "\n";
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/** Lowers a type that names a called procedure's record, such as `inferRouterOutputs<AppRouter>["a"]["b"]`. */
|
|
414
|
+
export const serviceRecords = (
|
|
415
|
+
program: ts.Program,
|
|
416
|
+
lowering: { file: string | undefined; records: ReadonlyMap<string, string> },
|
|
417
|
+
) => {
|
|
418
|
+
const source = lowering.file && program.getSourceFile(lowering.file);
|
|
419
|
+
if (!source) return undefined;
|
|
420
|
+
const checker = program.getTypeChecker();
|
|
421
|
+
const declarations = new Map(
|
|
422
|
+
source.statements.filter(ts.isInterfaceDeclaration).map((value) => [value.name.text, value]),
|
|
423
|
+
);
|
|
424
|
+
return (type: ts.Type): ts.Symbol | undefined => {
|
|
425
|
+
let key: string;
|
|
426
|
+
try {
|
|
427
|
+
key = recordKey(recordLayout(checker, type, "record", false));
|
|
428
|
+
} catch {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
const name = lowering.records.get(key);
|
|
432
|
+
const declaration = name === undefined ? undefined : declarations.get(name);
|
|
433
|
+
return declaration && checker.getSymbolAtLocation(declaration.name);
|
|
434
|
+
};
|
|
435
|
+
};
|