@dusted/anqst 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 +13 -0
- package/README.md +136 -0
- package/dist/src/app.js +369 -0
- package/dist/src/bin/anqst.js +8 -0
- package/dist/src/emit.js +2500 -0
- package/dist/src/errors.js +24 -0
- package/dist/src/model.js +2 -0
- package/dist/src/parser.js +259 -0
- package/dist/src/project.js +123 -0
- package/dist/src/verify.js +200 -0
- package/package.json +53 -0
- package/spec/AnQst-Spec-DSL.d.ts +217 -0
package/dist/src/emit.js
ADDED
|
@@ -0,0 +1,2500 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateOutputs = generateOutputs;
|
|
7
|
+
exports.writeGeneratedOutputs = writeGeneratedOutputs;
|
|
8
|
+
exports.installTypeScriptOutputs = installTypeScriptOutputs;
|
|
9
|
+
exports.installEmbeddedWebBundle = installEmbeddedWebBundle;
|
|
10
|
+
exports.installQtIntegrationCMake = installQtIntegrationCMake;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
14
|
+
function stripAnQstType(typeText) {
|
|
15
|
+
return typeText
|
|
16
|
+
.replace(/\bAnQst\.Type\.stringArray\b/g, "string[]")
|
|
17
|
+
.replace(/\bAnQst\.Type\.string\b/g, "string")
|
|
18
|
+
.replace(/\bAnQst\.Type\.number\b/g, "number")
|
|
19
|
+
.replace(/\bAnQst\.Type\.qint64\b/g, "bigint")
|
|
20
|
+
.replace(/\bAnQst\.Type\.quint64\b/g, "bigint")
|
|
21
|
+
.replace(/\bAnQst\.Type\.qint32\b/g, "number")
|
|
22
|
+
.replace(/\bAnQst\.Type\.quint32\b/g, "number")
|
|
23
|
+
.replace(/\bAnQst\.Type\.object\b/g, "object")
|
|
24
|
+
.replace(/\bAnQst\.Type\.json\b/g, "object");
|
|
25
|
+
}
|
|
26
|
+
function splitGeneric(typeText) {
|
|
27
|
+
const m = typeText.match(/^([A-Za-z0-9_.]+)<(.+)>$/);
|
|
28
|
+
if (!m)
|
|
29
|
+
return null;
|
|
30
|
+
return { name: m[1], arg: m[2].trim() };
|
|
31
|
+
}
|
|
32
|
+
function mapTsTypeToCpp(typeText) {
|
|
33
|
+
const raw = typeText.trim();
|
|
34
|
+
if (/\bAnQst\.Type\.qint64\b/.test(raw))
|
|
35
|
+
return "qint64";
|
|
36
|
+
if (/\bAnQst\.Type\.quint64\b/.test(raw))
|
|
37
|
+
return "quint64";
|
|
38
|
+
if (/\bAnQst\.Type\.qint32\b/.test(raw))
|
|
39
|
+
return "qint32";
|
|
40
|
+
if (/\bAnQst\.Type\.quint32\b/.test(raw))
|
|
41
|
+
return "quint32";
|
|
42
|
+
if (/\bAnQst\.Type\.qint16\b/.test(raw))
|
|
43
|
+
return "qint16";
|
|
44
|
+
if (/\bAnQst\.Type\.quint16\b/.test(raw))
|
|
45
|
+
return "quint16";
|
|
46
|
+
if (/\bAnQst\.Type\.qint8\b/.test(raw))
|
|
47
|
+
return "qint8";
|
|
48
|
+
if (/\bAnQst\.Type\.quint8\b/.test(raw))
|
|
49
|
+
return "quint8";
|
|
50
|
+
if (/\bAnQst\.Type\.stringArray\b/.test(raw))
|
|
51
|
+
return "QStringList";
|
|
52
|
+
if (/\bAnQst\.Type\.string\b/.test(raw))
|
|
53
|
+
return "QString";
|
|
54
|
+
if (/\bAnQst\.Type\.json\b/.test(raw) || /\bAnQst\.Type\.object\b/.test(raw))
|
|
55
|
+
return "QVariantMap";
|
|
56
|
+
if (/\bAnQst\.Type\.(u?int(8|16|32))\b/.test(raw)) {
|
|
57
|
+
const narrowed = raw.match(/\bAnQst\.Type\.(u?int(?:8|16|32))\b/)?.[1];
|
|
58
|
+
if (narrowed === "int8")
|
|
59
|
+
return "int8_t";
|
|
60
|
+
if (narrowed === "uint8")
|
|
61
|
+
return "uint8_t";
|
|
62
|
+
if (narrowed === "int16")
|
|
63
|
+
return "int16_t";
|
|
64
|
+
if (narrowed === "uint16")
|
|
65
|
+
return "uint16_t";
|
|
66
|
+
if (narrowed === "int32")
|
|
67
|
+
return "int32_t";
|
|
68
|
+
if (narrowed === "uint32")
|
|
69
|
+
return "uint32_t";
|
|
70
|
+
}
|
|
71
|
+
const t = stripAnQstType(raw);
|
|
72
|
+
if (t === "string")
|
|
73
|
+
return "QString";
|
|
74
|
+
if (t === "number")
|
|
75
|
+
return "double";
|
|
76
|
+
if (t === "boolean")
|
|
77
|
+
return "bool";
|
|
78
|
+
if (t === "bigint")
|
|
79
|
+
return "qint64";
|
|
80
|
+
if (t === "void")
|
|
81
|
+
return "void";
|
|
82
|
+
if (t === "object")
|
|
83
|
+
return "QVariantMap";
|
|
84
|
+
if (t.endsWith("[]")) {
|
|
85
|
+
return `QList<${mapTsTypeToCpp(t.slice(0, -2))}>`;
|
|
86
|
+
}
|
|
87
|
+
const g = splitGeneric(t);
|
|
88
|
+
if (g && g.name === "Array")
|
|
89
|
+
return `QList<${mapTsTypeToCpp(g.arg)}>`;
|
|
90
|
+
if (g && g.name === "ReadonlyArray")
|
|
91
|
+
return `QList<${mapTsTypeToCpp(g.arg)}>`;
|
|
92
|
+
if (g && g.name === "Record")
|
|
93
|
+
return "QVariantMap";
|
|
94
|
+
if (g && g.name === "Partial")
|
|
95
|
+
return mapTsTypeToCpp(g.arg);
|
|
96
|
+
if (g && g.name === "Promise")
|
|
97
|
+
return mapTsTypeToCpp(g.arg);
|
|
98
|
+
if (t.includes("|"))
|
|
99
|
+
return "QString";
|
|
100
|
+
return t;
|
|
101
|
+
}
|
|
102
|
+
function toCppArgs(member) {
|
|
103
|
+
return member.parameters.map((p) => `${mapTsTypeToCpp(p.typeText)} ${p.name}`).join(", ");
|
|
104
|
+
}
|
|
105
|
+
function callbackName(memberName) {
|
|
106
|
+
return `${memberName.charAt(0).toUpperCase()}${memberName.slice(1)}Callback`;
|
|
107
|
+
}
|
|
108
|
+
function pascalCase(value) {
|
|
109
|
+
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
110
|
+
}
|
|
111
|
+
function variantToCppExpression(cppType, expr) {
|
|
112
|
+
if (cppType === "QString")
|
|
113
|
+
return `${expr}.toString()`;
|
|
114
|
+
if (cppType === "QStringList")
|
|
115
|
+
return `${expr}.toStringList()`;
|
|
116
|
+
if (cppType === "QVariantMap")
|
|
117
|
+
return `${expr}.toMap()`;
|
|
118
|
+
if (cppType === "double")
|
|
119
|
+
return `${expr}.toDouble()`;
|
|
120
|
+
if (cppType === "bool")
|
|
121
|
+
return `${expr}.toBool()`;
|
|
122
|
+
if (cppType === "qint64")
|
|
123
|
+
return `${expr}.toLongLong()`;
|
|
124
|
+
if (cppType === "quint64")
|
|
125
|
+
return `${expr}.toULongLong()`;
|
|
126
|
+
if (cppType === "qint32" || cppType === "quint32")
|
|
127
|
+
return `${expr}.toInt()`;
|
|
128
|
+
if (cppType === "qint16" || cppType === "quint16")
|
|
129
|
+
return `static_cast<${cppType}>(${expr}.toInt())`;
|
|
130
|
+
if (cppType === "qint8" || cppType === "quint8")
|
|
131
|
+
return `static_cast<${cppType}>(${expr}.toInt())`;
|
|
132
|
+
if (cppType === "int8_t" || cppType === "uint8_t" || cppType === "int16_t" || cppType === "uint16_t" || cppType === "int32_t" || cppType === "uint32_t") {
|
|
133
|
+
return `static_cast<${cppType}>(${expr}.toInt())`;
|
|
134
|
+
}
|
|
135
|
+
return `${expr}.value<${cppType}>()`;
|
|
136
|
+
}
|
|
137
|
+
function cppToVariantExpression(cppType, expr) {
|
|
138
|
+
if (cppType === "QString" ||
|
|
139
|
+
cppType === "QStringList" ||
|
|
140
|
+
cppType === "QVariantMap" ||
|
|
141
|
+
cppType === "double" ||
|
|
142
|
+
cppType === "bool" ||
|
|
143
|
+
cppType === "qint64" ||
|
|
144
|
+
cppType === "quint64" ||
|
|
145
|
+
cppType === "qint32" ||
|
|
146
|
+
cppType === "quint32" ||
|
|
147
|
+
cppType === "qint16" ||
|
|
148
|
+
cppType === "quint16" ||
|
|
149
|
+
cppType === "qint8" ||
|
|
150
|
+
cppType === "quint8" ||
|
|
151
|
+
cppType === "int8_t" ||
|
|
152
|
+
cppType === "uint8_t" ||
|
|
153
|
+
cppType === "int16_t" ||
|
|
154
|
+
cppType === "uint16_t" ||
|
|
155
|
+
cppType === "int32_t" ||
|
|
156
|
+
cppType === "uint32_t") {
|
|
157
|
+
return `QVariant::fromValue(${expr})`;
|
|
158
|
+
}
|
|
159
|
+
return `QVariant::fromValue(${expr})`;
|
|
160
|
+
}
|
|
161
|
+
function collectStructDecls(spec) {
|
|
162
|
+
const out = new Map();
|
|
163
|
+
for (const d of spec.namespaceTypeDecls)
|
|
164
|
+
out.set(d.name, d);
|
|
165
|
+
for (const d of spec.importedTypeDecls.values())
|
|
166
|
+
out.set(d.name, d);
|
|
167
|
+
return [...out.values()];
|
|
168
|
+
}
|
|
169
|
+
function mapTypeTextToTs(typeText) {
|
|
170
|
+
return stripAnQstType(typeText.trim());
|
|
171
|
+
}
|
|
172
|
+
function parseTypeNodeFromText(typeText) {
|
|
173
|
+
const source = typescript_1.default.createSourceFile("__inline__.ts", `type __X = ${typeText};`, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
174
|
+
const stmt = source.statements.find(typescript_1.default.isTypeAliasDeclaration);
|
|
175
|
+
if (!stmt) {
|
|
176
|
+
throw new Error(`Unable to parse type text: ${typeText}`);
|
|
177
|
+
}
|
|
178
|
+
return stmt.type;
|
|
179
|
+
}
|
|
180
|
+
function qNameText(name) {
|
|
181
|
+
if (typescript_1.default.isIdentifier(name))
|
|
182
|
+
return name.text;
|
|
183
|
+
return `${qNameText(name.left)}.${name.right.text}`;
|
|
184
|
+
}
|
|
185
|
+
function typeRefs(typeNode) {
|
|
186
|
+
const refs = new Set();
|
|
187
|
+
const visit = (node) => {
|
|
188
|
+
if (typescript_1.default.isTypeReferenceNode(node))
|
|
189
|
+
refs.add(qNameText(node.typeName));
|
|
190
|
+
typescript_1.default.forEachChild(node, visit);
|
|
191
|
+
};
|
|
192
|
+
visit(typeNode);
|
|
193
|
+
return [...refs];
|
|
194
|
+
}
|
|
195
|
+
function isBuiltinOrLiteral(ref) {
|
|
196
|
+
return [
|
|
197
|
+
"string",
|
|
198
|
+
"number",
|
|
199
|
+
"boolean",
|
|
200
|
+
"void",
|
|
201
|
+
"object",
|
|
202
|
+
"bigint",
|
|
203
|
+
"BigInt",
|
|
204
|
+
"Array",
|
|
205
|
+
"ReadonlyArray",
|
|
206
|
+
"Record",
|
|
207
|
+
"Partial",
|
|
208
|
+
"Readonly",
|
|
209
|
+
"Date"
|
|
210
|
+
].includes(ref);
|
|
211
|
+
}
|
|
212
|
+
function collectReachableNamespaceDecls(spec) {
|
|
213
|
+
const localByName = new Map(spec.namespaceTypeDecls.map((decl) => [decl.name, decl]));
|
|
214
|
+
const queue = [...spec.namespaceTypeDecls.map((decl) => decl.name)];
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
const ordered = [];
|
|
217
|
+
while (queue.length > 0) {
|
|
218
|
+
const current = queue.shift();
|
|
219
|
+
if (seen.has(current))
|
|
220
|
+
continue;
|
|
221
|
+
seen.add(current);
|
|
222
|
+
const decl = localByName.get(current);
|
|
223
|
+
if (!decl)
|
|
224
|
+
continue;
|
|
225
|
+
ordered.push(decl);
|
|
226
|
+
for (const ref of decl.referencedTypeNames) {
|
|
227
|
+
if (localByName.has(ref) && !seen.has(ref)) {
|
|
228
|
+
queue.push(ref);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return ordered;
|
|
233
|
+
}
|
|
234
|
+
function collectRequiredImportedSymbols(spec) {
|
|
235
|
+
const required = new Set();
|
|
236
|
+
const localTypeNames = new Set(spec.namespaceTypeDecls.map((d) => d.name));
|
|
237
|
+
const collectRef = (ref) => {
|
|
238
|
+
if (isBuiltinOrLiteral(ref))
|
|
239
|
+
return;
|
|
240
|
+
if (ref.startsWith("AnQst."))
|
|
241
|
+
return;
|
|
242
|
+
if (localTypeNames.has(ref))
|
|
243
|
+
return;
|
|
244
|
+
required.add(ref.split(".")[0]);
|
|
245
|
+
};
|
|
246
|
+
for (const decl of collectReachableNamespaceDecls(spec)) {
|
|
247
|
+
for (const ref of decl.referencedTypeNames)
|
|
248
|
+
collectRef(ref);
|
|
249
|
+
}
|
|
250
|
+
for (const service of spec.services) {
|
|
251
|
+
for (const member of service.members) {
|
|
252
|
+
const texts = [...member.parameters.map((p) => p.typeText)];
|
|
253
|
+
if (member.payloadTypeText)
|
|
254
|
+
texts.push(member.payloadTypeText);
|
|
255
|
+
for (const typeText of texts) {
|
|
256
|
+
for (const ref of typeRefs(parseTypeNodeFromText(typeText))) {
|
|
257
|
+
collectRef(ref);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return required;
|
|
263
|
+
}
|
|
264
|
+
function normalizeImportPathForGenerated(specFilePath, generatedFileRelPath, moduleSpecifier) {
|
|
265
|
+
if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
266
|
+
return moduleSpecifier;
|
|
267
|
+
}
|
|
268
|
+
const specDir = node_path_1.default.dirname(specFilePath);
|
|
269
|
+
const generatedAbs = node_path_1.default.resolve(node_path_1.default.dirname(specFilePath), "generated_output", generatedFileRelPath);
|
|
270
|
+
const generatedDir = node_path_1.default.dirname(generatedAbs);
|
|
271
|
+
const resolvedModulePath = node_path_1.default.resolve(specDir, moduleSpecifier);
|
|
272
|
+
const relative = node_path_1.default.relative(generatedDir, resolvedModulePath);
|
|
273
|
+
const normalized = relative.split(node_path_1.default.sep).join("/");
|
|
274
|
+
if (normalized.startsWith("."))
|
|
275
|
+
return normalized;
|
|
276
|
+
return `./${normalized}`;
|
|
277
|
+
}
|
|
278
|
+
function renderRequiredTypeImports(spec, generatedFileRelPath) {
|
|
279
|
+
const requiredSymbols = collectRequiredImportedSymbols(spec);
|
|
280
|
+
if (requiredSymbols.size === 0)
|
|
281
|
+
return "";
|
|
282
|
+
const importLines = [];
|
|
283
|
+
for (const imp of spec.specImports) {
|
|
284
|
+
const defaultImport = imp.defaultImport && requiredSymbols.has(imp.defaultImport) ? imp.defaultImport : null;
|
|
285
|
+
const named = imp.namedImports.filter((n) => requiredSymbols.has(n.localName));
|
|
286
|
+
if (!defaultImport && named.length === 0)
|
|
287
|
+
continue;
|
|
288
|
+
const moduleSpecifier = normalizeImportPathForGenerated(spec.filePath, generatedFileRelPath, imp.moduleSpecifier);
|
|
289
|
+
const namedClause = named
|
|
290
|
+
.map((n) => (n.importedName === n.localName ? n.localName : `${n.importedName} as ${n.localName}`))
|
|
291
|
+
.join(", ");
|
|
292
|
+
if (defaultImport && named.length > 0) {
|
|
293
|
+
importLines.push(`import type ${defaultImport}, { ${namedClause} } from "${moduleSpecifier}";`);
|
|
294
|
+
}
|
|
295
|
+
else if (defaultImport) {
|
|
296
|
+
importLines.push(`import type ${defaultImport} from "${moduleSpecifier}";`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
importLines.push(`import type { ${namedClause} } from "${moduleSpecifier}";`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return importLines.length > 0 ? `${importLines.join("\n")}\n\n` : "";
|
|
303
|
+
}
|
|
304
|
+
function parseTypeDeclNode(nodeText) {
|
|
305
|
+
const sf = typescript_1.default.createSourceFile("__decl.ts", nodeText, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
306
|
+
for (const stmt of sf.statements) {
|
|
307
|
+
if (typescript_1.default.isInterfaceDeclaration(stmt) || typescript_1.default.isTypeAliasDeclaration(stmt))
|
|
308
|
+
return stmt;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
class CppTypeNormalizer {
|
|
313
|
+
constructor(spec) {
|
|
314
|
+
this.declMap = new Map();
|
|
315
|
+
this.seedOrder = [];
|
|
316
|
+
this.allKnownNames = new Set();
|
|
317
|
+
this.usedNames = new Set();
|
|
318
|
+
for (const decl of collectStructDecls(spec)) {
|
|
319
|
+
this.allKnownNames.add(decl.name);
|
|
320
|
+
this.usedNames.add(decl.name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
addSeedDecl(decl) {
|
|
324
|
+
const node = parseTypeDeclNode(decl.nodeText);
|
|
325
|
+
if (!node)
|
|
326
|
+
return;
|
|
327
|
+
if (this.declMap.has(decl.name))
|
|
328
|
+
return;
|
|
329
|
+
const normalized = this.normalizeNamedDecl(decl.name, node);
|
|
330
|
+
this.declMap.set(decl.name, normalized);
|
|
331
|
+
this.seedOrder.push(decl.name);
|
|
332
|
+
}
|
|
333
|
+
mapTypeText(typeText, nameHintParts) {
|
|
334
|
+
const node = parseTypeNodeFromText(typeText);
|
|
335
|
+
return this.mapTypeNode(node, nameHintParts, new Set());
|
|
336
|
+
}
|
|
337
|
+
buildContext() {
|
|
338
|
+
const order = this.topologicalOrder();
|
|
339
|
+
const orderedDecls = order.map((name) => this.declMap.get(name)).filter((x) => !!x);
|
|
340
|
+
const structNames = orderedDecls.filter((d) => d.kind === "struct").map((d) => d.name);
|
|
341
|
+
return {
|
|
342
|
+
orderedDecls,
|
|
343
|
+
structNames,
|
|
344
|
+
mapTypeText: (typeText, nameHintParts) => this.mapTypeText(typeText, nameHintParts)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
normalizeNamedDecl(name, node) {
|
|
348
|
+
if (typescript_1.default.isInterfaceDeclaration(node)) {
|
|
349
|
+
const deps = new Set();
|
|
350
|
+
const fields = [];
|
|
351
|
+
for (const member of node.members) {
|
|
352
|
+
if (!typescript_1.default.isPropertySignature(member) || !member.type || !typescript_1.default.isIdentifier(member.name))
|
|
353
|
+
continue;
|
|
354
|
+
const baseType = this.mapTypeNode(member.type, [name, member.name.text], deps);
|
|
355
|
+
fields.push({
|
|
356
|
+
name: member.name.text,
|
|
357
|
+
cppType: baseType,
|
|
358
|
+
optional: !!member.questionToken
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return { name, kind: "struct", fields, aliasType: null, deps, isUnionAlias: false };
|
|
362
|
+
}
|
|
363
|
+
const deps = new Set();
|
|
364
|
+
const aliasType = this.mapTypeNode(node.type, [name], deps);
|
|
365
|
+
return {
|
|
366
|
+
name,
|
|
367
|
+
kind: "alias",
|
|
368
|
+
fields: [],
|
|
369
|
+
aliasType,
|
|
370
|
+
deps,
|
|
371
|
+
isUnionAlias: node.type.getText().includes("|")
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
mapTypeNode(typeNode, nameHintParts, deps) {
|
|
375
|
+
if (typescript_1.default.isParenthesizedTypeNode(typeNode)) {
|
|
376
|
+
return this.mapTypeNode(typeNode.type, nameHintParts, deps);
|
|
377
|
+
}
|
|
378
|
+
if (typescript_1.default.isUnionTypeNode(typeNode)) {
|
|
379
|
+
return "QString";
|
|
380
|
+
}
|
|
381
|
+
if (typescript_1.default.isTypeLiteralNode(typeNode)) {
|
|
382
|
+
return this.ensureSyntheticStruct(typeNode, nameHintParts, deps);
|
|
383
|
+
}
|
|
384
|
+
if (typescript_1.default.isArrayTypeNode(typeNode)) {
|
|
385
|
+
const itemType = this.mapTypeNode(typeNode.elementType, [...nameHintParts, "Item"], deps);
|
|
386
|
+
return `QList<${itemType}>`;
|
|
387
|
+
}
|
|
388
|
+
if (typescript_1.default.isLiteralTypeNode(typeNode)) {
|
|
389
|
+
if (typescript_1.default.isStringLiteral(typeNode.literal))
|
|
390
|
+
return "QString";
|
|
391
|
+
if (typescript_1.default.isNumericLiteral(typeNode.literal))
|
|
392
|
+
return "double";
|
|
393
|
+
if (typeNode.literal.kind === typescript_1.default.SyntaxKind.TrueKeyword || typeNode.literal.kind === typescript_1.default.SyntaxKind.FalseKeyword)
|
|
394
|
+
return "bool";
|
|
395
|
+
return "QString";
|
|
396
|
+
}
|
|
397
|
+
if (typescript_1.default.isTypeReferenceNode(typeNode)) {
|
|
398
|
+
const name = qNameText(typeNode.typeName);
|
|
399
|
+
const rawText = typeNode.getText();
|
|
400
|
+
if (name.startsWith("AnQst.Type.")) {
|
|
401
|
+
return mapTsTypeToCpp(rawText);
|
|
402
|
+
}
|
|
403
|
+
const args = typeNode.typeArguments ?? [];
|
|
404
|
+
if ((name === "Array" || name === "ReadonlyArray") && args.length === 1) {
|
|
405
|
+
const itemType = this.mapTypeNode(args[0], [...nameHintParts, "Item"], deps);
|
|
406
|
+
return `QList<${itemType}>`;
|
|
407
|
+
}
|
|
408
|
+
if (name === "Record")
|
|
409
|
+
return "QVariantMap";
|
|
410
|
+
if (name === "Partial" && args.length === 1) {
|
|
411
|
+
return this.mapTypeNode(args[0], nameHintParts, deps);
|
|
412
|
+
}
|
|
413
|
+
if (name === "Promise" && args.length === 1) {
|
|
414
|
+
return this.mapTypeNode(args[0], nameHintParts, deps);
|
|
415
|
+
}
|
|
416
|
+
const mapped = mapTsTypeToCpp(rawText);
|
|
417
|
+
this.collectKnownTypeDeps(mapped, deps);
|
|
418
|
+
return mapped;
|
|
419
|
+
}
|
|
420
|
+
const mapped = mapTsTypeToCpp(typeNode.getText());
|
|
421
|
+
this.collectKnownTypeDeps(mapped, deps);
|
|
422
|
+
return mapped;
|
|
423
|
+
}
|
|
424
|
+
ensureSyntheticStruct(typeNode, nameHintParts, deps) {
|
|
425
|
+
const baseName = this.makeSyntheticBaseName(nameHintParts);
|
|
426
|
+
const synthesizedName = this.allocateUniqueName(baseName);
|
|
427
|
+
if (this.declMap.has(synthesizedName)) {
|
|
428
|
+
deps.add(synthesizedName);
|
|
429
|
+
return synthesizedName;
|
|
430
|
+
}
|
|
431
|
+
this.allKnownNames.add(synthesizedName);
|
|
432
|
+
const fields = [];
|
|
433
|
+
const localDeps = new Set();
|
|
434
|
+
for (const member of typeNode.members) {
|
|
435
|
+
if (!typescript_1.default.isPropertySignature(member) || !member.type || !typescript_1.default.isIdentifier(member.name))
|
|
436
|
+
continue;
|
|
437
|
+
const cppType = this.mapTypeNode(member.type, [...nameHintParts, member.name.text], localDeps);
|
|
438
|
+
fields.push({
|
|
439
|
+
name: member.name.text,
|
|
440
|
+
cppType,
|
|
441
|
+
optional: !!member.questionToken
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
this.declMap.set(synthesizedName, {
|
|
445
|
+
name: synthesizedName,
|
|
446
|
+
kind: "struct",
|
|
447
|
+
fields,
|
|
448
|
+
aliasType: null,
|
|
449
|
+
deps: localDeps,
|
|
450
|
+
isUnionAlias: false
|
|
451
|
+
});
|
|
452
|
+
this.seedOrder.push(synthesizedName);
|
|
453
|
+
deps.add(synthesizedName);
|
|
454
|
+
return synthesizedName;
|
|
455
|
+
}
|
|
456
|
+
collectKnownTypeDeps(cppType, deps) {
|
|
457
|
+
for (const match of cppType.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
|
|
458
|
+
const token = match[0];
|
|
459
|
+
if (this.allKnownNames.has(token)) {
|
|
460
|
+
deps.add(token);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
makeSyntheticBaseName(parts) {
|
|
465
|
+
const cleaned = parts
|
|
466
|
+
.map((p) => p.replace(/[^A-Za-z0-9_]/g, "_"))
|
|
467
|
+
.map((p) => p.replace(/_+/g, "_"))
|
|
468
|
+
.map((p) => p.replace(/^_+|_+$/g, ""))
|
|
469
|
+
.filter((p) => p.length > 0);
|
|
470
|
+
return cleaned.join("_") || "AnonymousType";
|
|
471
|
+
}
|
|
472
|
+
allocateUniqueName(baseName) {
|
|
473
|
+
let candidate = baseName;
|
|
474
|
+
let i = 2;
|
|
475
|
+
while (this.usedNames.has(candidate)) {
|
|
476
|
+
candidate = `${baseName}_${i}`;
|
|
477
|
+
i += 1;
|
|
478
|
+
}
|
|
479
|
+
this.usedNames.add(candidate);
|
|
480
|
+
return candidate;
|
|
481
|
+
}
|
|
482
|
+
topologicalOrder() {
|
|
483
|
+
const visiting = new Set();
|
|
484
|
+
const visited = new Set();
|
|
485
|
+
const ordered = [];
|
|
486
|
+
const visit = (name) => {
|
|
487
|
+
if (visited.has(name))
|
|
488
|
+
return;
|
|
489
|
+
if (visiting.has(name))
|
|
490
|
+
return;
|
|
491
|
+
visiting.add(name);
|
|
492
|
+
const decl = this.declMap.get(name);
|
|
493
|
+
if (decl) {
|
|
494
|
+
for (const dep of [...decl.deps].sort()) {
|
|
495
|
+
if (dep === name)
|
|
496
|
+
continue;
|
|
497
|
+
if (this.declMap.has(dep))
|
|
498
|
+
visit(dep);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
visiting.delete(name);
|
|
502
|
+
visited.add(name);
|
|
503
|
+
ordered.push(name);
|
|
504
|
+
};
|
|
505
|
+
for (const name of this.seedOrder) {
|
|
506
|
+
visit(name);
|
|
507
|
+
}
|
|
508
|
+
return ordered;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function renderCppDecl(decl) {
|
|
512
|
+
if (decl.kind === "alias") {
|
|
513
|
+
if (decl.isUnionAlias && decl.aliasType === "QString") {
|
|
514
|
+
return `using ${decl.name} = QString; // union mapped conservatively`;
|
|
515
|
+
}
|
|
516
|
+
return `using ${decl.name} = ${decl.aliasType ?? "QString"};`;
|
|
517
|
+
}
|
|
518
|
+
const lines = [];
|
|
519
|
+
lines.push(`struct ${decl.name} {`);
|
|
520
|
+
for (const field of decl.fields) {
|
|
521
|
+
const cppType = field.optional ? `std::optional<${field.cppType}>` : field.cppType;
|
|
522
|
+
lines.push(` ${cppType} ${field.name};`);
|
|
523
|
+
}
|
|
524
|
+
const comparisons = decl.fields.map((f) => `${f.name} == other.${f.name}`);
|
|
525
|
+
lines.push(` bool operator==(const ${decl.name}& other) const { return ${comparisons.length > 0 ? comparisons.join(" && ") : "true"}; }`);
|
|
526
|
+
lines.push("};");
|
|
527
|
+
return lines.join("\n");
|
|
528
|
+
}
|
|
529
|
+
function buildCppTypeContext(spec) {
|
|
530
|
+
const normalizer = new CppTypeNormalizer(spec);
|
|
531
|
+
for (const decl of collectStructDecls(spec)) {
|
|
532
|
+
normalizer.addSeedDecl(decl);
|
|
533
|
+
}
|
|
534
|
+
for (const service of spec.services) {
|
|
535
|
+
for (const member of service.members) {
|
|
536
|
+
if (member.payloadTypeText) {
|
|
537
|
+
normalizer.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
538
|
+
}
|
|
539
|
+
for (const param of member.parameters) {
|
|
540
|
+
normalizer.mapTypeText(param.typeText, [service.name, member.name, param.name]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return normalizer.buildContext();
|
|
545
|
+
}
|
|
546
|
+
function renderTypesHeader(spec, cppTypes) {
|
|
547
|
+
const decls = cppTypes.orderedDecls.map(renderCppDecl).join("\n\n");
|
|
548
|
+
const metatypes = cppTypes.structNames
|
|
549
|
+
.flatMap((name) => [
|
|
550
|
+
`Q_DECLARE_METATYPE(${spec.widgetName}::${name})`,
|
|
551
|
+
`Q_DECLARE_METATYPE(QList<${spec.widgetName}::${name}>)`
|
|
552
|
+
])
|
|
553
|
+
.join("\n");
|
|
554
|
+
return `#pragma once
|
|
555
|
+
#include <QString>
|
|
556
|
+
#include <QStringList>
|
|
557
|
+
#include <QList>
|
|
558
|
+
#include <QVariantMap>
|
|
559
|
+
#include <QMetaType>
|
|
560
|
+
#include <cstdint>
|
|
561
|
+
#include <optional>
|
|
562
|
+
|
|
563
|
+
namespace ${spec.widgetName} {
|
|
564
|
+
|
|
565
|
+
${decls}
|
|
566
|
+
|
|
567
|
+
} // namespace ${spec.widgetName}
|
|
568
|
+
|
|
569
|
+
${metatypes}
|
|
570
|
+
`;
|
|
571
|
+
}
|
|
572
|
+
function renderWidgetHeader(spec, cppTypes) {
|
|
573
|
+
const callbackAliases = [];
|
|
574
|
+
const publicMethods = [];
|
|
575
|
+
const signals = [];
|
|
576
|
+
const properties = [];
|
|
577
|
+
const fields = [];
|
|
578
|
+
const outputSetters = [];
|
|
579
|
+
const bindings = [];
|
|
580
|
+
for (const service of spec.services) {
|
|
581
|
+
for (const member of service.members) {
|
|
582
|
+
bindings.push({ service: service.name, member: member.name, kind: member.kind });
|
|
583
|
+
const memberPascal = pascalCase(member.name);
|
|
584
|
+
if (member.kind === "Call" && member.payloadTypeText) {
|
|
585
|
+
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
586
|
+
const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
|
|
587
|
+
callbackAliases.push(`using ${memberPascal}Handler = std::function<${cppType}(${args})>;`);
|
|
588
|
+
publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
|
|
589
|
+
fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
|
|
590
|
+
}
|
|
591
|
+
else if (member.kind === "Emitter") {
|
|
592
|
+
const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
|
|
593
|
+
callbackAliases.push(`using ${memberPascal}Handler = std::function<void(${args})>;`);
|
|
594
|
+
publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
|
|
595
|
+
fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
|
|
596
|
+
}
|
|
597
|
+
else if (member.kind === "Slot") {
|
|
598
|
+
const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
|
|
599
|
+
const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
|
|
600
|
+
publicMethods.push(`${ret} ${member.name}(${args}${args ? ", " : ""}bool* ok = nullptr, QString* error = nullptr);`);
|
|
601
|
+
}
|
|
602
|
+
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
603
|
+
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
604
|
+
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
605
|
+
properties.push(`Q_PROPERTY(${cppType} ${member.name} READ ${member.name} WRITE set${cap} NOTIFY ${member.name}Changed)`);
|
|
606
|
+
publicMethods.push(`${cppType} ${member.name}() const;`);
|
|
607
|
+
publicMethods.push(`void set${cap}(const ${cppType}& value);`);
|
|
608
|
+
signals.push(`void ${member.name}Changed(const ${cppType}& value);`);
|
|
609
|
+
fields.push(`${cppType} m_${member.name}{};`);
|
|
610
|
+
if (member.kind === "Input") {
|
|
611
|
+
callbackAliases.push(`using ${memberPascal}Handler = std::function<void(const ${cppType}& value)>;`);
|
|
612
|
+
publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
|
|
613
|
+
fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
outputSetters.push(`void publish${memberPascal}(const ${cppType}& value);`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return `#pragma once
|
|
622
|
+
#include <QHash>
|
|
623
|
+
#include <QVariant>
|
|
624
|
+
#include <QVariantList>
|
|
625
|
+
#include <functional>
|
|
626
|
+
#include "AnQstWebHostBase.h"
|
|
627
|
+
#include "${spec.widgetName}Types.h"
|
|
628
|
+
|
|
629
|
+
namespace ${spec.widgetName} {
|
|
630
|
+
|
|
631
|
+
class ${spec.widgetName} : public AnQstWebHostBase {
|
|
632
|
+
Q_OBJECT
|
|
633
|
+
${properties.map((p) => ` ${p}`).join("\n")}
|
|
634
|
+
|
|
635
|
+
public:
|
|
636
|
+
explicit ${spec.widgetName}(QWidget* parent = nullptr);
|
|
637
|
+
~${spec.widgetName}() override;
|
|
638
|
+
bool enableDebug();
|
|
639
|
+
static constexpr const char* kBootstrapEntryPoint = "index.html";
|
|
640
|
+
static constexpr const char* kBootstrapContentRoot = "qrc:/${spec.widgetName.toLowerCase()}";
|
|
641
|
+
static constexpr const char* kBootstrapBridgeObject = "${spec.widgetName}Bridge";
|
|
642
|
+
|
|
643
|
+
${callbackAliases.map((s) => ` ${s}`).join("\n")}
|
|
644
|
+
${publicMethods.map((s) => ` ${s}`).join("\n")}
|
|
645
|
+
${outputSetters.map((s) => ` ${s}`).join("\n")}
|
|
646
|
+
|
|
647
|
+
signals:
|
|
648
|
+
${signals.map((s) => ` ${s}`).join("\n")}
|
|
649
|
+
void diagnosticsForwarded(const QVariantMap& payload);
|
|
650
|
+
|
|
651
|
+
private:
|
|
652
|
+
struct BridgeBindingRow {
|
|
653
|
+
const char* service;
|
|
654
|
+
const char* member;
|
|
655
|
+
const char* kind;
|
|
656
|
+
};
|
|
657
|
+
static const BridgeBindingRow kBridgeBindings[];
|
|
658
|
+
static constexpr int kBridgeBindingsCount = ${bindings.length};
|
|
659
|
+
static QString makeBindingKey(const QString& service, const QString& member);
|
|
660
|
+
void installBridgeBindings();
|
|
661
|
+
QVariant handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args);
|
|
662
|
+
void handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args);
|
|
663
|
+
void handleGeneratedInput(const QString& service, const QString& member, const QVariant& value);
|
|
664
|
+
|
|
665
|
+
${fields.map((f) => ` ${f}`).join("\n")}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
} // namespace ${spec.widgetName}
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
function renderCppStub(spec, cppTypes) {
|
|
672
|
+
const lines = [];
|
|
673
|
+
lines.push(`#include "include/${spec.widgetName}.h"`);
|
|
674
|
+
lines.push(`#include <QDebug>`);
|
|
675
|
+
lines.push(`#include <QMetaType>`);
|
|
676
|
+
lines.push("");
|
|
677
|
+
lines.push(`extern int qInitResources_${spec.widgetName}();`);
|
|
678
|
+
lines.push("");
|
|
679
|
+
lines.push("namespace {");
|
|
680
|
+
lines.push("void registerGeneratedMetaTypes() {");
|
|
681
|
+
lines.push(" static const bool registered = []() {");
|
|
682
|
+
for (const typeName of cppTypes.structNames) {
|
|
683
|
+
lines.push(` qRegisterMetaType<${spec.widgetName}::${typeName}>("${spec.widgetName}::${typeName}");`);
|
|
684
|
+
lines.push(` qRegisterMetaType<QList<${spec.widgetName}::${typeName}>>("QList<${spec.widgetName}::${typeName}>");`);
|
|
685
|
+
}
|
|
686
|
+
lines.push(" return true;");
|
|
687
|
+
lines.push(" }();");
|
|
688
|
+
lines.push(" Q_UNUSED(registered);");
|
|
689
|
+
lines.push("}");
|
|
690
|
+
lines.push("}");
|
|
691
|
+
lines.push("");
|
|
692
|
+
lines.push(`namespace ${spec.widgetName} {`);
|
|
693
|
+
lines.push("");
|
|
694
|
+
lines.push(`const ${spec.widgetName}::BridgeBindingRow ${spec.widgetName}::kBridgeBindings[] = {`);
|
|
695
|
+
for (const service of spec.services) {
|
|
696
|
+
for (const member of service.members) {
|
|
697
|
+
lines.push(` {"${service.name}", "${member.name}", "${member.kind}"},`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
lines.push(`};`);
|
|
701
|
+
lines.push("");
|
|
702
|
+
lines.push(`${spec.widgetName}::${spec.widgetName}(QWidget* parent) : AnQstWebHostBase(parent) {`);
|
|
703
|
+
lines.push(` static const bool kResourcesInitialized = []() {`);
|
|
704
|
+
lines.push(` ::qInitResources_${spec.widgetName}();`);
|
|
705
|
+
lines.push(` return true;`);
|
|
706
|
+
lines.push(` }();`);
|
|
707
|
+
lines.push(` Q_UNUSED(kResourcesInitialized);`);
|
|
708
|
+
lines.push(` registerGeneratedMetaTypes();`);
|
|
709
|
+
lines.push(` installBridgeBindings();`);
|
|
710
|
+
lines.push(` QObject::connect(this, &AnQstWebHostBase::onHostError, this, &${spec.widgetName}::diagnosticsForwarded);`);
|
|
711
|
+
lines.push(` const bool rootOk = setContentRoot(QString::fromUtf8(kBootstrapContentRoot));`);
|
|
712
|
+
lines.push(` const bool bridgeOk = setBridgeObject(this, QString::fromUtf8(kBootstrapBridgeObject));`);
|
|
713
|
+
lines.push(` const bool loadOk = rootOk && bridgeOk && loadEntryPoint(QString::fromUtf8(kBootstrapEntryPoint));`);
|
|
714
|
+
lines.push(` if (!loadOk) {`);
|
|
715
|
+
lines.push(` qWarning() << "${spec.widgetName} bootstrap failed.";`);
|
|
716
|
+
lines.push(` }`);
|
|
717
|
+
lines.push("}");
|
|
718
|
+
lines.push("");
|
|
719
|
+
lines.push(`${spec.widgetName}::~${spec.widgetName}() = default;`);
|
|
720
|
+
lines.push("");
|
|
721
|
+
lines.push(`bool ${spec.widgetName}::enableDebug() {`);
|
|
722
|
+
lines.push(` return AnQstWebHostBase::enableDebug();`);
|
|
723
|
+
lines.push("}");
|
|
724
|
+
lines.push("");
|
|
725
|
+
lines.push(`QString ${spec.widgetName}::makeBindingKey(const QString& service, const QString& member) {`);
|
|
726
|
+
lines.push(` return service + QStringLiteral("::") + member;`);
|
|
727
|
+
lines.push(`}`);
|
|
728
|
+
lines.push("");
|
|
729
|
+
lines.push(`void ${spec.widgetName}::installBridgeBindings() {`);
|
|
730
|
+
lines.push(` setCallHandler([this](const QString& service, const QString& member, const QVariantList& args) -> QVariant {`);
|
|
731
|
+
lines.push(` return handleGeneratedCall(service, member, args);`);
|
|
732
|
+
lines.push(` });`);
|
|
733
|
+
lines.push(` setEmitterHandler([this](const QString& service, const QString& member, const QVariantList& args) {`);
|
|
734
|
+
lines.push(` handleGeneratedEmitter(service, member, args);`);
|
|
735
|
+
lines.push(` });`);
|
|
736
|
+
lines.push(` setInputHandler([this](const QString& service, const QString& member, const QVariant& value) {`);
|
|
737
|
+
lines.push(` handleGeneratedInput(service, member, value);`);
|
|
738
|
+
lines.push(` });`);
|
|
739
|
+
lines.push(`}`);
|
|
740
|
+
lines.push("");
|
|
741
|
+
lines.push(`QVariant ${spec.widgetName}::handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args) {`);
|
|
742
|
+
for (const service of spec.services) {
|
|
743
|
+
for (const member of service.members) {
|
|
744
|
+
if (member.kind !== "Call" || !member.payloadTypeText)
|
|
745
|
+
continue;
|
|
746
|
+
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
747
|
+
const pascal = pascalCase(member.name);
|
|
748
|
+
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
749
|
+
lines.push(` if (!m_${member.name}Handler) return QVariant();`);
|
|
750
|
+
for (let i = 0; i < member.parameters.length; i++) {
|
|
751
|
+
const p = member.parameters[i];
|
|
752
|
+
const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
|
|
753
|
+
lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
|
|
754
|
+
}
|
|
755
|
+
const argNames = member.parameters.map((p) => p.name).join(", ");
|
|
756
|
+
lines.push(` const ${cppType} result = m_${member.name}Handler(${argNames});`);
|
|
757
|
+
lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
|
|
758
|
+
lines.push(` }`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
lines.push(` return QVariant();`);
|
|
762
|
+
lines.push(`}`);
|
|
763
|
+
lines.push("");
|
|
764
|
+
lines.push(`void ${spec.widgetName}::handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args) {`);
|
|
765
|
+
for (const service of spec.services) {
|
|
766
|
+
for (const member of service.members) {
|
|
767
|
+
if (member.kind !== "Emitter")
|
|
768
|
+
continue;
|
|
769
|
+
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
770
|
+
lines.push(` if (!m_${member.name}Handler) return;`);
|
|
771
|
+
for (let i = 0; i < member.parameters.length; i++) {
|
|
772
|
+
const p = member.parameters[i];
|
|
773
|
+
const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
|
|
774
|
+
lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
|
|
775
|
+
}
|
|
776
|
+
const argNames = member.parameters.map((p) => p.name).join(", ");
|
|
777
|
+
lines.push(` m_${member.name}Handler(${argNames});`);
|
|
778
|
+
lines.push(` return;`);
|
|
779
|
+
lines.push(` }`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
lines.push(`}`);
|
|
783
|
+
lines.push("");
|
|
784
|
+
lines.push(`void ${spec.widgetName}::handleGeneratedInput(const QString& service, const QString& member, const QVariant& value) {`);
|
|
785
|
+
for (const service of spec.services) {
|
|
786
|
+
for (const member of service.members) {
|
|
787
|
+
if (member.kind !== "Input" || !member.payloadTypeText)
|
|
788
|
+
continue;
|
|
789
|
+
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
790
|
+
lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
|
|
791
|
+
lines.push(` const ${cppType} typedValue = ${variantToCppExpression(cppType, "value")};`);
|
|
792
|
+
lines.push(` set${pascalCase(member.name)}(typedValue);`);
|
|
793
|
+
lines.push(` if (m_${member.name}Handler) m_${member.name}Handler(typedValue);`);
|
|
794
|
+
lines.push(` return;`);
|
|
795
|
+
lines.push(` }`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
lines.push(`}`);
|
|
799
|
+
lines.push("");
|
|
800
|
+
for (const service of spec.services) {
|
|
801
|
+
for (const member of service.members) {
|
|
802
|
+
const memberPascal = pascalCase(member.name);
|
|
803
|
+
if ((member.kind === "Call" || member.kind === "Input") && member.payloadTypeText) {
|
|
804
|
+
lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
|
|
805
|
+
lines.push(` m_${member.name}Handler = handler;`);
|
|
806
|
+
lines.push("}");
|
|
807
|
+
lines.push("");
|
|
808
|
+
}
|
|
809
|
+
else if (member.kind === "Emitter") {
|
|
810
|
+
lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
|
|
811
|
+
lines.push(` m_${member.name}Handler = handler;`);
|
|
812
|
+
lines.push("}");
|
|
813
|
+
lines.push("");
|
|
814
|
+
}
|
|
815
|
+
else if (member.kind === "Input" && member.payloadTypeText) {
|
|
816
|
+
lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
|
|
817
|
+
lines.push(` m_${member.name}Handler = handler;`);
|
|
818
|
+
lines.push("}");
|
|
819
|
+
lines.push("");
|
|
820
|
+
}
|
|
821
|
+
if (member.kind === "Slot") {
|
|
822
|
+
const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
|
|
823
|
+
const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
|
|
824
|
+
const argsWithMeta = `${args}${args ? ", " : ""}bool* ok, QString* error`;
|
|
825
|
+
lines.push(`${ret} ${spec.widgetName}::${member.name}(${argsWithMeta}) {`);
|
|
826
|
+
lines.push(` QVariantList invokeArgs;`);
|
|
827
|
+
for (const p of member.parameters) {
|
|
828
|
+
const pType = mapTsTypeToCpp(p.typeText);
|
|
829
|
+
lines.push(` invokeArgs.push_back(${cppToVariantExpression(pType, p.name)});`);
|
|
830
|
+
}
|
|
831
|
+
lines.push(` QVariant result;`);
|
|
832
|
+
lines.push(` QString invokeError;`);
|
|
833
|
+
lines.push(` const bool success = invokeSlot(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), invokeArgs, &result, &invokeError);`);
|
|
834
|
+
lines.push(` if (ok != nullptr) *ok = success;`);
|
|
835
|
+
lines.push(` if (error != nullptr) *error = invokeError;`);
|
|
836
|
+
if (ret === "void") {
|
|
837
|
+
lines.push(` if (!success) return;`);
|
|
838
|
+
lines.push(` return;`);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
lines.push(` if (!success) return ${ret}{};`);
|
|
842
|
+
lines.push(` return ${variantToCppExpression(ret, "result")};`);
|
|
843
|
+
}
|
|
844
|
+
lines.push("}");
|
|
845
|
+
lines.push("");
|
|
846
|
+
}
|
|
847
|
+
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
848
|
+
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
849
|
+
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
850
|
+
lines.push(`${cppType} ${spec.widgetName}::${member.name}() const {`);
|
|
851
|
+
lines.push(` return m_${member.name};`);
|
|
852
|
+
lines.push("}");
|
|
853
|
+
lines.push("");
|
|
854
|
+
lines.push(`void ${spec.widgetName}::set${cap}(const ${cppType}& value) {`);
|
|
855
|
+
lines.push(` if (m_${member.name} == value) return;`);
|
|
856
|
+
lines.push(` m_${member.name} = value;`);
|
|
857
|
+
if (member.kind === "Output") {
|
|
858
|
+
lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), ${cppToVariantExpression(cppType, "value")});`);
|
|
859
|
+
}
|
|
860
|
+
lines.push(` emit ${member.name}Changed(value);`);
|
|
861
|
+
lines.push("}");
|
|
862
|
+
lines.push("");
|
|
863
|
+
if (member.kind === "Output") {
|
|
864
|
+
lines.push(`void ${spec.widgetName}::publish${pascalCase(member.name)}(const ${cppType}& value) {`);
|
|
865
|
+
lines.push(` set${cap}(value);`);
|
|
866
|
+
lines.push(`}`);
|
|
867
|
+
lines.push("");
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
lines.push(`} // namespace ${spec.widgetName}`);
|
|
873
|
+
return lines.join("\n");
|
|
874
|
+
}
|
|
875
|
+
function renderCMake(spec) {
|
|
876
|
+
return `cmake_minimum_required(VERSION 3.21)
|
|
877
|
+
project(${spec.widgetName}Library LANGUAGES CXX)
|
|
878
|
+
|
|
879
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
880
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
881
|
+
set(CMAKE_AUTOMOC ON)
|
|
882
|
+
set(CMAKE_AUTORCC ON)
|
|
883
|
+
|
|
884
|
+
if(NOT TARGET anqstwebhostbase)
|
|
885
|
+
message(FATAL_ERROR "Target 'anqstwebhostbase' is required before adding generated widget library ${spec.widgetName}Widget.")
|
|
886
|
+
endif()
|
|
887
|
+
|
|
888
|
+
add_library(${spec.widgetName}Widget
|
|
889
|
+
${spec.widgetName}.cpp
|
|
890
|
+
${spec.widgetName}.qrc
|
|
891
|
+
include/${spec.widgetName}.h
|
|
892
|
+
include/${spec.widgetName}Types.h
|
|
893
|
+
)
|
|
894
|
+
target_include_directories(${spec.widgetName}Widget
|
|
895
|
+
PUBLIC
|
|
896
|
+
\${CMAKE_CURRENT_SOURCE_DIR}/include
|
|
897
|
+
)
|
|
898
|
+
target_link_libraries(${spec.widgetName}Widget
|
|
899
|
+
PUBLIC
|
|
900
|
+
anqstwebhostbase
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
# Uses transitive Qt and include requirements from anqstwebhostbase.
|
|
904
|
+
`;
|
|
905
|
+
}
|
|
906
|
+
function escapeXml(text) {
|
|
907
|
+
return text
|
|
908
|
+
.replace(/&/g, "&")
|
|
909
|
+
.replace(/"/g, """)
|
|
910
|
+
.replace(/</g, "<")
|
|
911
|
+
.replace(/>/g, ">");
|
|
912
|
+
}
|
|
913
|
+
function normalizeSlashes(value) {
|
|
914
|
+
return value.split(node_path_1.default.sep).join("/");
|
|
915
|
+
}
|
|
916
|
+
function renderEmbeddedQrc(widgetName, embeddedWebFiles) {
|
|
917
|
+
const files = [...embeddedWebFiles].sort();
|
|
918
|
+
const lines = [];
|
|
919
|
+
lines.push("<RCC>");
|
|
920
|
+
lines.push(` <qresource prefix="/${widgetName.toLowerCase()}">`);
|
|
921
|
+
if (files.length === 0) {
|
|
922
|
+
lines.push(" <!-- anqst build will populate embedded web assets under webapp/ -->");
|
|
923
|
+
}
|
|
924
|
+
for (const relPath of files) {
|
|
925
|
+
lines.push(` <file alias="${escapeXml(relPath)}">webapp/${escapeXml(relPath)}</file>`);
|
|
926
|
+
}
|
|
927
|
+
lines.push(" </qresource>");
|
|
928
|
+
lines.push("</RCC>");
|
|
929
|
+
return `${lines.join("\n")}\n`;
|
|
930
|
+
}
|
|
931
|
+
function renderNpmPackage(spec) {
|
|
932
|
+
return JSON.stringify({
|
|
933
|
+
name: `${spec.widgetName.toLowerCase()}-generated`,
|
|
934
|
+
version: "0.1.0",
|
|
935
|
+
private: true,
|
|
936
|
+
types: "types/index.d.ts",
|
|
937
|
+
main: "services.js",
|
|
938
|
+
exports: {
|
|
939
|
+
".": {
|
|
940
|
+
types: "./types/index.d.ts",
|
|
941
|
+
default: "./index.js"
|
|
942
|
+
},
|
|
943
|
+
"./services": {
|
|
944
|
+
types: "./types/services.d.ts",
|
|
945
|
+
default: "./services.js"
|
|
946
|
+
},
|
|
947
|
+
"./types": {
|
|
948
|
+
types: "./types/types.d.ts",
|
|
949
|
+
default: "./types.js"
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
anqst: {
|
|
953
|
+
widget: spec.widgetName,
|
|
954
|
+
services: spec.services.map((s) => s.name),
|
|
955
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport
|
|
956
|
+
}
|
|
957
|
+
}, null, 2);
|
|
958
|
+
}
|
|
959
|
+
function renderTypeDeclarations(spec, exported = false) {
|
|
960
|
+
const decls = collectReachableNamespaceDecls(spec)
|
|
961
|
+
.map((d) => {
|
|
962
|
+
const normalized = stripAnQstType(d.nodeText);
|
|
963
|
+
if (!exported)
|
|
964
|
+
return normalized;
|
|
965
|
+
return normalized.replace(/^(\s*)(interface|type)\b/m, "$1export $2");
|
|
966
|
+
})
|
|
967
|
+
.join("\n\n");
|
|
968
|
+
if (decls.trim().length === 0)
|
|
969
|
+
return "";
|
|
970
|
+
return `${decls}\n`;
|
|
971
|
+
}
|
|
972
|
+
function renderLocalTypeImports(spec) {
|
|
973
|
+
const localTypeNames = collectReachableNamespaceDecls(spec).map((decl) => decl.name);
|
|
974
|
+
if (localTypeNames.length === 0)
|
|
975
|
+
return "";
|
|
976
|
+
return `import type { ${localTypeNames.join(", ")} } from "./types";`;
|
|
977
|
+
}
|
|
978
|
+
function renderTsService(spec, serviceName) {
|
|
979
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
980
|
+
const fieldLines = [];
|
|
981
|
+
const methodLines = [];
|
|
982
|
+
const setMembers = [];
|
|
983
|
+
const onSlotMembers = [];
|
|
984
|
+
const constructorBodyLines = [];
|
|
985
|
+
constructorBodyLines.push(" this._bridge.ready().catch((error) => console.error('AnQst bridge ready() failed', error, (error as { stack?: unknown })?.stack));");
|
|
986
|
+
for (const m of members) {
|
|
987
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
988
|
+
const valueArgs = m.parameters.map((p) => p.name).join(", ");
|
|
989
|
+
const valueArray = valueArgs.length > 0 ? `[${valueArgs}]` : "[]";
|
|
990
|
+
if (m.kind === "Call") {
|
|
991
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
992
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${valueArray}); }`);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (m.kind === "Emitter") {
|
|
996
|
+
methodLines.push(` ${m.name}(${args}): void { this._bridge.emit("${serviceName}", "${m.name}", ${valueArray}); }`);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (m.kind === "Slot") {
|
|
1000
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
1001
|
+
onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${ret}): void => {`);
|
|
1002
|
+
onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", handler as (...args: unknown[]) => unknown);`);
|
|
1003
|
+
onSlotMembers.push(" },");
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
1007
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
1008
|
+
fieldLines.push(` private readonly _${m.name} = signal<${tsType}>((undefined as unknown) as ${tsType});`);
|
|
1009
|
+
methodLines.push(` ${m.name}(): ${tsType} { return this._${m.name}(); }`);
|
|
1010
|
+
if (m.kind === "Input") {
|
|
1011
|
+
setMembers.push(` ${m.name}: (value: ${tsType}): void => {`);
|
|
1012
|
+
setMembers.push(` this._${m.name}.set(value);`);
|
|
1013
|
+
setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", value);`);
|
|
1014
|
+
setMembers.push(" },");
|
|
1015
|
+
}
|
|
1016
|
+
if (m.kind === "Output") {
|
|
1017
|
+
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => this._${m.name}.set(value as ${tsType}));`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const constructorLines = [
|
|
1022
|
+
" constructor() {",
|
|
1023
|
+
...constructorBodyLines,
|
|
1024
|
+
" }",
|
|
1025
|
+
];
|
|
1026
|
+
return `@Injectable({ providedIn: "root" })
|
|
1027
|
+
export class ${serviceName} {
|
|
1028
|
+
private readonly _bridge = inject(AnQstBridgeRuntime);
|
|
1029
|
+
${fieldLines.join("\n")}
|
|
1030
|
+
${constructorLines.join("\n")}
|
|
1031
|
+
readonly set = {
|
|
1032
|
+
${setMembers.join("\n")}
|
|
1033
|
+
};
|
|
1034
|
+
readonly onSlot = {
|
|
1035
|
+
${onSlotMembers.join("\n")}
|
|
1036
|
+
};
|
|
1037
|
+
${methodLines.join("\n")}
|
|
1038
|
+
}
|
|
1039
|
+
`;
|
|
1040
|
+
}
|
|
1041
|
+
function renderTsServiceDts(spec, serviceName) {
|
|
1042
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
1043
|
+
const setMembers = [];
|
|
1044
|
+
const onSlotMembers = [];
|
|
1045
|
+
const classMembers = [];
|
|
1046
|
+
const setInterfaceName = `${serviceName}Set`;
|
|
1047
|
+
const onSlotInterfaceName = `${serviceName}OnSlot`;
|
|
1048
|
+
for (const m of members) {
|
|
1049
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
1050
|
+
if (m.kind === "Call") {
|
|
1051
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
1052
|
+
classMembers.push(` ${m.name}(${args}): Promise<${ret}>;`);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (m.kind === "Emitter") {
|
|
1056
|
+
classMembers.push(` ${m.name}(${args}): void;`);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (m.kind === "Slot") {
|
|
1060
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
1061
|
+
onSlotMembers.push(` ${m.name}(handler: (${args}) => ${ret}): void;`);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
1065
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
1066
|
+
classMembers.push(` ${m.name}(): ${tsType};`);
|
|
1067
|
+
if (m.kind === "Input") {
|
|
1068
|
+
setMembers.push(` ${m.name}(value: ${tsType}): void;`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const setInterfaceDecl = setMembers.length > 0
|
|
1073
|
+
? `export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`
|
|
1074
|
+
: `export interface ${setInterfaceName} {}`;
|
|
1075
|
+
const onSlotInterfaceDecl = onSlotMembers.length > 0
|
|
1076
|
+
? `export interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`
|
|
1077
|
+
: `export interface ${onSlotInterfaceName} {}`;
|
|
1078
|
+
const classMemberBlock = classMembers.length > 0 ? `\n${classMembers.join("\n")}` : "";
|
|
1079
|
+
return `${setInterfaceDecl}
|
|
1080
|
+
|
|
1081
|
+
${onSlotInterfaceDecl}
|
|
1082
|
+
|
|
1083
|
+
export declare class ${serviceName} {
|
|
1084
|
+
readonly set: ${setInterfaceName};
|
|
1085
|
+
readonly onSlot: ${onSlotInterfaceName};${classMemberBlock}
|
|
1086
|
+
}`;
|
|
1087
|
+
}
|
|
1088
|
+
function renderTsServices(spec) {
|
|
1089
|
+
const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name)).join("\n");
|
|
1090
|
+
const externalTypeImports = renderRequiredTypeImports(spec, "npmpackage/services.ts").trim();
|
|
1091
|
+
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
1092
|
+
const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
|
|
1093
|
+
const typeImportsBlock = typeImports.length > 0 ? `${typeImports}\n\n` : "";
|
|
1094
|
+
return `import { Injectable, inject, signal } from "@angular/core";
|
|
1095
|
+
${typeImportsBlock}
|
|
1096
|
+
|
|
1097
|
+
type SlotHandler = (...args: unknown[]) => unknown;
|
|
1098
|
+
type OutputHandler = (value: unknown) => void;
|
|
1099
|
+
type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
|
|
1100
|
+
type OutputListener = (service: string, member: string, value: unknown) => void;
|
|
1101
|
+
|
|
1102
|
+
interface HostBridgeApi {
|
|
1103
|
+
anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
|
|
1104
|
+
anQstBridge_emit(service: string, member: string, args: unknown[]): void;
|
|
1105
|
+
anQstBridge_setInput(service: string, member: string, value: unknown): void;
|
|
1106
|
+
anQstBridge_registerSlot(service: string, member: string): void;
|
|
1107
|
+
anQstBridge_resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
1108
|
+
anQstBridge_outputUpdated: { connect: (cb: (service: string, member: string, value: unknown) => void) => void };
|
|
1109
|
+
anQstBridge_slotInvocationRequested: {
|
|
1110
|
+
connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
interface QWebChannelCtor {
|
|
1115
|
+
new (
|
|
1116
|
+
transport: unknown,
|
|
1117
|
+
initCallback: (channel: { objects: Record<string, HostBridgeApi | undefined> }) => void
|
|
1118
|
+
): unknown;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
interface BridgeAdapter {
|
|
1122
|
+
call<T>(service: string, member: string, args: unknown[]): Promise<T>;
|
|
1123
|
+
emit(service: string, member: string, args: unknown[]): void;
|
|
1124
|
+
setInput(service: string, member: string, value: unknown): void;
|
|
1125
|
+
registerSlot(service: string, member: string): void;
|
|
1126
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
1127
|
+
onOutput(handler: OutputListener): void;
|
|
1128
|
+
onSlotInvocation(handler: SlotInvocationListener): void;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
class QtWebChannelAdapter implements BridgeAdapter {
|
|
1132
|
+
private constructor(private readonly host: HostBridgeApi) {}
|
|
1133
|
+
|
|
1134
|
+
static async create(): Promise<QtWebChannelAdapter> {
|
|
1135
|
+
const anyWindow = window as unknown as {
|
|
1136
|
+
qt?: { webChannelTransport?: unknown };
|
|
1137
|
+
QWebChannel?: QWebChannelCtor;
|
|
1138
|
+
};
|
|
1139
|
+
if (typeof anyWindow.QWebChannel !== "function" || anyWindow.qt?.webChannelTransport === undefined) {
|
|
1140
|
+
throw new Error("Qt WebChannel transport is unavailable.");
|
|
1141
|
+
}
|
|
1142
|
+
return await new Promise<QtWebChannelAdapter>((resolve, reject) => {
|
|
1143
|
+
try {
|
|
1144
|
+
const QWebChannel = anyWindow.QWebChannel as QWebChannelCtor;
|
|
1145
|
+
new QWebChannel(anyWindow.qt!.webChannelTransport, (channel) => {
|
|
1146
|
+
try {
|
|
1147
|
+
const host = channel.objects["${spec.widgetName}Bridge"];
|
|
1148
|
+
if (host === undefined) {
|
|
1149
|
+
reject(new Error("${spec.widgetName}Bridge bridge object is unavailable."));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
resolve(new QtWebChannelAdapter(host));
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
1164
|
+
return new Promise<T>((resolve) => {
|
|
1165
|
+
this.host.anQstBridge_call(service, member, args, (result) => resolve(result as T));
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
1170
|
+
this.host.anQstBridge_emit(service, member, args);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
1174
|
+
this.host.anQstBridge_setInput(service, member, value);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
registerSlot(service: string, member: string): void {
|
|
1178
|
+
this.host.anQstBridge_registerSlot(service, member);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
1182
|
+
this.host.anQstBridge_resolveSlot(requestId, ok, payload, error);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
onOutput(handler: OutputListener): void {
|
|
1186
|
+
this.host.anQstBridge_outputUpdated.connect(handler);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
1190
|
+
this.host.anQstBridge_slotInvocationRequested.connect(handler);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
1195
|
+
private readonly pending = new Map<string, (result: unknown) => void>();
|
|
1196
|
+
private readonly outputListeners: OutputListener[] = [];
|
|
1197
|
+
private readonly slotListeners: SlotInvocationListener[] = [];
|
|
1198
|
+
private requestCounter = 0;
|
|
1199
|
+
|
|
1200
|
+
private constructor(private readonly socket: WebSocket) {
|
|
1201
|
+
this.socket.addEventListener("message", (event) => {
|
|
1202
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
1203
|
+
const message = JSON.parse(raw) as Record<string, unknown>;
|
|
1204
|
+
const type = String(message["type"] ?? "");
|
|
1205
|
+
if (type === "callResult") {
|
|
1206
|
+
const requestId = String(message["requestId"] ?? "");
|
|
1207
|
+
const resolver = this.pending.get(requestId);
|
|
1208
|
+
if (resolver) {
|
|
1209
|
+
this.pending.delete(requestId);
|
|
1210
|
+
resolver(message["result"]);
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (type === "outputUpdated") {
|
|
1215
|
+
const service = String(message["service"] ?? "");
|
|
1216
|
+
const member = String(message["member"] ?? "");
|
|
1217
|
+
for (const listener of this.outputListeners) {
|
|
1218
|
+
listener(service, member, message["value"]);
|
|
1219
|
+
}
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (type === "slotInvocationRequested") {
|
|
1223
|
+
const requestId = String(message["requestId"] ?? "");
|
|
1224
|
+
const service = String(message["service"] ?? "");
|
|
1225
|
+
const member = String(message["member"] ?? "");
|
|
1226
|
+
const args = Array.isArray(message["args"]) ? (message["args"] as unknown[]) : [];
|
|
1227
|
+
for (const listener of this.slotListeners) {
|
|
1228
|
+
listener(requestId, service, member, args);
|
|
1229
|
+
}
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (type === "hostError") {
|
|
1233
|
+
console.error("AnQst host error:", message["payload"]);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
static async create(): Promise<WebSocketBridgeAdapter> {
|
|
1239
|
+
const configResponse = await fetch("/anqst-dev-config.json", { cache: "no-store" });
|
|
1240
|
+
if (!configResponse.ok) {
|
|
1241
|
+
throw new Error("AnQst host bootstrap missing: unable to read /anqst-dev-config.json");
|
|
1242
|
+
}
|
|
1243
|
+
const config = (await configResponse.json()) as { wsUrl?: string };
|
|
1244
|
+
if (!config.wsUrl) {
|
|
1245
|
+
throw new Error("AnQst host bootstrap missing: wsUrl is unavailable.");
|
|
1246
|
+
}
|
|
1247
|
+
return await new Promise<WebSocketBridgeAdapter>((resolve, reject) => {
|
|
1248
|
+
const socket = new WebSocket(config.wsUrl!);
|
|
1249
|
+
socket.addEventListener("open", () => resolve(new WebSocketBridgeAdapter(socket)));
|
|
1250
|
+
socket.addEventListener("error", () => reject(new Error("Failed to connect to AnQst WebSocket bridge.")));
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
1255
|
+
const requestId = \`req-\${++this.requestCounter}\`;
|
|
1256
|
+
const payload = { type: "call", requestId, service, member, args };
|
|
1257
|
+
return await new Promise<T>((resolve) => {
|
|
1258
|
+
this.pending.set(requestId, (value) => resolve(value as T));
|
|
1259
|
+
this.socket.send(JSON.stringify(payload));
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
1264
|
+
this.socket.send(JSON.stringify({ type: "emit", service, member, args }));
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
1268
|
+
this.socket.send(JSON.stringify({ type: "setInput", service, member, value }));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
registerSlot(service: string, member: string): void {
|
|
1272
|
+
this.socket.send(JSON.stringify({ type: "registerSlot", service, member }));
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
1276
|
+
this.socket.send(JSON.stringify({ type: "resolveSlot", requestId, ok, payload, error }));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
onOutput(handler: OutputListener): void {
|
|
1280
|
+
this.outputListeners.push(handler);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
1284
|
+
this.slotListeners.push(handler);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
@Injectable({ providedIn: "root" })
|
|
1289
|
+
class AnQstBridgeRuntime {
|
|
1290
|
+
private adapter: BridgeAdapter | null = null;
|
|
1291
|
+
private readonly slotHandlers = new Map<string, SlotHandler>();
|
|
1292
|
+
private readonly outputHandlers = new Map<string, OutputHandler[]>();
|
|
1293
|
+
private readonly startup = this.init();
|
|
1294
|
+
|
|
1295
|
+
async ready(): Promise<void> {
|
|
1296
|
+
return this.startup;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
1300
|
+
const adapter = await this.requireAdapter();
|
|
1301
|
+
return adapter.call<T>(service, member, args);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
1305
|
+
if (this.adapter !== null) {
|
|
1306
|
+
this.adapter.emit(service, member, args);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
this.ready()
|
|
1310
|
+
.then(() => this.requireAdapterSync().emit(service, member, args))
|
|
1311
|
+
.catch((error) => console.error(error));
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
1315
|
+
if (this.adapter !== null) {
|
|
1316
|
+
this.adapter.setInput(service, member, value);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
this.ready()
|
|
1320
|
+
.then(() => this.requireAdapterSync().setInput(service, member, value))
|
|
1321
|
+
.catch((error) => console.error(error));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
registerSlot(service: string, member: string, handler: SlotHandler): void {
|
|
1325
|
+
const key = this.key(service, member);
|
|
1326
|
+
this.slotHandlers.set(key, handler);
|
|
1327
|
+
if (this.adapter !== null) {
|
|
1328
|
+
this.adapter.registerSlot(service, member);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
this.ready()
|
|
1332
|
+
.then(() => this.requireAdapterSync().registerSlot(service, member))
|
|
1333
|
+
.catch((error) => console.error(error));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
onOutput(service: string, member: string, handler: OutputHandler): void {
|
|
1337
|
+
const key = this.key(service, member);
|
|
1338
|
+
const existing = this.outputHandlers.get(key) ?? [];
|
|
1339
|
+
existing.push(handler);
|
|
1340
|
+
this.outputHandlers.set(key, existing);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private requireAdapterSync(): BridgeAdapter {
|
|
1344
|
+
if (this.adapter === null) {
|
|
1345
|
+
throw new Error("AnQst bridge is not ready.");
|
|
1346
|
+
}
|
|
1347
|
+
return this.adapter;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
private async requireAdapter(): Promise<BridgeAdapter> {
|
|
1351
|
+
await this.startup;
|
|
1352
|
+
return this.requireAdapterSync();
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private async init(): Promise<void> {
|
|
1356
|
+
const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
|
|
1357
|
+
if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
|
|
1358
|
+
this.adapter = await QtWebChannelAdapter.create();
|
|
1359
|
+
} else {
|
|
1360
|
+
this.adapter = await WebSocketBridgeAdapter.create();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
this.adapter.onOutput((service, member, value) => {
|
|
1364
|
+
const key = this.key(service, member);
|
|
1365
|
+
for (const outputHandler of this.outputHandlers.get(key) ?? []) {
|
|
1366
|
+
outputHandler(value);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
this.adapter.onSlotInvocation((requestId, service, member, args) => {
|
|
1370
|
+
const key = this.key(service, member);
|
|
1371
|
+
const handler = this.slotHandlers.get(key);
|
|
1372
|
+
if (handler === undefined) {
|
|
1373
|
+
this.adapter!.resolveSlot(requestId, false, undefined, "No slot handler registered.");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
const result = handler(...args);
|
|
1378
|
+
this.adapter!.resolveSlot(requestId, true, result, "");
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
this.adapter!.resolveSlot(requestId, false, undefined, String(error));
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
for (const key of this.slotHandlers.keys()) {
|
|
1384
|
+
const parts = key.split("::");
|
|
1385
|
+
if (parts.length === 2) {
|
|
1386
|
+
this.adapter.registerSlot(parts[0], parts[1]);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private key(service: string, member: string): string {
|
|
1392
|
+
return \`\${service}::\${member}\`;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
}
|
|
1396
|
+
${serviceClasses}
|
|
1397
|
+
`;
|
|
1398
|
+
}
|
|
1399
|
+
function renderTsTypes(spec) {
|
|
1400
|
+
const typeImports = renderRequiredTypeImports(spec, "npmpackage/types.ts").trim();
|
|
1401
|
+
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
1402
|
+
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
1403
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
1404
|
+
}
|
|
1405
|
+
function renderTypeServicesDts(spec) {
|
|
1406
|
+
const externalTypeImports = renderRequiredTypeImports(spec, "npmpackage/types/services.d.ts").trim();
|
|
1407
|
+
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
1408
|
+
const serviceDecls = spec.services
|
|
1409
|
+
.map((s) => renderTsServiceDts(spec, s.name))
|
|
1410
|
+
.join("\n\n");
|
|
1411
|
+
const sections = [externalTypeImports, localTypeImports, serviceDecls.trim()].filter((s) => s.length > 0);
|
|
1412
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
1413
|
+
}
|
|
1414
|
+
function renderTypeTypesDts(spec) {
|
|
1415
|
+
const typeImports = renderRequiredTypeImports(spec, "npmpackage/types/types.d.ts").trim();
|
|
1416
|
+
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
1417
|
+
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
1418
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
1419
|
+
}
|
|
1420
|
+
function renderTsIndex() {
|
|
1421
|
+
return `export type Services = typeof import("./services");
|
|
1422
|
+
export type Types = typeof import("./types");
|
|
1423
|
+
`;
|
|
1424
|
+
}
|
|
1425
|
+
function renderTypeIndexDts() {
|
|
1426
|
+
return `export type Services = typeof import("../services");
|
|
1427
|
+
export type Types = typeof import("../types");
|
|
1428
|
+
`;
|
|
1429
|
+
}
|
|
1430
|
+
function renderJsModule() {
|
|
1431
|
+
return `"use strict";
|
|
1432
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1433
|
+
`;
|
|
1434
|
+
}
|
|
1435
|
+
function renderJsIndex() {
|
|
1436
|
+
return renderJsModule();
|
|
1437
|
+
}
|
|
1438
|
+
function renderJsServices() {
|
|
1439
|
+
return renderJsModule();
|
|
1440
|
+
}
|
|
1441
|
+
function renderJsTypes() {
|
|
1442
|
+
return renderJsModule();
|
|
1443
|
+
}
|
|
1444
|
+
function renderNodeExpressWsPackage(spec) {
|
|
1445
|
+
return JSON.stringify({
|
|
1446
|
+
name: `${spec.widgetName.toLowerCase()}-node-express-ws-generated`,
|
|
1447
|
+
version: "0.1.0",
|
|
1448
|
+
private: true,
|
|
1449
|
+
types: "types/index.d.ts",
|
|
1450
|
+
main: "index.ts",
|
|
1451
|
+
exports: {
|
|
1452
|
+
".": {
|
|
1453
|
+
types: "./types/index.d.ts",
|
|
1454
|
+
default: "./index.ts"
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
anqst: {
|
|
1458
|
+
widget: spec.widgetName,
|
|
1459
|
+
services: spec.services.map((s) => s.name),
|
|
1460
|
+
target: "node_express_ws"
|
|
1461
|
+
}
|
|
1462
|
+
}, null, 2);
|
|
1463
|
+
}
|
|
1464
|
+
function nodeParamTuple(member) {
|
|
1465
|
+
if (member.parameters.length === 0)
|
|
1466
|
+
return "[]";
|
|
1467
|
+
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
1468
|
+
}
|
|
1469
|
+
function nodeParamArgs(member) {
|
|
1470
|
+
return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
1471
|
+
}
|
|
1472
|
+
function nodeParamValues(member) {
|
|
1473
|
+
if (member.parameters.length === 0)
|
|
1474
|
+
return "[]";
|
|
1475
|
+
return `[${member.parameters.map((p) => p.name).join(", ")}]`;
|
|
1476
|
+
}
|
|
1477
|
+
function nodeCap(value) {
|
|
1478
|
+
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
1479
|
+
}
|
|
1480
|
+
function renderNodeExpressWsTypes(spec) {
|
|
1481
|
+
const typeImports = renderRequiredTypeImports(spec, `${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
|
|
1482
|
+
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
1483
|
+
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
1484
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
1485
|
+
}
|
|
1486
|
+
function renderNodeExpressWsIndex(spec) {
|
|
1487
|
+
const typeImports = renderRequiredTypeImports(spec, `${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
1488
|
+
const typeDecls = renderTypeDeclarations(spec, true);
|
|
1489
|
+
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
1490
|
+
const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
|
|
1491
|
+
const handlerInterfaces = spec.services
|
|
1492
|
+
.map((service) => {
|
|
1493
|
+
const lines = [];
|
|
1494
|
+
for (const member of service.members) {
|
|
1495
|
+
const args = nodeParamArgs(member);
|
|
1496
|
+
const prefixedArgs = args.length > 0 ? `, ${args}` : "";
|
|
1497
|
+
if (member.kind === "Call" && member.payloadTypeText) {
|
|
1498
|
+
const ret = mapTypeTextToTs(member.payloadTypeText);
|
|
1499
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
|
|
1500
|
+
}
|
|
1501
|
+
else if (member.kind === "Emitter") {
|
|
1502
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
|
|
1503
|
+
}
|
|
1504
|
+
else if (member.kind === "Input" && member.payloadTypeText) {
|
|
1505
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}, value: ${mapTypeTextToTs(member.payloadTypeText)}): void | Promise<void>;`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return `export interface ${service.name}NodeHandlers {\n${lines.join("\n")}\n}`;
|
|
1509
|
+
})
|
|
1510
|
+
.join("\n\n");
|
|
1511
|
+
const implementationFields = spec.services.map((service) => ` ${service.name}: ${service.name}NodeHandlers;`).join("\n");
|
|
1512
|
+
const slotHelpers = spec.services
|
|
1513
|
+
.flatMap((service) => service.members
|
|
1514
|
+
.filter((member) => member.kind === "Slot")
|
|
1515
|
+
.map((member) => {
|
|
1516
|
+
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
1517
|
+
const args = nodeParamArgs(member);
|
|
1518
|
+
return ` ${service.name}_${member.name}(${args}${args ? ", " : ""}timeoutMs = this.defaultSlotTimeoutMs): Promise<${ret}> {
|
|
1519
|
+
return this.invokeSlot("${service.name}", "${member.name}", ${nodeParamValues(member)}, timeoutMs) as Promise<${ret}>;
|
|
1520
|
+
}`;
|
|
1521
|
+
}))
|
|
1522
|
+
.join("\n");
|
|
1523
|
+
const outputHelpers = spec.services
|
|
1524
|
+
.flatMap((service) => service.members
|
|
1525
|
+
.filter((member) => member.kind === "Output" && member.payloadTypeText)
|
|
1526
|
+
.map((member) => {
|
|
1527
|
+
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
1528
|
+
return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
|
|
1529
|
+
this.setOutputValue("${service.name}", "${member.name}", value);
|
|
1530
|
+
}`;
|
|
1531
|
+
}))
|
|
1532
|
+
.join("\n");
|
|
1533
|
+
const sessionServiceInterfaces = spec.services
|
|
1534
|
+
.map((service) => {
|
|
1535
|
+
const slotLines = service.members
|
|
1536
|
+
.filter((member) => member.kind === "Slot")
|
|
1537
|
+
.map((member) => {
|
|
1538
|
+
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
1539
|
+
const args = nodeParamArgs(member);
|
|
1540
|
+
return ` ${member.name}(${args}${args.length > 0 ? ", " : ""}timeoutMs?: number): Promise<${ret}>;`;
|
|
1541
|
+
});
|
|
1542
|
+
const signalMembers = service.members
|
|
1543
|
+
.filter((member) => member.kind === "Emitter")
|
|
1544
|
+
.map((member) => {
|
|
1545
|
+
const args = nodeParamArgs(member);
|
|
1546
|
+
return ` ${member.name}(handler: (${args}) => void): () => void;`;
|
|
1547
|
+
});
|
|
1548
|
+
const propertyMembers = service.members
|
|
1549
|
+
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
1550
|
+
.map((member) => {
|
|
1551
|
+
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
1552
|
+
if (member.kind === "Input") {
|
|
1553
|
+
return ` ${member.name}: {\n get(): Promise<${typeText}>;\n on(handler: (value: ${typeText}) => void): () => void;\n };`;
|
|
1554
|
+
}
|
|
1555
|
+
return ` ${member.name}: {\n set(value: ${typeText}): void;\n };`;
|
|
1556
|
+
});
|
|
1557
|
+
return `export interface ${service.name}SessionBridgeService {\n${slotLines.join("\n")}\n signal: {\n${signalMembers.join("\n")}\n };\n property: {\n${propertyMembers.join("\n")}\n };\n}`;
|
|
1558
|
+
})
|
|
1559
|
+
.join("\n\n");
|
|
1560
|
+
const widgetServiceFields = spec.services.map((service) => ` ${service.name}: ${service.name}SessionBridgeService;`).join("\n");
|
|
1561
|
+
const sessionBridgeFactory = spec.services
|
|
1562
|
+
.map((service) => {
|
|
1563
|
+
const slotMembers = service.members
|
|
1564
|
+
.filter((member) => member.kind === "Slot")
|
|
1565
|
+
.map((member) => {
|
|
1566
|
+
const args = member.parameters.map((p) => p.name).join(", ");
|
|
1567
|
+
const typedArgs = nodeParamArgs(member);
|
|
1568
|
+
return ` ${member.name}: (${typedArgs}${typedArgs.length > 0 ? ", " : ""}timeoutMs = defaultSlotTimeoutMs) => session.${service.name}_${member.name}(${args}${args.length > 0 ? ", " : ""}timeoutMs),`;
|
|
1569
|
+
})
|
|
1570
|
+
.join("\n");
|
|
1571
|
+
const signalMembers = service.members
|
|
1572
|
+
.filter((member) => member.kind === "Emitter")
|
|
1573
|
+
.map((member) => {
|
|
1574
|
+
const args = nodeParamArgs(member);
|
|
1575
|
+
return ` ${member.name}: (handler: (${args}) => void) => session.onSignal("${service.name}", "${member.name}", handler as (...args: unknown[]) => void),`;
|
|
1576
|
+
})
|
|
1577
|
+
.join("\n");
|
|
1578
|
+
const propertyMembers = service.members
|
|
1579
|
+
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
1580
|
+
.map((member) => {
|
|
1581
|
+
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
1582
|
+
if (member.kind === "Input") {
|
|
1583
|
+
return ` ${member.name}: {\n get: () => session.readInput("${service.name}", "${member.name}") as Promise<${typeText}>,\n on: (handler: (value: ${typeText}) => void) => session.onInput("${service.name}", "${member.name}", handler as (value: unknown) => void)\n },`;
|
|
1584
|
+
}
|
|
1585
|
+
return ` ${member.name}: {\n set: (value: ${typeText}) => session.set${service.name}_${nodeCap(member.name)}(value)\n },`;
|
|
1586
|
+
})
|
|
1587
|
+
.join("\n");
|
|
1588
|
+
return ` ${service.name}: {\n${slotMembers}\n signal: {\n${signalMembers}\n },\n property: {\n${propertyMembers}\n }\n },`;
|
|
1589
|
+
})
|
|
1590
|
+
.join("\n");
|
|
1591
|
+
const callDispatch = spec.services
|
|
1592
|
+
.flatMap((service) => service.members
|
|
1593
|
+
.filter((member) => member.kind === "Call" && member.payloadTypeText)
|
|
1594
|
+
.map((member) => {
|
|
1595
|
+
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
1596
|
+
const handler = implementation.${service.name}.${member.name};
|
|
1597
|
+
if (typeof handler !== "function") {
|
|
1598
|
+
const err = new Error("Missing Call handler ${service.name}.${member.name}");
|
|
1599
|
+
emitDiagnostic({
|
|
1600
|
+
code: "HandlerNotRegisteredError",
|
|
1601
|
+
severity: "fatal",
|
|
1602
|
+
category: "bridge",
|
|
1603
|
+
recoverable: false,
|
|
1604
|
+
message: err.message,
|
|
1605
|
+
sessionId: session.id,
|
|
1606
|
+
service,
|
|
1607
|
+
member,
|
|
1608
|
+
requestId
|
|
1609
|
+
});
|
|
1610
|
+
sendJson(session.socket, {
|
|
1611
|
+
type: "callResult",
|
|
1612
|
+
requestId,
|
|
1613
|
+
result: { __anqstError: { code: "HandlerNotRegisteredError", message: err.message, service, member } }
|
|
1614
|
+
});
|
|
1615
|
+
throw err;
|
|
1616
|
+
}
|
|
1617
|
+
Promise.resolve(handler(buildHandlerBridge(session), ...(args as ${nodeParamTuple(member)})))
|
|
1618
|
+
.then((result) => sendJson(session.socket, { type: "callResult", requestId, result }))
|
|
1619
|
+
.catch((error) => {
|
|
1620
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1621
|
+
emitDiagnostic({
|
|
1622
|
+
code: "CallHandlerError",
|
|
1623
|
+
severity: "error",
|
|
1624
|
+
category: "bridge",
|
|
1625
|
+
recoverable: true,
|
|
1626
|
+
message,
|
|
1627
|
+
sessionId: session.id,
|
|
1628
|
+
service,
|
|
1629
|
+
member,
|
|
1630
|
+
requestId
|
|
1631
|
+
});
|
|
1632
|
+
sendJson(session.socket, {
|
|
1633
|
+
type: "callResult",
|
|
1634
|
+
requestId,
|
|
1635
|
+
result: { __anqstError: { code: "CallHandlerError", message, service, member } }
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
return;
|
|
1639
|
+
}`;
|
|
1640
|
+
}))
|
|
1641
|
+
.join("\n");
|
|
1642
|
+
const emitterDispatch = spec.services
|
|
1643
|
+
.flatMap((service) => service.members
|
|
1644
|
+
.filter((member) => member.kind === "Emitter")
|
|
1645
|
+
.map((member) => {
|
|
1646
|
+
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
1647
|
+
const handler = implementation.${service.name}.${member.name};
|
|
1648
|
+
if (typeof handler !== "function") {
|
|
1649
|
+
const err = new Error("Missing Emitter handler ${service.name}.${member.name}");
|
|
1650
|
+
emitDiagnostic({
|
|
1651
|
+
code: "HandlerNotRegisteredError",
|
|
1652
|
+
severity: "fatal",
|
|
1653
|
+
category: "bridge",
|
|
1654
|
+
recoverable: false,
|
|
1655
|
+
message: err.message,
|
|
1656
|
+
sessionId: session.id,
|
|
1657
|
+
service,
|
|
1658
|
+
member
|
|
1659
|
+
});
|
|
1660
|
+
throw err;
|
|
1661
|
+
}
|
|
1662
|
+
void Promise.resolve(handler(buildHandlerBridge(session), ...(args as ${nodeParamTuple(member)}))).catch((error) => {
|
|
1663
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1664
|
+
emitDiagnostic({
|
|
1665
|
+
code: "EmitterHandlerError",
|
|
1666
|
+
severity: "error",
|
|
1667
|
+
category: "bridge",
|
|
1668
|
+
recoverable: true,
|
|
1669
|
+
message,
|
|
1670
|
+
sessionId: session.id,
|
|
1671
|
+
service,
|
|
1672
|
+
member
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
return;
|
|
1676
|
+
}`;
|
|
1677
|
+
}))
|
|
1678
|
+
.join("\n");
|
|
1679
|
+
const inputDispatch = spec.services
|
|
1680
|
+
.flatMap((service) => service.members
|
|
1681
|
+
.filter((member) => member.kind === "Input" && member.payloadTypeText)
|
|
1682
|
+
.map((member) => {
|
|
1683
|
+
return ` if (service === "${service.name}" && member === "${member.name}") {
|
|
1684
|
+
const handler = implementation.${service.name}.${member.name};
|
|
1685
|
+
if (typeof handler !== "function") {
|
|
1686
|
+
const err = new Error("Missing Input handler ${service.name}.${member.name}");
|
|
1687
|
+
emitDiagnostic({
|
|
1688
|
+
code: "HandlerNotRegisteredError",
|
|
1689
|
+
severity: "fatal",
|
|
1690
|
+
category: "bridge",
|
|
1691
|
+
recoverable: false,
|
|
1692
|
+
message: err.message,
|
|
1693
|
+
sessionId: session.id,
|
|
1694
|
+
service,
|
|
1695
|
+
member
|
|
1696
|
+
});
|
|
1697
|
+
throw err;
|
|
1698
|
+
}
|
|
1699
|
+
void Promise.resolve(handler(buildHandlerBridge(session), value as ${mapTypeTextToTs(member.payloadTypeText)})).catch((error) => {
|
|
1700
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1701
|
+
emitDiagnostic({
|
|
1702
|
+
code: "InputHandlerError",
|
|
1703
|
+
severity: "error",
|
|
1704
|
+
category: "bridge",
|
|
1705
|
+
recoverable: true,
|
|
1706
|
+
message,
|
|
1707
|
+
sessionId: session.id,
|
|
1708
|
+
service,
|
|
1709
|
+
member
|
|
1710
|
+
});
|
|
1711
|
+
});
|
|
1712
|
+
return;
|
|
1713
|
+
}`;
|
|
1714
|
+
}))
|
|
1715
|
+
.join("\n");
|
|
1716
|
+
return `import type { Express, Request } from "express";
|
|
1717
|
+
import type { WebSocket, WebSocketServer } from "ws";
|
|
1718
|
+
${typeImports}
|
|
1719
|
+
${typeDecls}
|
|
1720
|
+
|
|
1721
|
+
${handlerInterfaces}
|
|
1722
|
+
|
|
1723
|
+
export interface ${spec.widgetName}NodeImplementation {
|
|
1724
|
+
${implementationFields}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
${sessionServiceInterfaces}
|
|
1728
|
+
|
|
1729
|
+
export interface ${sessionBridgeTypeName} {
|
|
1730
|
+
${spec.widgetName}: {
|
|
1731
|
+
${widgetServiceFields}
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
export interface ${handlerBridgeTypeName} {
|
|
1736
|
+
own: ${sessionBridgeTypeName};
|
|
1737
|
+
others: Record<string, ${sessionBridgeTypeName}>;
|
|
1738
|
+
sessions: Record<string, ${sessionBridgeTypeName}>;
|
|
1739
|
+
sessionId: string;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
export interface AnQstDiagnostic {
|
|
1743
|
+
code: string;
|
|
1744
|
+
severity: "info" | "warn" | "error" | "fatal";
|
|
1745
|
+
category: string;
|
|
1746
|
+
recoverable: boolean;
|
|
1747
|
+
message: string;
|
|
1748
|
+
timestamp: string;
|
|
1749
|
+
sessionId?: string;
|
|
1750
|
+
service?: string;
|
|
1751
|
+
member?: string;
|
|
1752
|
+
requestId?: string;
|
|
1753
|
+
context?: Record<string, unknown>;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
type SlotPending = {
|
|
1757
|
+
resolve: (value: unknown) => void;
|
|
1758
|
+
reject: (error: Error) => void;
|
|
1759
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
type QueuedSlotInvocation = {
|
|
1763
|
+
requestId: string;
|
|
1764
|
+
service: string;
|
|
1765
|
+
member: string;
|
|
1766
|
+
args: unknown[];
|
|
1767
|
+
timeoutMs: number;
|
|
1768
|
+
resolve: (value: unknown) => void;
|
|
1769
|
+
reject: (error: Error) => void;
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
function sendJson(socket: WebSocket, payload: Record<string, unknown>): void {
|
|
1773
|
+
if (socket.readyState === 1) {
|
|
1774
|
+
socket.send(JSON.stringify(payload));
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function makeWsUrl(req: Request, wsPath: string): string {
|
|
1779
|
+
const forwarded = req.header("x-forwarded-proto");
|
|
1780
|
+
const protocol = (forwarded ?? req.protocol).toLowerCase() === "https" ? "wss" : "ws";
|
|
1781
|
+
return \`\${protocol}://\${req.get("host") ?? "localhost"}\${wsPath}\`;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function nowIso(): string {
|
|
1785
|
+
return new Date().toISOString();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
class ${spec.widgetName}NodeSession {
|
|
1789
|
+
readonly registeredSlots = new Set<string>();
|
|
1790
|
+
private readonly pending = new Map<string, SlotPending>();
|
|
1791
|
+
private readonly queued = new Map<string, QueuedSlotInvocation[]>();
|
|
1792
|
+
private readonly signalListeners = new Map<string, Set<(...args: unknown[]) => void>>();
|
|
1793
|
+
private readonly inputListeners = new Map<string, Set<(value: unknown) => void>>();
|
|
1794
|
+
private readonly inputState = new Map<string, unknown>();
|
|
1795
|
+
private requestCounter = 0;
|
|
1796
|
+
|
|
1797
|
+
constructor(
|
|
1798
|
+
readonly id: string,
|
|
1799
|
+
readonly socket: WebSocket,
|
|
1800
|
+
private readonly defaultSlotTimeoutMs: number,
|
|
1801
|
+
private readonly maxQueuedPerSlot: number,
|
|
1802
|
+
private readonly emitDiagnostic: (diagnostic: Omit<AnQstDiagnostic, "timestamp">) => void
|
|
1803
|
+
) {}
|
|
1804
|
+
|
|
1805
|
+
close(reason = "Session closed"): void {
|
|
1806
|
+
for (const pending of this.pending.values()) {
|
|
1807
|
+
clearTimeout(pending.timeout);
|
|
1808
|
+
pending.reject(new Error(reason));
|
|
1809
|
+
}
|
|
1810
|
+
this.pending.clear();
|
|
1811
|
+
for (const queue of this.queued.values()) {
|
|
1812
|
+
for (const item of queue) item.reject(new Error(reason));
|
|
1813
|
+
}
|
|
1814
|
+
this.queued.clear();
|
|
1815
|
+
this.signalListeners.clear();
|
|
1816
|
+
this.inputListeners.clear();
|
|
1817
|
+
this.inputState.clear();
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
registerSlot(service: string, member: string): void {
|
|
1821
|
+
const key = \`\${service}::\${member}\`;
|
|
1822
|
+
this.registeredSlots.add(key);
|
|
1823
|
+
const queue = this.queued.get(key);
|
|
1824
|
+
if (!queue || queue.length === 0) return;
|
|
1825
|
+
this.queued.delete(key);
|
|
1826
|
+
for (const item of queue) this.dispatchSlot(item);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
1830
|
+
const pending = this.pending.get(requestId);
|
|
1831
|
+
if (!pending) return;
|
|
1832
|
+
clearTimeout(pending.timeout);
|
|
1833
|
+
this.pending.delete(requestId);
|
|
1834
|
+
if (ok) {
|
|
1835
|
+
pending.resolve(payload);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
pending.reject(new Error(error || "Slot invocation failed."));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
invokeSlot(service: string, member: string, args: unknown[], timeoutMs = this.defaultSlotTimeoutMs): Promise<unknown> {
|
|
1842
|
+
return new Promise((resolve, reject) => {
|
|
1843
|
+
const requestId = \`slot-\${this.id}-\${++this.requestCounter}\`;
|
|
1844
|
+
const item: QueuedSlotInvocation = { requestId, service, member, args, timeoutMs, resolve, reject };
|
|
1845
|
+
const key = \`\${service}::\${member}\`;
|
|
1846
|
+
if (!this.registeredSlots.has(key)) {
|
|
1847
|
+
const queue = this.queued.get(key) ?? [];
|
|
1848
|
+
if (queue.length >= this.maxQueuedPerSlot) {
|
|
1849
|
+
const dropped = queue.shift();
|
|
1850
|
+
dropped?.reject(new Error("Slot queue overflow."));
|
|
1851
|
+
this.emitDiagnostic({
|
|
1852
|
+
code: "SlotQueueOverflowError",
|
|
1853
|
+
severity: "warn",
|
|
1854
|
+
category: "bridge",
|
|
1855
|
+
recoverable: true,
|
|
1856
|
+
message: "Slot queue exceeded capacity; oldest queued request dropped.",
|
|
1857
|
+
sessionId: this.id,
|
|
1858
|
+
service,
|
|
1859
|
+
member,
|
|
1860
|
+
context: { maxQueuedPerSlot: this.maxQueuedPerSlot }
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
queue.push(item);
|
|
1864
|
+
this.queued.set(key, queue);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
this.dispatchSlot(item);
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
setOutputValue(service: string, member: string, value: unknown): void {
|
|
1872
|
+
sendJson(this.socket, { type: "outputUpdated", service, member, value });
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
onSignal(service: string, member: string, handler: (...args: unknown[]) => void): () => void {
|
|
1876
|
+
const key = \`\${service}::\${member}\`;
|
|
1877
|
+
const listeners = this.signalListeners.get(key) ?? new Set<(...args: unknown[]) => void>();
|
|
1878
|
+
listeners.add(handler);
|
|
1879
|
+
this.signalListeners.set(key, listeners);
|
|
1880
|
+
return () => {
|
|
1881
|
+
const existing = this.signalListeners.get(key);
|
|
1882
|
+
if (!existing) return;
|
|
1883
|
+
existing.delete(handler);
|
|
1884
|
+
if (existing.size === 0) this.signalListeners.delete(key);
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
emitSignal(service: string, member: string, args: unknown[]): void {
|
|
1889
|
+
const key = \`\${service}::\${member}\`;
|
|
1890
|
+
for (const handler of this.signalListeners.get(key) ?? []) {
|
|
1891
|
+
try {
|
|
1892
|
+
handler(...args);
|
|
1893
|
+
} catch {
|
|
1894
|
+
// Listener errors are intentionally isolated from protocol handling.
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
onInput(service: string, member: string, handler: (value: unknown) => void): () => void {
|
|
1900
|
+
const key = \`\${service}::\${member}\`;
|
|
1901
|
+
const listeners = this.inputListeners.get(key) ?? new Set<(value: unknown) => void>();
|
|
1902
|
+
listeners.add(handler);
|
|
1903
|
+
this.inputListeners.set(key, listeners);
|
|
1904
|
+
if (this.inputState.has(key)) {
|
|
1905
|
+
handler(this.inputState.get(key));
|
|
1906
|
+
}
|
|
1907
|
+
return () => {
|
|
1908
|
+
const existing = this.inputListeners.get(key);
|
|
1909
|
+
if (!existing) return;
|
|
1910
|
+
existing.delete(handler);
|
|
1911
|
+
if (existing.size === 0) this.inputListeners.delete(key);
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
setInputState(service: string, member: string, value: unknown): void {
|
|
1916
|
+
const key = \`\${service}::\${member}\`;
|
|
1917
|
+
this.inputState.set(key, value);
|
|
1918
|
+
for (const handler of this.inputListeners.get(key) ?? []) {
|
|
1919
|
+
try {
|
|
1920
|
+
handler(value);
|
|
1921
|
+
} catch {
|
|
1922
|
+
// Listener errors are intentionally isolated from protocol handling.
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
readInput(service: string, member: string): Promise<unknown> {
|
|
1928
|
+
const key = \`\${service}::\${member}\`;
|
|
1929
|
+
if (!this.inputState.has(key)) {
|
|
1930
|
+
return Promise.reject(new Error(\`Input value for \${service}.\${member} is unavailable\`));
|
|
1931
|
+
}
|
|
1932
|
+
return Promise.resolve(this.inputState.get(key));
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
${slotHelpers}
|
|
1936
|
+
${outputHelpers}
|
|
1937
|
+
|
|
1938
|
+
private dispatchSlot(item: QueuedSlotInvocation): void {
|
|
1939
|
+
const timeout = setTimeout(() => {
|
|
1940
|
+
this.pending.delete(item.requestId);
|
|
1941
|
+
item.reject(new Error("slot invocation timeout"));
|
|
1942
|
+
this.emitDiagnostic({
|
|
1943
|
+
code: "BridgeTimeoutError",
|
|
1944
|
+
severity: "error",
|
|
1945
|
+
category: "bridge",
|
|
1946
|
+
recoverable: true,
|
|
1947
|
+
message: "Slot invocation timed out.",
|
|
1948
|
+
sessionId: this.id,
|
|
1949
|
+
service: item.service,
|
|
1950
|
+
member: item.member,
|
|
1951
|
+
requestId: item.requestId
|
|
1952
|
+
});
|
|
1953
|
+
}, item.timeoutMs);
|
|
1954
|
+
this.pending.set(item.requestId, { resolve: item.resolve, reject: item.reject, timeout });
|
|
1955
|
+
sendJson(this.socket, {
|
|
1956
|
+
type: "slotInvocationRequested",
|
|
1957
|
+
requestId: item.requestId,
|
|
1958
|
+
service: item.service,
|
|
1959
|
+
member: item.member,
|
|
1960
|
+
args: item.args
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
export interface ${spec.widgetName}NodeBridgeOptions {
|
|
1966
|
+
app: Express;
|
|
1967
|
+
wsServer: WebSocketServer;
|
|
1968
|
+
implementation: ${spec.widgetName}NodeImplementation;
|
|
1969
|
+
wsPath?: string;
|
|
1970
|
+
wsUrl?: string;
|
|
1971
|
+
devConfigPath?: string;
|
|
1972
|
+
defaultSlotTimeoutMs?: number;
|
|
1973
|
+
maxQueuedSlotInvocationsPerSlot?: number;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
export interface ${spec.widgetName}NodeBridge {
|
|
1977
|
+
onSession(listener: (session: ${spec.widgetName}NodeSession) => void): () => void;
|
|
1978
|
+
subscribeDiagnostics(listener: (diagnostic: AnQstDiagnostic) => void): () => void;
|
|
1979
|
+
getSessions(): ReadonlyArray<${spec.widgetName}NodeSession>;
|
|
1980
|
+
getSessionInterfaces(): Record<string, ${sessionBridgeTypeName}>;
|
|
1981
|
+
close(): void;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
export function create${spec.widgetName}NodeExpressWsBridge(options: ${spec.widgetName}NodeBridgeOptions): ${spec.widgetName}NodeBridge {
|
|
1985
|
+
const wsPath = options.wsPath ?? "/anqst-bridge";
|
|
1986
|
+
const devConfigPath = options.devConfigPath ?? "/anqst-dev-config.json";
|
|
1987
|
+
const defaultSlotTimeoutMs = options.defaultSlotTimeoutMs ?? 120000;
|
|
1988
|
+
const maxQueuedPerSlot = options.maxQueuedSlotInvocationsPerSlot ?? 1024;
|
|
1989
|
+
const sessions = new Map<WebSocket, ${spec.widgetName}NodeSession>();
|
|
1990
|
+
const diagnosticListeners = new Set<(diagnostic: AnQstDiagnostic) => void>();
|
|
1991
|
+
const sessionListeners = new Set<(session: ${spec.widgetName}NodeSession) => void>();
|
|
1992
|
+
let sessionCounter = 0;
|
|
1993
|
+
const implementation = options.implementation;
|
|
1994
|
+
|
|
1995
|
+
const emitDiagnostic = (diagnostic: Omit<AnQstDiagnostic, "timestamp">): void => {
|
|
1996
|
+
const next: AnQstDiagnostic = { ...diagnostic, timestamp: nowIso() };
|
|
1997
|
+
for (const listener of diagnosticListeners) listener(next);
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
const getSessionInterfaces = (): Record<string, ${sessionBridgeTypeName}> => {
|
|
2001
|
+
const out: Record<string, ${sessionBridgeTypeName}> = {};
|
|
2002
|
+
for (const session of sessions.values()) {
|
|
2003
|
+
out[session.id] = {
|
|
2004
|
+
${spec.widgetName}: {
|
|
2005
|
+
${sessionBridgeFactory}
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
return out;
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
const buildHandlerBridge = (session: ${spec.widgetName}NodeSession): ${handlerBridgeTypeName} => {
|
|
2013
|
+
const byId = getSessionInterfaces();
|
|
2014
|
+
const others: Record<string, ${sessionBridgeTypeName}> = {};
|
|
2015
|
+
for (const [id, view] of Object.entries(byId)) {
|
|
2016
|
+
if (id === session.id) continue;
|
|
2017
|
+
others[id] = view;
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
own: byId[session.id],
|
|
2021
|
+
others,
|
|
2022
|
+
sessions: byId,
|
|
2023
|
+
sessionId: session.id
|
|
2024
|
+
};
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
options.app.get(devConfigPath, (req, res) => {
|
|
2028
|
+
res.json({
|
|
2029
|
+
wsUrl: options.wsUrl ?? makeWsUrl(req, wsPath),
|
|
2030
|
+
bridgeObject: "${spec.widgetName}Bridge"
|
|
2031
|
+
});
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
const handleMessage = (session: ${spec.widgetName}NodeSession, raw: string): void => {
|
|
2035
|
+
let message: Record<string, unknown>;
|
|
2036
|
+
try {
|
|
2037
|
+
message = JSON.parse(raw) as Record<string, unknown>;
|
|
2038
|
+
} catch {
|
|
2039
|
+
emitDiagnostic({
|
|
2040
|
+
code: "DeserializationError",
|
|
2041
|
+
severity: "warn",
|
|
2042
|
+
category: "bridge",
|
|
2043
|
+
recoverable: true,
|
|
2044
|
+
message: "Incoming WS payload is not valid JSON.",
|
|
2045
|
+
sessionId: session.id
|
|
2046
|
+
});
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
const type = String(message.type ?? "");
|
|
2050
|
+
if (type === "registerSlot") {
|
|
2051
|
+
session.registerSlot(String(message.service ?? ""), String(message.member ?? ""));
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (type === "resolveSlot") {
|
|
2055
|
+
session.resolveSlot(String(message.requestId ?? ""), Boolean(message.ok), message.payload, String(message.error ?? ""));
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (type === "call") {
|
|
2059
|
+
const service = String(message.service ?? "");
|
|
2060
|
+
const member = String(message.member ?? "");
|
|
2061
|
+
const requestId = String(message.requestId ?? "");
|
|
2062
|
+
const args = Array.isArray(message.args) ? (message.args as unknown[]) : [];
|
|
2063
|
+
${callDispatch}
|
|
2064
|
+
const err = new Error(\`No Call mapping found for \${service}.\${member}\`);
|
|
2065
|
+
emitDiagnostic({
|
|
2066
|
+
code: "HandlerNotRegisteredError",
|
|
2067
|
+
severity: "fatal",
|
|
2068
|
+
category: "bridge",
|
|
2069
|
+
recoverable: false,
|
|
2070
|
+
message: err.message,
|
|
2071
|
+
sessionId: session.id,
|
|
2072
|
+
service,
|
|
2073
|
+
member,
|
|
2074
|
+
requestId
|
|
2075
|
+
});
|
|
2076
|
+
sendJson(session.socket, {
|
|
2077
|
+
type: "callResult",
|
|
2078
|
+
requestId,
|
|
2079
|
+
result: { __anqstError: { code: "HandlerNotRegisteredError", message: err.message, service, member } }
|
|
2080
|
+
});
|
|
2081
|
+
throw err;
|
|
2082
|
+
}
|
|
2083
|
+
if (type === "emit") {
|
|
2084
|
+
const service = String(message.service ?? "");
|
|
2085
|
+
const member = String(message.member ?? "");
|
|
2086
|
+
const args = Array.isArray(message.args) ? (message.args as unknown[]) : [];
|
|
2087
|
+
session.emitSignal(service, member, args);
|
|
2088
|
+
${emitterDispatch}
|
|
2089
|
+
const err = new Error(\`No Emitter mapping found for \${service}.\${member}\`);
|
|
2090
|
+
emitDiagnostic({
|
|
2091
|
+
code: "HandlerNotRegisteredError",
|
|
2092
|
+
severity: "fatal",
|
|
2093
|
+
category: "bridge",
|
|
2094
|
+
recoverable: false,
|
|
2095
|
+
message: err.message,
|
|
2096
|
+
sessionId: session.id,
|
|
2097
|
+
service,
|
|
2098
|
+
member
|
|
2099
|
+
});
|
|
2100
|
+
throw err;
|
|
2101
|
+
}
|
|
2102
|
+
if (type === "setInput") {
|
|
2103
|
+
const service = String(message.service ?? "");
|
|
2104
|
+
const member = String(message.member ?? "");
|
|
2105
|
+
const value = message.value;
|
|
2106
|
+
session.setInputState(service, member, value);
|
|
2107
|
+
${inputDispatch}
|
|
2108
|
+
const err = new Error(\`No Input mapping found for \${service}.\${member}\`);
|
|
2109
|
+
emitDiagnostic({
|
|
2110
|
+
code: "HandlerNotRegisteredError",
|
|
2111
|
+
severity: "fatal",
|
|
2112
|
+
category: "bridge",
|
|
2113
|
+
recoverable: false,
|
|
2114
|
+
message: err.message,
|
|
2115
|
+
sessionId: session.id,
|
|
2116
|
+
service,
|
|
2117
|
+
member
|
|
2118
|
+
});
|
|
2119
|
+
throw err;
|
|
2120
|
+
}
|
|
2121
|
+
emitDiagnostic({
|
|
2122
|
+
code: "ProtocolMessageUnknown",
|
|
2123
|
+
severity: "warn",
|
|
2124
|
+
category: "bridge",
|
|
2125
|
+
recoverable: true,
|
|
2126
|
+
message: \`Unknown WS message type '\${type}'.\`,
|
|
2127
|
+
sessionId: session.id
|
|
2128
|
+
});
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
const onConnection = (socket: WebSocket): void => {
|
|
2132
|
+
const session = new ${spec.widgetName}NodeSession(
|
|
2133
|
+
\`session-\${++sessionCounter}\`,
|
|
2134
|
+
socket,
|
|
2135
|
+
defaultSlotTimeoutMs,
|
|
2136
|
+
maxQueuedPerSlot,
|
|
2137
|
+
emitDiagnostic
|
|
2138
|
+
);
|
|
2139
|
+
sessions.set(socket, session);
|
|
2140
|
+
for (const listener of sessionListeners) listener(session);
|
|
2141
|
+
sendJson(socket, { type: "hostReady" });
|
|
2142
|
+
socket.on("message", (data) => {
|
|
2143
|
+
handleMessage(session, typeof data === "string" ? data : data.toString());
|
|
2144
|
+
});
|
|
2145
|
+
socket.on("close", () => {
|
|
2146
|
+
session.close("Session closed");
|
|
2147
|
+
sessions.delete(socket);
|
|
2148
|
+
});
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
options.wsServer.on("connection", onConnection);
|
|
2152
|
+
|
|
2153
|
+
return {
|
|
2154
|
+
onSession(listener) {
|
|
2155
|
+
sessionListeners.add(listener);
|
|
2156
|
+
for (const session of sessions.values()) listener(session);
|
|
2157
|
+
return () => sessionListeners.delete(listener);
|
|
2158
|
+
},
|
|
2159
|
+
subscribeDiagnostics(listener) {
|
|
2160
|
+
diagnosticListeners.add(listener);
|
|
2161
|
+
return () => diagnosticListeners.delete(listener);
|
|
2162
|
+
},
|
|
2163
|
+
getSessions() {
|
|
2164
|
+
return [...sessions.values()];
|
|
2165
|
+
},
|
|
2166
|
+
getSessionInterfaces() {
|
|
2167
|
+
return getSessionInterfaces();
|
|
2168
|
+
},
|
|
2169
|
+
close() {
|
|
2170
|
+
options.wsServer.off("connection", onConnection);
|
|
2171
|
+
for (const session of sessions.values()) session.close("Bridge closed");
|
|
2172
|
+
sessions.clear();
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
`;
|
|
2177
|
+
}
|
|
2178
|
+
function renderTypeRootIndexDts(spec) {
|
|
2179
|
+
const indexDecls = renderTypeIndexDts().trim();
|
|
2180
|
+
const typeDecls = renderTypeTypesDts(spec).trim();
|
|
2181
|
+
const serviceDecls = renderTypeServicesDts(spec).trim();
|
|
2182
|
+
const sections = [indexDecls, typeDecls, serviceDecls].filter((s) => s.length > 0);
|
|
2183
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
2184
|
+
}
|
|
2185
|
+
function generatedCppLibraryDirName(widgetName) {
|
|
2186
|
+
return `${widgetName}_QtWidget`;
|
|
2187
|
+
}
|
|
2188
|
+
function generatedNodeExpressWsDirName(widgetName) {
|
|
2189
|
+
return `${widgetName}_node_express_ws`;
|
|
2190
|
+
}
|
|
2191
|
+
function generateOutputs(spec, options = { emitQWidget: true, emitAngularService: true, emitNodeExpressWs: false }) {
|
|
2192
|
+
const cppDir = generatedCppLibraryDirName(spec.widgetName);
|
|
2193
|
+
const nodeDir = generatedNodeExpressWsDirName(spec.widgetName);
|
|
2194
|
+
const outputs = {};
|
|
2195
|
+
if (options.emitAngularService) {
|
|
2196
|
+
outputs["npmpackage/package.json"] = renderNpmPackage(spec);
|
|
2197
|
+
outputs["npmpackage/index.ts"] = renderTsIndex();
|
|
2198
|
+
outputs["npmpackage/services.ts"] = renderTsServices(spec);
|
|
2199
|
+
outputs["npmpackage/types.ts"] = renderTsTypes(spec);
|
|
2200
|
+
outputs["npmpackage/index.js"] = renderJsIndex();
|
|
2201
|
+
outputs["npmpackage/services.js"] = renderJsServices();
|
|
2202
|
+
outputs["npmpackage/types.js"] = renderJsTypes();
|
|
2203
|
+
outputs["npmpackage/types/index.d.ts"] = renderTypeRootIndexDts(spec);
|
|
2204
|
+
outputs["npmpackage/types/services.d.ts"] = renderTypeServicesDts(spec);
|
|
2205
|
+
outputs["npmpackage/types/types.d.ts"] = renderTypeTypesDts(spec);
|
|
2206
|
+
}
|
|
2207
|
+
if (options.emitQWidget) {
|
|
2208
|
+
const cppTypes = buildCppTypeContext(spec);
|
|
2209
|
+
outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
|
|
2210
|
+
outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
|
|
2211
|
+
outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetHeader(spec, cppTypes);
|
|
2212
|
+
outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
|
|
2213
|
+
outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes);
|
|
2214
|
+
}
|
|
2215
|
+
if (options.emitNodeExpressWs) {
|
|
2216
|
+
outputs[`${nodeDir}/package.json`] = renderNodeExpressWsPackage(spec);
|
|
2217
|
+
outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec);
|
|
2218
|
+
outputs[`${nodeDir}/types/index.d.ts`] = renderNodeExpressWsTypes(spec);
|
|
2219
|
+
}
|
|
2220
|
+
return outputs;
|
|
2221
|
+
}
|
|
2222
|
+
function writeGeneratedOutputs(cwd, outputs) {
|
|
2223
|
+
const outputRoot = node_path_1.default.join(cwd, "generated_output");
|
|
2224
|
+
for (const [relPath, content] of Object.entries(outputs)) {
|
|
2225
|
+
const filePath = node_path_1.default.join(outputRoot, relPath);
|
|
2226
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
2227
|
+
node_fs_1.default.writeFileSync(filePath, content, "utf8");
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
function installTypeScriptOutputs(cwd) {
|
|
2231
|
+
const sourceDir = node_path_1.default.join(cwd, "generated_output", "npmpackage");
|
|
2232
|
+
const targetDir = node_path_1.default.join(cwd, "src", "anqst-generated");
|
|
2233
|
+
if (!node_fs_1.default.existsSync(sourceDir))
|
|
2234
|
+
return;
|
|
2235
|
+
node_fs_1.default.rmSync(targetDir, { recursive: true, force: true });
|
|
2236
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
2237
|
+
const queue = [sourceDir];
|
|
2238
|
+
while (queue.length > 0) {
|
|
2239
|
+
const current = queue.shift();
|
|
2240
|
+
for (const entry of node_fs_1.default.readdirSync(current, { withFileTypes: true })) {
|
|
2241
|
+
const abs = node_path_1.default.join(current, entry.name);
|
|
2242
|
+
const rel = node_path_1.default.relative(sourceDir, abs);
|
|
2243
|
+
const dst = node_path_1.default.join(targetDir, rel);
|
|
2244
|
+
if (entry.isDirectory()) {
|
|
2245
|
+
node_fs_1.default.mkdirSync(dst, { recursive: true });
|
|
2246
|
+
queue.push(abs);
|
|
2247
|
+
}
|
|
2248
|
+
else if (entry.isFile()) {
|
|
2249
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dst), { recursive: true });
|
|
2250
|
+
node_fs_1.default.copyFileSync(abs, dst);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
function listFilesRecursively(rootDir) {
|
|
2256
|
+
const output = [];
|
|
2257
|
+
const queue = [rootDir];
|
|
2258
|
+
while (queue.length > 0) {
|
|
2259
|
+
const current = queue.shift();
|
|
2260
|
+
const entries = node_fs_1.default.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
2261
|
+
for (const entry of entries) {
|
|
2262
|
+
const abs = node_path_1.default.join(current, entry.name);
|
|
2263
|
+
if (entry.isDirectory()) {
|
|
2264
|
+
queue.push(abs);
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
if (entry.isFile()) {
|
|
2268
|
+
output.push(normalizeSlashes(node_path_1.default.relative(rootDir, abs)));
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return output.sort();
|
|
2273
|
+
}
|
|
2274
|
+
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
2275
|
+
const queue = [sourceDir];
|
|
2276
|
+
while (queue.length > 0) {
|
|
2277
|
+
const current = queue.shift();
|
|
2278
|
+
const entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
2279
|
+
for (const entry of entries) {
|
|
2280
|
+
const sourceAbs = node_path_1.default.join(current, entry.name);
|
|
2281
|
+
const rel = node_path_1.default.relative(sourceDir, sourceAbs);
|
|
2282
|
+
const targetAbs = node_path_1.default.join(targetDir, rel);
|
|
2283
|
+
if (entry.isDirectory()) {
|
|
2284
|
+
node_fs_1.default.mkdirSync(targetAbs, { recursive: true });
|
|
2285
|
+
queue.push(sourceAbs);
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
if (entry.isFile()) {
|
|
2289
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetAbs), { recursive: true });
|
|
2290
|
+
node_fs_1.default.copyFileSync(sourceAbs, targetAbs);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function resolveDistWebRoot(cwd) {
|
|
2296
|
+
const distDir = node_path_1.default.join(cwd, "dist");
|
|
2297
|
+
if (!node_fs_1.default.existsSync(distDir)) {
|
|
2298
|
+
return null;
|
|
2299
|
+
}
|
|
2300
|
+
const candidates = [];
|
|
2301
|
+
const angularJsonPath = node_path_1.default.join(cwd, "angular.json");
|
|
2302
|
+
if (node_fs_1.default.existsSync(angularJsonPath)) {
|
|
2303
|
+
try {
|
|
2304
|
+
const angularJson = JSON.parse(node_fs_1.default.readFileSync(angularJsonPath, "utf8"));
|
|
2305
|
+
const projectNames = Object.keys(angularJson.projects ?? {});
|
|
2306
|
+
const orderedProjects = [];
|
|
2307
|
+
if (typeof angularJson.defaultProject === "string" && angularJson.defaultProject.length > 0) {
|
|
2308
|
+
orderedProjects.push(angularJson.defaultProject);
|
|
2309
|
+
}
|
|
2310
|
+
for (const projectName of projectNames) {
|
|
2311
|
+
if (!orderedProjects.includes(projectName)) {
|
|
2312
|
+
orderedProjects.push(projectName);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
for (const projectName of orderedProjects) {
|
|
2316
|
+
const outputPathValue = angularJson.projects?.[projectName]?.architect?.build?.options?.outputPath;
|
|
2317
|
+
if (typeof outputPathValue === "string" && outputPathValue.length > 0) {
|
|
2318
|
+
const absolute = node_path_1.default.resolve(cwd, outputPathValue);
|
|
2319
|
+
candidates.push(node_path_1.default.join(absolute, "browser"));
|
|
2320
|
+
candidates.push(absolute);
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
if (outputPathValue &&
|
|
2324
|
+
typeof outputPathValue === "object" &&
|
|
2325
|
+
typeof outputPathValue.base === "string" &&
|
|
2326
|
+
outputPathValue.base.length > 0) {
|
|
2327
|
+
const absolute = node_path_1.default.resolve(cwd, outputPathValue.base);
|
|
2328
|
+
candidates.push(node_path_1.default.join(absolute, "browser"));
|
|
2329
|
+
candidates.push(absolute);
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
candidates.push(node_path_1.default.join(distDir, projectName, "browser"));
|
|
2333
|
+
candidates.push(node_path_1.default.join(distDir, projectName));
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
catch {
|
|
2337
|
+
// Best-effort: fallback candidates below.
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
const seen = new Set();
|
|
2341
|
+
const dedupedCandidates = candidates.filter((candidate) => {
|
|
2342
|
+
if (seen.has(candidate))
|
|
2343
|
+
return false;
|
|
2344
|
+
seen.add(candidate);
|
|
2345
|
+
return true;
|
|
2346
|
+
});
|
|
2347
|
+
for (const candidate of dedupedCandidates) {
|
|
2348
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(candidate, "index.html"))) {
|
|
2349
|
+
return candidate;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const discovered = [];
|
|
2353
|
+
const walk = (dir, depth) => {
|
|
2354
|
+
if (depth > 6)
|
|
2355
|
+
return;
|
|
2356
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
2357
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
2358
|
+
if (entry.isDirectory()) {
|
|
2359
|
+
walk(abs, depth + 1);
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
if (entry.isFile() && entry.name === "index.html") {
|
|
2363
|
+
discovered.push(dir);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
walk(distDir, 0);
|
|
2368
|
+
if (discovered.length === 0) {
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
discovered.sort((a, b) => {
|
|
2372
|
+
const aBrowser = normalizeSlashes(a).includes("/browser") ? 0 : 1;
|
|
2373
|
+
const bBrowser = normalizeSlashes(b).includes("/browser") ? 0 : 1;
|
|
2374
|
+
if (aBrowser !== bBrowser)
|
|
2375
|
+
return aBrowser - bBrowser;
|
|
2376
|
+
return a.length - b.length;
|
|
2377
|
+
});
|
|
2378
|
+
return discovered[0];
|
|
2379
|
+
}
|
|
2380
|
+
function installEmbeddedWebBundle(cwd, widgetName) {
|
|
2381
|
+
const distWebRoot = resolveDistWebRoot(cwd);
|
|
2382
|
+
if (distWebRoot === null) {
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(distWebRoot, "index.html"))) {
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
const cppLibraryRoot = node_path_1.default.join(cwd, "generated_output", generatedCppLibraryDirName(widgetName));
|
|
2389
|
+
const cppLibraryWebRoot = node_path_1.default.join(cppLibraryRoot, "webapp");
|
|
2390
|
+
node_fs_1.default.rmSync(cppLibraryWebRoot, { recursive: true, force: true });
|
|
2391
|
+
node_fs_1.default.mkdirSync(cppLibraryWebRoot, { recursive: true });
|
|
2392
|
+
copyDirectoryRecursive(distWebRoot, cppLibraryWebRoot);
|
|
2393
|
+
normalizeEmbeddedIndexHtml(node_path_1.default.join(cppLibraryWebRoot, "index.html"), cppLibraryWebRoot);
|
|
2394
|
+
const embeddedFiles = listFilesRecursively(cppLibraryWebRoot);
|
|
2395
|
+
const qrcPath = node_path_1.default.join(cppLibraryRoot, `${widgetName}.qrc`);
|
|
2396
|
+
node_fs_1.default.writeFileSync(qrcPath, renderEmbeddedQrc(widgetName, embeddedFiles), "utf8");
|
|
2397
|
+
return true;
|
|
2398
|
+
}
|
|
2399
|
+
function normalizeEmbeddedIndexHtml(indexPath, webRoot) {
|
|
2400
|
+
if (!node_fs_1.default.existsSync(indexPath)) {
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
let html = node_fs_1.default.readFileSync(indexPath, "utf8");
|
|
2404
|
+
if (html.includes('<base href="/">')) {
|
|
2405
|
+
html = html.replace('<base href="/">', '<base href="./">');
|
|
2406
|
+
}
|
|
2407
|
+
html = html.replace(/<link\b[^>]*href="([^"]+\.css)"[^>]*>\s*/g, (full, href) => {
|
|
2408
|
+
const absolute = node_path_1.default.join(webRoot, href);
|
|
2409
|
+
if (!node_fs_1.default.existsSync(absolute)) {
|
|
2410
|
+
return "";
|
|
2411
|
+
}
|
|
2412
|
+
if (node_fs_1.default.statSync(absolute).size === 0) {
|
|
2413
|
+
return "";
|
|
2414
|
+
}
|
|
2415
|
+
return full;
|
|
2416
|
+
});
|
|
2417
|
+
node_fs_1.default.writeFileSync(indexPath, html, "utf8");
|
|
2418
|
+
}
|
|
2419
|
+
function renderQtIntegrationCMake(widgetName) {
|
|
2420
|
+
const generatedRootVar = "ANQST_GENERATED_CPP_DIR";
|
|
2421
|
+
const generatedIncludeVar = "ANQST_GENERATED_INCLUDE_DIR";
|
|
2422
|
+
const webappRootVar = "ANQST_WEBAPP_ROOT";
|
|
2423
|
+
const widgetTarget = `${widgetName}Widget`;
|
|
2424
|
+
const autogenTarget = `${widgetTarget}_anqst_codegen`;
|
|
2425
|
+
return `cmake_minimum_required(VERSION 3.21)
|
|
2426
|
+
|
|
2427
|
+
set(${webappRootVar} "\${CMAKE_CURRENT_LIST_DIR}/..")
|
|
2428
|
+
set(${generatedRootVar} "\${${webappRootVar}}/generated_output/${generatedCppLibraryDirName(widgetName)}")
|
|
2429
|
+
set(${generatedIncludeVar} "\${${generatedRootVar}}/include")
|
|
2430
|
+
|
|
2431
|
+
if(TARGET ${widgetTarget})
|
|
2432
|
+
return()
|
|
2433
|
+
endif()
|
|
2434
|
+
|
|
2435
|
+
if(NOT TARGET anqstwebhostbase)
|
|
2436
|
+
message(FATAL_ERROR "Target 'anqstwebhostbase' must exist before including anqst-cmake for ${widgetName}.")
|
|
2437
|
+
endif()
|
|
2438
|
+
|
|
2439
|
+
find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
|
|
2440
|
+
set(CMAKE_AUTOMOC ON)
|
|
2441
|
+
set(CMAKE_AUTOUIC ON)
|
|
2442
|
+
set(CMAKE_AUTORCC ON)
|
|
2443
|
+
|
|
2444
|
+
find_program(ANQST_NPM_EXECUTABLE npm REQUIRED)
|
|
2445
|
+
|
|
2446
|
+
add_custom_command(
|
|
2447
|
+
OUTPUT
|
|
2448
|
+
"\${${generatedRootVar}}/CMakeLists.txt"
|
|
2449
|
+
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
2450
|
+
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
2451
|
+
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
2452
|
+
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
2453
|
+
"\${${generatedRootVar}}/webapp/index.html"
|
|
2454
|
+
COMMAND "\${ANQST_NPM_EXECUTABLE}" install
|
|
2455
|
+
COMMAND "\${ANQST_NPM_EXECUTABLE}" run anqst:build
|
|
2456
|
+
WORKING_DIRECTORY "\${${webappRootVar}}"
|
|
2457
|
+
COMMENT "Generating AnQst widget library (${widgetTarget}) from Angular project"
|
|
2458
|
+
VERBATIM
|
|
2459
|
+
)
|
|
2460
|
+
|
|
2461
|
+
add_custom_target(${autogenTarget}
|
|
2462
|
+
DEPENDS
|
|
2463
|
+
"\${${generatedRootVar}}/CMakeLists.txt"
|
|
2464
|
+
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
2465
|
+
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
2466
|
+
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
2467
|
+
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
2468
|
+
"\${${generatedRootVar}}/webapp/index.html"
|
|
2469
|
+
)
|
|
2470
|
+
|
|
2471
|
+
set_source_files_properties(
|
|
2472
|
+
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
2473
|
+
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
2474
|
+
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
2475
|
+
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
2476
|
+
PROPERTIES GENERATED TRUE
|
|
2477
|
+
)
|
|
2478
|
+
|
|
2479
|
+
add_library(${widgetTarget}
|
|
2480
|
+
"\${${generatedRootVar}}/${widgetName}.qrc"
|
|
2481
|
+
"\${${generatedRootVar}}/${widgetName}.cpp"
|
|
2482
|
+
"\${${generatedIncludeVar}}/${widgetName}.h"
|
|
2483
|
+
"\${${generatedIncludeVar}}/${widgetName}Types.h"
|
|
2484
|
+
)
|
|
2485
|
+
add_dependencies(${widgetTarget} ${autogenTarget})
|
|
2486
|
+
target_include_directories(${widgetTarget}
|
|
2487
|
+
PUBLIC
|
|
2488
|
+
"\${${generatedIncludeVar}}"
|
|
2489
|
+
)
|
|
2490
|
+
target_link_libraries(${widgetTarget}
|
|
2491
|
+
PUBLIC
|
|
2492
|
+
anqstwebhostbase
|
|
2493
|
+
)
|
|
2494
|
+
`;
|
|
2495
|
+
}
|
|
2496
|
+
function installQtIntegrationCMake(cwd, widgetName) {
|
|
2497
|
+
const integrationDir = node_path_1.default.join(cwd, "anqst-cmake");
|
|
2498
|
+
node_fs_1.default.mkdirSync(integrationDir, { recursive: true });
|
|
2499
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(integrationDir, "CMakeLists.txt"), renderQtIntegrationCMake(widgetName), "utf8");
|
|
2500
|
+
}
|