@dusted/anqst 1.5.1 → 1.7.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/README.md +45 -4
- package/dist/src/app.js +78 -15
- package/dist/src/boundary-codec-analysis.js +4 -2
- package/dist/src/emit.js +1556 -120
- package/dist/src/layout.js +9 -3
- package/dist/src/parser.js +161 -6
- package/dist/src/project.js +3 -3
- package/dist/src/verify.js +4 -2
- package/package.json +1 -1
- package/spec/AnQst-Spec-DSL.d.ts +22 -24
package/dist/src/emit.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.generateOutputs = generateOutputs;
|
|
7
7
|
exports.writeGeneratedOutputs = writeGeneratedOutputs;
|
|
8
|
+
exports.buildVanillaJsBrowserBundle = buildVanillaJsBrowserBundle;
|
|
8
9
|
exports.installEmbeddedWebBundle = installEmbeddedWebBundle;
|
|
9
10
|
exports.installQtIntegrationCMake = installQtIntegrationCMake;
|
|
10
11
|
exports.installQtDesignerPluginCMake = installQtDesignerPluginCMake;
|
|
@@ -277,6 +278,52 @@ function sanitizeIdentifier(value) {
|
|
|
277
278
|
const withFallback = trimmed.length > 0 ? trimmed : "Codec";
|
|
278
279
|
return /^[0-9]/.test(withFallback) ? `T_${withFallback}` : withFallback;
|
|
279
280
|
}
|
|
281
|
+
/** Qualify generated struct/enum names in public widget headers so TUs that include multiple widgets
|
|
282
|
+
* (each with `using namespace WidgetName`) do not get ambiguous unqualified types (e.g. two `Magic`). */
|
|
283
|
+
const CPP_WIDGET_HEADER_PRIMITIVES = new Set([
|
|
284
|
+
"void",
|
|
285
|
+
"bool",
|
|
286
|
+
"double",
|
|
287
|
+
"float",
|
|
288
|
+
"QString",
|
|
289
|
+
"QByteArray",
|
|
290
|
+
"QVariantMap",
|
|
291
|
+
"QStringList",
|
|
292
|
+
"qint64",
|
|
293
|
+
"quint64",
|
|
294
|
+
"qint32",
|
|
295
|
+
"quint32",
|
|
296
|
+
"qint16",
|
|
297
|
+
"quint16",
|
|
298
|
+
"qint8",
|
|
299
|
+
"quint8",
|
|
300
|
+
"int8_t",
|
|
301
|
+
"uint8_t",
|
|
302
|
+
"int16_t",
|
|
303
|
+
"uint16_t",
|
|
304
|
+
"int32_t",
|
|
305
|
+
"uint32_t"
|
|
306
|
+
]);
|
|
307
|
+
function qualifyCppTypeForWidgetHeaderPublicApi(widgetName, cppType) {
|
|
308
|
+
const trimmed = cppType.trim();
|
|
309
|
+
const listInner = /^QList<(.+)>$/.exec(trimmed);
|
|
310
|
+
if (listInner) {
|
|
311
|
+
return `QList<${qualifyCppTypeForWidgetHeaderPublicApi(widgetName, listInner[1])}>`;
|
|
312
|
+
}
|
|
313
|
+
if (trimmed.includes("::")) {
|
|
314
|
+
return trimmed;
|
|
315
|
+
}
|
|
316
|
+
if (CPP_WIDGET_HEADER_PRIMITIVES.has(trimmed)) {
|
|
317
|
+
return trimmed;
|
|
318
|
+
}
|
|
319
|
+
if (trimmed.startsWith("std::")) {
|
|
320
|
+
return trimmed;
|
|
321
|
+
}
|
|
322
|
+
if (/^(qint|quint)[0-9]{1,2}$/.test(trimmed)) {
|
|
323
|
+
return trimmed;
|
|
324
|
+
}
|
|
325
|
+
return `${widgetName}::${trimmed}`;
|
|
326
|
+
}
|
|
280
327
|
function variantToCppExpression(cppType, expr) {
|
|
281
328
|
if (cppType === "QString")
|
|
282
329
|
return `${expr}.toString()`;
|
|
@@ -415,8 +462,10 @@ function collectStructDecls(spec) {
|
|
|
415
462
|
const out = new Map();
|
|
416
463
|
for (const d of spec.namespaceTypeDecls)
|
|
417
464
|
out.set(d.name, d);
|
|
418
|
-
for (const d of spec.importedTypeDecls.values())
|
|
419
|
-
out.
|
|
465
|
+
for (const d of spec.importedTypeDecls.values()) {
|
|
466
|
+
if (!out.has(d.name))
|
|
467
|
+
out.set(d.name, d);
|
|
468
|
+
}
|
|
420
469
|
return [...out.values()];
|
|
421
470
|
}
|
|
422
471
|
function mapTypeTextToTs(typeText) {
|
|
@@ -528,14 +577,18 @@ function normalizeImportPathForGenerated(specFilePath, generatedFileRelPath, mod
|
|
|
528
577
|
return normalized;
|
|
529
578
|
return `./${normalized}`;
|
|
530
579
|
}
|
|
531
|
-
function renderRequiredTypeImports(spec, generatedFileRelPath) {
|
|
580
|
+
function renderRequiredTypeImports(spec, generatedFileRelPath, omitSymbols = new Set()) {
|
|
532
581
|
const requiredSymbols = collectRequiredImportedSymbols(spec);
|
|
533
582
|
if (requiredSymbols.size === 0)
|
|
534
583
|
return "";
|
|
535
584
|
const importLines = [];
|
|
536
585
|
for (const imp of spec.specImports) {
|
|
537
|
-
const defaultImport = imp.defaultImport
|
|
538
|
-
|
|
586
|
+
const defaultImport = imp.defaultImport
|
|
587
|
+
&& requiredSymbols.has(imp.defaultImport)
|
|
588
|
+
&& !omitSymbols.has(imp.defaultImport)
|
|
589
|
+
? imp.defaultImport
|
|
590
|
+
: null;
|
|
591
|
+
const named = imp.namedImports.filter((n) => requiredSymbols.has(n.localName) && !omitSymbols.has(n.localName));
|
|
539
592
|
if (!defaultImport && named.length === 0)
|
|
540
593
|
continue;
|
|
541
594
|
const moduleSpecifier = normalizeImportPathForGenerated(spec.filePath, generatedFileRelPath, imp.moduleSpecifier);
|
|
@@ -1183,32 +1236,44 @@ function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
|
|
|
1183
1236
|
const properties = [];
|
|
1184
1237
|
const fields = [];
|
|
1185
1238
|
const publicSlots = [];
|
|
1186
|
-
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) =>
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1239
|
+
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) => {
|
|
1240
|
+
const qPayload = qualifyCppTypeForWidgetHeaderPublicApi(spec.widgetName, helper.cppType);
|
|
1241
|
+
return [
|
|
1242
|
+
`static QByteArray encodeDragDropPayload_${helper.typeName}(const ${qPayload}& payload);`,
|
|
1243
|
+
`static std::optional<${qPayload}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
|
|
1244
|
+
];
|
|
1245
|
+
});
|
|
1246
|
+
const qType = (t) => qualifyCppTypeForWidgetHeaderPublicApi(spec.widgetName, t);
|
|
1190
1247
|
for (const service of spec.services) {
|
|
1191
1248
|
for (const member of service.members) {
|
|
1192
1249
|
const memberPascal = pascalCase(member.name);
|
|
1193
1250
|
if (member.kind === "Call" && member.payloadTypeText) {
|
|
1194
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1195
|
-
const args = member.parameters
|
|
1251
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1252
|
+
const args = member.parameters
|
|
1253
|
+
.map((p) => `const ${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))}& ${p.name}`)
|
|
1254
|
+
.join(", ");
|
|
1196
1255
|
callbackAliases.push(`using ${memberPascal}Handler = std::function<${cppType}(${args})>;`);
|
|
1197
1256
|
handleMethods.push(` void ${member.name}(const ${memberPascal}Handler& handler) const;`);
|
|
1198
1257
|
callSetterMethods.push(`void set${memberPascal}CallHandler(const ${memberPascal}Handler& handler);`);
|
|
1199
1258
|
fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
|
|
1200
1259
|
}
|
|
1201
1260
|
else if (member.kind === "Emitter") {
|
|
1202
|
-
const args = member.parameters
|
|
1261
|
+
const args = member.parameters
|
|
1262
|
+
.map((p) => `const ${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))}& ${p.name}`)
|
|
1263
|
+
.join(", ");
|
|
1203
1264
|
signals.push(`void ${member.name}(${args});`);
|
|
1204
1265
|
}
|
|
1205
1266
|
else if (member.kind === "Slot") {
|
|
1206
|
-
const ret = member.payloadTypeText
|
|
1207
|
-
|
|
1267
|
+
const ret = member.payloadTypeText
|
|
1268
|
+
? qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]))
|
|
1269
|
+
: "void";
|
|
1270
|
+
const args = member.parameters
|
|
1271
|
+
.map((p) => `${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))} ${p.name}`)
|
|
1272
|
+
.join(", ");
|
|
1208
1273
|
slotMethods.push(`${ret} slot_${member.name}(${args});`);
|
|
1209
1274
|
}
|
|
1210
1275
|
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
1211
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1276
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1212
1277
|
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
1213
1278
|
properties.push(`Q_PROPERTY(${cppType} ${member.name} READ ${member.name} WRITE set${cap} NOTIFY ${member.name}Changed)`);
|
|
1214
1279
|
publicMethods.push(`${cppType} ${member.name}() const;`);
|
|
@@ -1225,11 +1290,11 @@ function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
|
|
|
1225
1290
|
}
|
|
1226
1291
|
}
|
|
1227
1292
|
else if (member.kind === "DropTarget" && member.payloadTypeText) {
|
|
1228
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1293
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1229
1294
|
signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
|
|
1230
1295
|
}
|
|
1231
1296
|
else if (member.kind === "HoverTarget" && member.payloadTypeText) {
|
|
1232
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1297
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1233
1298
|
signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
|
|
1234
1299
|
signals.push(`void ${member.name}Left();`);
|
|
1235
1300
|
}
|
|
@@ -2012,7 +2077,8 @@ function renderNpmPackage(spec) {
|
|
|
2012
2077
|
anqst: {
|
|
2013
2078
|
widget: spec.widgetName,
|
|
2014
2079
|
services: spec.services.map((s) => s.name),
|
|
2015
|
-
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport
|
|
2080
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport,
|
|
2081
|
+
outputContractVersion: 2
|
|
2016
2082
|
}
|
|
2017
2083
|
}, null, 2);
|
|
2018
2084
|
}
|
|
@@ -2041,6 +2107,19 @@ function slotHandlerReturnType(tsRet) {
|
|
|
2041
2107
|
}
|
|
2042
2108
|
return `${tsRet} | Promise<${tsRet}> | Error`;
|
|
2043
2109
|
}
|
|
2110
|
+
/** Angular and vanilla: emit `readonly set` / `readonly onSlot` only when the spec provides members for that namespace. */
|
|
2111
|
+
function formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers) {
|
|
2112
|
+
const blocks = [];
|
|
2113
|
+
if (setMembers.length > 0) {
|
|
2114
|
+
blocks.push(` readonly set = {\n${setMembers.join("\n")}\n };`);
|
|
2115
|
+
}
|
|
2116
|
+
if (onSlotMembers.length > 0) {
|
|
2117
|
+
blocks.push(` readonly onSlot = {\n${onSlotMembers.join("\n")}\n };`);
|
|
2118
|
+
}
|
|
2119
|
+
if (blocks.length === 0)
|
|
2120
|
+
return "";
|
|
2121
|
+
return `\n${blocks.join("\n")}`;
|
|
2122
|
+
}
|
|
2044
2123
|
function renderTsService(spec, serviceName, codecCatalog) {
|
|
2045
2124
|
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
2046
2125
|
const fieldLines = [];
|
|
@@ -2191,14 +2270,7 @@ function renderTsService(spec, serviceName, codecCatalog) {
|
|
|
2191
2270
|
export class ${serviceName} {
|
|
2192
2271
|
private readonly _bridge = inject(AnQstBridgeRuntime);
|
|
2193
2272
|
${fieldLines.join("\n")}
|
|
2194
|
-
${constructorLines.join("\n")}
|
|
2195
|
-
readonly set = {
|
|
2196
|
-
${setMembers.join("\n")}
|
|
2197
|
-
};
|
|
2198
|
-
readonly onSlot = {
|
|
2199
|
-
${onSlotMembers.join("\n")}
|
|
2200
|
-
};
|
|
2201
|
-
${methodLines.join("\n")}
|
|
2273
|
+
${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers)}${methodLines.length > 0 ? `\n${methodLines.join("\n")}` : ""}
|
|
2202
2274
|
}
|
|
2203
2275
|
`;
|
|
2204
2276
|
}
|
|
@@ -2241,25 +2313,29 @@ function renderTsServiceDts(spec, serviceName) {
|
|
|
2241
2313
|
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
2242
2314
|
}
|
|
2243
2315
|
}
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
readonly onSlot: ${onSlotInterfaceName}
|
|
2316
|
+
const interfaceBlocks = [];
|
|
2317
|
+
if (setMembers.length > 0) {
|
|
2318
|
+
interfaceBlocks.push(`export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`);
|
|
2319
|
+
}
|
|
2320
|
+
if (onSlotMembers.length > 0) {
|
|
2321
|
+
interfaceBlocks.push(`export interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`);
|
|
2322
|
+
}
|
|
2323
|
+
const interfaceSection = interfaceBlocks.length > 0 ? `${interfaceBlocks.join("\n\n")}\n\n` : "";
|
|
2324
|
+
const namespaceLines = [];
|
|
2325
|
+
if (setMembers.length > 0) {
|
|
2326
|
+
namespaceLines.push(` readonly set: ${setInterfaceName};`);
|
|
2327
|
+
}
|
|
2328
|
+
if (onSlotMembers.length > 0) {
|
|
2329
|
+
namespaceLines.push(` readonly onSlot: ${onSlotInterfaceName};`);
|
|
2330
|
+
}
|
|
2331
|
+
const declareBodyLines = [...namespaceLines, ...classMembers];
|
|
2332
|
+
return `${interfaceSection}export declare class ${serviceName} {
|
|
2333
|
+
${declareBodyLines.join("\n")}
|
|
2258
2334
|
}`;
|
|
2259
2335
|
}
|
|
2260
2336
|
function renderTsServices(spec, codecCatalog) {
|
|
2261
2337
|
const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name, codecCatalog)).join("\n");
|
|
2262
|
-
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
|
|
2338
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/services.ts`).trim();
|
|
2263
2339
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
2264
2340
|
const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
|
|
2265
2341
|
const typeImportsBlock = typeImports.length > 0 ? `${typeImports}\n\n` : "";
|
|
@@ -3077,13 +3153,13 @@ ${serviceClasses}
|
|
|
3077
3153
|
`;
|
|
3078
3154
|
}
|
|
3079
3155
|
function renderTsTypes(spec) {
|
|
3080
|
-
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types.ts`).trim();
|
|
3156
|
+
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types.ts`).trim();
|
|
3081
3157
|
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
3082
3158
|
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
3083
3159
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
3084
3160
|
}
|
|
3085
3161
|
function renderTypeServicesDts(spec) {
|
|
3086
|
-
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
|
|
3162
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types/services.d.ts`).trim();
|
|
3087
3163
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
3088
3164
|
const bridgeDiagnosticsDecl = `export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
3089
3165
|
|
|
@@ -3120,7 +3196,7 @@ export declare class AnQstBridgeDiagnostics {
|
|
|
3120
3196
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
3121
3197
|
}
|
|
3122
3198
|
function renderTypeTypesDts(spec) {
|
|
3123
|
-
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/types.d.ts`).trim();
|
|
3199
|
+
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types/types.d.ts`).trim();
|
|
3124
3200
|
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
3125
3201
|
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
3126
3202
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
@@ -3149,74 +3225,1363 @@ function renderJsServices() {
|
|
|
3149
3225
|
function renderJsTypes() {
|
|
3150
3226
|
return renderJsModule();
|
|
3151
3227
|
}
|
|
3152
|
-
function
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3228
|
+
function renderVanillaServiceTs(spec, serviceName, codecCatalog) {
|
|
3229
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
3230
|
+
const fieldLines = [];
|
|
3231
|
+
const methodLines = [];
|
|
3232
|
+
const setMembers = [];
|
|
3233
|
+
const onSlotMembers = [];
|
|
3234
|
+
const constructorBodyLines = [];
|
|
3235
|
+
for (const m of members) {
|
|
3236
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
3237
|
+
const paramSites = m.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, serviceName, m.name, p.name));
|
|
3238
|
+
const encodedValueArray = paramSites.length > 0
|
|
3239
|
+
? `[${m.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
3240
|
+
: "[]";
|
|
3241
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, serviceName, m.name);
|
|
3242
|
+
if (m.kind === "Call") {
|
|
3243
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
3244
|
+
if (payloadSite) {
|
|
3245
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { const result = await this._bridge.call<unknown>("${serviceName}", "${m.name}", ${encodedValueArray}); return decode${payloadSite.codecId}(result); }`);
|
|
3163
3246
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
target: "node_express_ws"
|
|
3247
|
+
else {
|
|
3248
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${encodedValueArray}); }`);
|
|
3249
|
+
}
|
|
3250
|
+
continue;
|
|
3169
3251
|
}
|
|
3170
|
-
|
|
3252
|
+
if (m.kind === "Emitter") {
|
|
3253
|
+
methodLines.push(` ${m.name}(${args}): void {`);
|
|
3254
|
+
methodLines.push(` let encodedArgs: unknown[];`);
|
|
3255
|
+
methodLines.push(` try {`);
|
|
3256
|
+
methodLines.push(` encodedArgs = ${encodedValueArray};`);
|
|
3257
|
+
methodLines.push(` } catch (error) {`);
|
|
3258
|
+
methodLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3259
|
+
methodLines.push(` code: "SerializationError",`);
|
|
3260
|
+
methodLines.push(` severity: "error",`);
|
|
3261
|
+
methodLines.push(` category: "bridge",`);
|
|
3262
|
+
methodLines.push(` recoverable: true,`);
|
|
3263
|
+
methodLines.push(` message: \`Failed to serialize Emitter ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3264
|
+
methodLines.push(` service: "${serviceName}",`);
|
|
3265
|
+
methodLines.push(` member: "${m.name}",`);
|
|
3266
|
+
methodLines.push(` context: { interaction: "Emitter" }`);
|
|
3267
|
+
methodLines.push(` });`);
|
|
3268
|
+
methodLines.push(` return;`);
|
|
3269
|
+
methodLines.push(` }`);
|
|
3270
|
+
methodLines.push(` this._bridge.emit("${serviceName}", "${m.name}", encodedArgs);`);
|
|
3271
|
+
methodLines.push(` }`);
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
if (m.kind === "Slot") {
|
|
3275
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
3276
|
+
const decodedArgs = m.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(wireArgs[${index}])` : `wireArgs[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ");
|
|
3277
|
+
onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
|
|
3278
|
+
onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", (...wireArgs: unknown[]) => {`);
|
|
3279
|
+
onSlotMembers.push(` const result = handler(${decodedArgs});`);
|
|
3280
|
+
if (payloadSite) {
|
|
3281
|
+
onSlotMembers.push(` if (result instanceof Promise) return result.then((value) => value instanceof Error ? value : encode${payloadSite.codecId}(value));`);
|
|
3282
|
+
onSlotMembers.push(` return result instanceof Error ? result : encode${payloadSite.codecId}(result);`);
|
|
3283
|
+
}
|
|
3284
|
+
else {
|
|
3285
|
+
onSlotMembers.push(" return result;");
|
|
3286
|
+
}
|
|
3287
|
+
onSlotMembers.push(" });");
|
|
3288
|
+
onSlotMembers.push(" },");
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
3292
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3293
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<${tsType} | undefined>(undefined);`);
|
|
3294
|
+
methodLines.push(` ${m.name}(): ${tsType} | undefined { return this._${m.name}.get(); }`);
|
|
3295
|
+
if (m.kind === "Input") {
|
|
3296
|
+
setMembers.push(` ${m.name}: (value: ${tsType}): void => {`);
|
|
3297
|
+
setMembers.push(` let encodedValue: unknown;`);
|
|
3298
|
+
setMembers.push(` try {`);
|
|
3299
|
+
setMembers.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"};`);
|
|
3300
|
+
setMembers.push(` } catch (error) {`);
|
|
3301
|
+
setMembers.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3302
|
+
setMembers.push(` code: "SerializationError",`);
|
|
3303
|
+
setMembers.push(` severity: "error",`);
|
|
3304
|
+
setMembers.push(` category: "bridge",`);
|
|
3305
|
+
setMembers.push(` recoverable: true,`);
|
|
3306
|
+
setMembers.push(` message: \`Failed to serialize Input ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3307
|
+
setMembers.push(` service: "${serviceName}",`);
|
|
3308
|
+
setMembers.push(` member: "${m.name}",`);
|
|
3309
|
+
setMembers.push(` context: { interaction: "Input" }`);
|
|
3310
|
+
setMembers.push(` });`);
|
|
3311
|
+
setMembers.push(` return;`);
|
|
3312
|
+
setMembers.push(` }`);
|
|
3313
|
+
setMembers.push(` this._${m.name}.set(value);`);
|
|
3314
|
+
setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", encodedValue);`);
|
|
3315
|
+
setMembers.push(" },");
|
|
3316
|
+
}
|
|
3317
|
+
if (m.kind === "Output") {
|
|
3318
|
+
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => {`);
|
|
3319
|
+
constructorBodyLines.push(` this._${m.name}.set(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${tsType}`});`);
|
|
3320
|
+
constructorBodyLines.push(` });`);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
3324
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3325
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
3326
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
3327
|
+
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}.get(); }`);
|
|
3328
|
+
constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
3329
|
+
constructorBodyLines.push(` try {`);
|
|
3330
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
3331
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
3332
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3333
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
3334
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
3335
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
3336
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
3337
|
+
constructorBodyLines.push(` message: \`Failed to deserialize DropTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3338
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
3339
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
3340
|
+
constructorBodyLines.push(` context: { interaction: "DropTarget" }`);
|
|
3341
|
+
constructorBodyLines.push(` });`);
|
|
3342
|
+
constructorBodyLines.push(` }`);
|
|
3343
|
+
constructorBodyLines.push(` });`);
|
|
3344
|
+
}
|
|
3345
|
+
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
3346
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3347
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
3348
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
3349
|
+
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}.get(); }`);
|
|
3350
|
+
constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
3351
|
+
constructorBodyLines.push(` try {`);
|
|
3352
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
3353
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
3354
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3355
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
3356
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
3357
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
3358
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
3359
|
+
constructorBodyLines.push(` message: \`Failed to deserialize HoverTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3360
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
3361
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
3362
|
+
constructorBodyLines.push(` context: { interaction: "HoverTarget" }`);
|
|
3363
|
+
constructorBodyLines.push(` });`);
|
|
3364
|
+
constructorBodyLines.push(` }`);
|
|
3365
|
+
constructorBodyLines.push(` });`);
|
|
3366
|
+
constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
const constructorLines = [
|
|
3370
|
+
" constructor(private readonly _bridge: AnQstBridgeRuntime) {",
|
|
3371
|
+
...constructorBodyLines,
|
|
3372
|
+
" }",
|
|
3373
|
+
];
|
|
3374
|
+
return `class ${serviceName} {
|
|
3375
|
+
${fieldLines.join("\n")}
|
|
3376
|
+
${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers)}${methodLines.length > 0 ? `\n${methodLines.join("\n")}` : ""}
|
|
3171
3377
|
}
|
|
3172
|
-
|
|
3173
|
-
if (member.parameters.length === 0)
|
|
3174
|
-
return "[]";
|
|
3175
|
-
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
3378
|
+
`;
|
|
3176
3379
|
}
|
|
3177
|
-
function
|
|
3178
|
-
|
|
3380
|
+
function collectVanillaValueClasses(spec) {
|
|
3381
|
+
const classes = [];
|
|
3382
|
+
const requiredImportedSymbols = collectRequiredImportedSymbols(spec);
|
|
3383
|
+
const candidateDecls = [
|
|
3384
|
+
...spec.namespaceTypeDecls,
|
|
3385
|
+
...[...spec.importedTypeDecls.values()].filter((decl) => requiredImportedSymbols.has(decl.name))
|
|
3386
|
+
];
|
|
3387
|
+
for (const decl of candidateDecls) {
|
|
3388
|
+
if (decl.kind !== "interface")
|
|
3389
|
+
continue;
|
|
3390
|
+
const node = parseTypeDeclNode(decl.nodeText);
|
|
3391
|
+
if (!node || !typescript_1.default.isInterfaceDeclaration(node))
|
|
3392
|
+
continue;
|
|
3393
|
+
const fields = [];
|
|
3394
|
+
let supported = true;
|
|
3395
|
+
for (const member of node.members) {
|
|
3396
|
+
if (!typescript_1.default.isPropertySignature(member) || !member.type || !member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
3397
|
+
supported = false;
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
fields.push({
|
|
3401
|
+
name: member.name.text,
|
|
3402
|
+
typeText: member.type.getText(),
|
|
3403
|
+
optional: !!member.questionToken
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
if (supported) {
|
|
3407
|
+
classes.push({
|
|
3408
|
+
name: decl.name,
|
|
3409
|
+
fields
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
return classes;
|
|
3179
3414
|
}
|
|
3180
|
-
function
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3415
|
+
function renderVanillaValueClassConstructorArgs(fields) {
|
|
3416
|
+
let lastRequiredIndex = -1;
|
|
3417
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
3418
|
+
if (!fields[i].optional) {
|
|
3419
|
+
lastRequiredIndex = i;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
return fields.map((field, index) => {
|
|
3423
|
+
const tsType = mapTypeTextToTs(field.typeText);
|
|
3424
|
+
if (!field.optional) {
|
|
3425
|
+
return `${field.name}: ${tsType}`;
|
|
3426
|
+
}
|
|
3427
|
+
if (index > lastRequiredIndex) {
|
|
3428
|
+
return `${field.name}?: ${tsType}`;
|
|
3429
|
+
}
|
|
3430
|
+
return `${field.name}: ${tsType} | undefined`;
|
|
3431
|
+
}).join(", ");
|
|
3432
|
+
}
|
|
3433
|
+
function renderVanillaValueClassTs(model) {
|
|
3434
|
+
const fieldLines = model.fields.map((field) => {
|
|
3435
|
+
const tsType = mapTypeTextToTs(field.typeText);
|
|
3436
|
+
return field.optional
|
|
3437
|
+
? ` ${field.name}?: ${tsType};`
|
|
3438
|
+
: ` ${field.name}: ${tsType};`;
|
|
3439
|
+
});
|
|
3440
|
+
const constructorArgs = renderVanillaValueClassConstructorArgs(model.fields);
|
|
3441
|
+
const constructorBody = model.fields.map((field) => ` this.${field.name} = ${field.name};`);
|
|
3442
|
+
const constructorLines = [
|
|
3443
|
+
` constructor(${constructorArgs}) {`,
|
|
3444
|
+
...constructorBody,
|
|
3445
|
+
" }"
|
|
3446
|
+
];
|
|
3447
|
+
return `class ${model.name} {
|
|
3448
|
+
${fieldLines.join("\n")}
|
|
3449
|
+
|
|
3450
|
+
${constructorLines.join("\n")}
|
|
3451
|
+
}`;
|
|
3184
3452
|
}
|
|
3185
|
-
function
|
|
3186
|
-
|
|
3453
|
+
function renderVanillaValueClassDts(model) {
|
|
3454
|
+
const fieldLines = model.fields.map((field) => {
|
|
3455
|
+
const tsType = mapTypeTextToTs(field.typeText);
|
|
3456
|
+
return field.optional
|
|
3457
|
+
? ` ${field.name}?: ${tsType};`
|
|
3458
|
+
: ` ${field.name}: ${tsType};`;
|
|
3459
|
+
});
|
|
3460
|
+
const constructorArgs = renderVanillaValueClassConstructorArgs(model.fields);
|
|
3461
|
+
return `interface ${model.name} {
|
|
3462
|
+
${fieldLines.join("\n")}
|
|
3187
3463
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3464
|
+
|
|
3465
|
+
declare const ${model.name}: {
|
|
3466
|
+
new (${constructorArgs}): ${model.name};
|
|
3467
|
+
prototype: ${model.name};
|
|
3468
|
+
};`;
|
|
3193
3469
|
}
|
|
3194
|
-
function
|
|
3195
|
-
const
|
|
3196
|
-
const
|
|
3197
|
-
const
|
|
3198
|
-
const
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
const
|
|
3470
|
+
function renderVanillaBrowserTs(spec, codecCatalog, emitExports = false) {
|
|
3471
|
+
const localTypeDecls = renderTypeDeclarations(spec).trim();
|
|
3472
|
+
const localTypesBlock = localTypeDecls.length > 0 ? `${localTypeDecls}\n\n` : "";
|
|
3473
|
+
const valueClasses = collectVanillaValueClasses(spec);
|
|
3474
|
+
const valueClassDecls = valueClasses.map((model) => renderVanillaValueClassTs(model)).join("\n\n");
|
|
3475
|
+
const valueClassBlock = valueClassDecls.length > 0 ? `${valueClassDecls}\n\n` : "";
|
|
3476
|
+
const dragDropHelperBlock = renderTsDragDropPayloadHelpers(spec, codecCatalog).trim();
|
|
3477
|
+
const dragDropHelpers = dragDropHelperBlock.length > 0 ? `\n// Drag/drop payload helpers\n${dragDropHelperBlock}\n` : "";
|
|
3478
|
+
const serviceClasses = spec.services.map((s) => renderVanillaServiceTs(spec, s.name, codecCatalog)).join("\n");
|
|
3479
|
+
const frontendShapeLines = [
|
|
3480
|
+
" diagnostics: AnQstBridgeDiagnostics;",
|
|
3481
|
+
...spec.services.map((s) => ` ${s.name}: ${s.name};`),
|
|
3482
|
+
...valueClasses.map((model) => ` ${model.name}: typeof ${model.name};`)
|
|
3483
|
+
];
|
|
3484
|
+
const frontendFactoryLines = [
|
|
3485
|
+
" diagnostics: new AnQstBridgeDiagnostics(bridge)",
|
|
3486
|
+
...spec.services.map((s) => ` ${s.name}: new ${s.name}(bridge)`),
|
|
3487
|
+
...valueClasses.map((model) => ` ${model.name}`)
|
|
3488
|
+
];
|
|
3489
|
+
const exportedRuntimeSymbols = [
|
|
3490
|
+
"AnQstBridgeDiagnostics",
|
|
3491
|
+
...spec.services.map((s) => s.name),
|
|
3492
|
+
...valueClasses.map((model) => model.name),
|
|
3493
|
+
"createFrontend"
|
|
3494
|
+
];
|
|
3495
|
+
const exportedTypeSymbols = [
|
|
3496
|
+
"AnQstBridgeDiagnostic",
|
|
3497
|
+
"AnQstBridgeState",
|
|
3498
|
+
`${spec.widgetName}Frontend`,
|
|
3499
|
+
`${spec.widgetName}Global`,
|
|
3500
|
+
"AnQstGeneratedRoot"
|
|
3501
|
+
];
|
|
3502
|
+
const exportsBlock = emitExports
|
|
3503
|
+
? `\nexport { ${exportedRuntimeSymbols.join(", ")} };\nexport type { ${exportedTypeSymbols.join(", ")} };\n`
|
|
3504
|
+
: "";
|
|
3505
|
+
return `${localTypesBlock}${valueClassBlock}// Boundary codec plan helpers
|
|
3506
|
+
${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
|
|
3507
|
+
${dragDropHelpers}
|
|
3508
|
+
|
|
3509
|
+
type SlotHandler = (...args: unknown[]) => unknown;
|
|
3510
|
+
type OutputHandler = (value: unknown) => void;
|
|
3511
|
+
type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
|
|
3512
|
+
type OutputListener = (service: string, member: string, value: unknown) => void;
|
|
3513
|
+
type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
3514
|
+
type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
3515
|
+
type HoverLeftListener = (service: string, member: string) => void;
|
|
3516
|
+
type HostDiagnosticListener = (payload: unknown) => void;
|
|
3517
|
+
type DisconnectListener = () => void;
|
|
3518
|
+
|
|
3519
|
+
type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
3520
|
+
type AnQstBridgeSource = "frontend" | "host";
|
|
3521
|
+
type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
3522
|
+
type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
3523
|
+
|
|
3524
|
+
interface AnQstBridgeDiagnostic {
|
|
3525
|
+
code: string;
|
|
3526
|
+
severity: AnQstBridgeSeverity;
|
|
3527
|
+
category: string;
|
|
3528
|
+
recoverable: boolean;
|
|
3529
|
+
message: string;
|
|
3530
|
+
timestamp: string;
|
|
3531
|
+
source: AnQstBridgeSource;
|
|
3532
|
+
transport?: AnQstBridgeTransport;
|
|
3533
|
+
service?: string;
|
|
3534
|
+
member?: string;
|
|
3535
|
+
requestId?: string;
|
|
3536
|
+
context?: Record<string, unknown>;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
interface HostBridgeApi {
|
|
3540
|
+
anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
|
|
3541
|
+
anQstBridge_emit(service: string, member: string, args: unknown[]): void;
|
|
3542
|
+
anQstBridge_setInput(service: string, member: string, value: unknown): void;
|
|
3543
|
+
anQstBridge_registerSlot(service: string, member: string): void;
|
|
3544
|
+
anQstBridge_resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
3545
|
+
anQstBridge_outputUpdated: { connect: (cb: (service: string, member: string, value: unknown) => void) => void };
|
|
3546
|
+
anQstBridge_slotInvocationRequested: {
|
|
3547
|
+
connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
|
|
3548
|
+
};
|
|
3549
|
+
anQstBridge_hostDiagnostic?: { connect: (cb: (payload: unknown) => void) => void };
|
|
3550
|
+
anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
3551
|
+
anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
3552
|
+
anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
interface QWebChannelCtor {
|
|
3556
|
+
new (
|
|
3557
|
+
transport: unknown,
|
|
3558
|
+
initCallback: (channel: { objects: Record<string, HostBridgeApi | undefined> }) => void
|
|
3559
|
+
): unknown;
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
interface BridgeAdapter {
|
|
3563
|
+
readonly transport: AnQstBridgeTransport;
|
|
3564
|
+
call<T>(service: string, member: string, args: unknown[]): Promise<T>;
|
|
3565
|
+
emit(service: string, member: string, args: unknown[]): void;
|
|
3566
|
+
setInput(service: string, member: string, value: unknown): void;
|
|
3567
|
+
registerSlot(service: string, member: string): void;
|
|
3568
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
3569
|
+
onOutput(handler: OutputListener): void;
|
|
3570
|
+
onSlotInvocation(handler: SlotInvocationListener): void;
|
|
3571
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void;
|
|
3572
|
+
onDisconnected(handler: DisconnectListener): void;
|
|
3573
|
+
onDrop(handler: DropListener): void;
|
|
3574
|
+
onHover(handler: HoverListener): void;
|
|
3575
|
+
onHoverLeft(handler: HoverLeftListener): void;
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
interface ValueCell<T> {
|
|
3579
|
+
get(): T;
|
|
3580
|
+
set(value: T): void;
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
function createValueCell<T>(initial: T): ValueCell<T> {
|
|
3584
|
+
let current = initial;
|
|
3585
|
+
return {
|
|
3586
|
+
get(): T {
|
|
3587
|
+
return current;
|
|
3588
|
+
},
|
|
3589
|
+
set(value: T): void {
|
|
3590
|
+
current = value;
|
|
3591
|
+
}
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
function errorMessage(error: unknown): string {
|
|
3596
|
+
if (error instanceof Error && typeof error.message === "string" && error.message.length > 0) {
|
|
3597
|
+
return error.message;
|
|
3598
|
+
}
|
|
3599
|
+
return String(error);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
function normalizeSeverity(value: unknown): AnQstBridgeSeverity {
|
|
3603
|
+
if (value === "info" || value === "warn" || value === "error" || value === "fatal") {
|
|
3604
|
+
return value;
|
|
3605
|
+
}
|
|
3606
|
+
return "error";
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
3610
|
+
if (value === null || typeof value !== "object") {
|
|
3611
|
+
return undefined;
|
|
3612
|
+
}
|
|
3613
|
+
return value as Record<string, unknown>;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
3617
|
+
const value = record?.[key];
|
|
3618
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
function readBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
|
|
3622
|
+
const value = record?.[key];
|
|
3623
|
+
return typeof value === "boolean" ? value : undefined;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
function readContext(record: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
3627
|
+
const context = asRecord(record?.["context"]);
|
|
3628
|
+
return context === undefined ? undefined : context;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function normalizeHostDiagnostic(payload: unknown, transport: AnQstBridgeTransport): Omit<AnQstBridgeDiagnostic, "timestamp"> {
|
|
3632
|
+
const row = asRecord(payload);
|
|
3633
|
+
if (row === undefined) {
|
|
3634
|
+
return {
|
|
3635
|
+
code: "HostDiagnosticMalformed",
|
|
3636
|
+
severity: "error",
|
|
3637
|
+
category: "bridge",
|
|
3638
|
+
recoverable: true,
|
|
3639
|
+
message: "Host emitted a malformed diagnostic payload.",
|
|
3640
|
+
source: "host",
|
|
3641
|
+
transport
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
const context = readContext(row);
|
|
3646
|
+
return {
|
|
3647
|
+
code: readString(row, "code") ?? "HostDiagnostic",
|
|
3648
|
+
severity: normalizeSeverity(row["severity"]),
|
|
3649
|
+
category: readString(row, "category") ?? "bridge",
|
|
3650
|
+
recoverable: readBoolean(row, "recoverable") ?? true,
|
|
3651
|
+
message: readString(row, "message") ?? "Host emitted a diagnostic payload.",
|
|
3652
|
+
source: "host",
|
|
3653
|
+
transport,
|
|
3654
|
+
service: readString(row, "service") ?? readString(context, "service"),
|
|
3655
|
+
member: readString(row, "member") ?? readString(context, "member"),
|
|
3656
|
+
requestId: readString(row, "requestId") ?? readString(context, "requestId"),
|
|
3657
|
+
context
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
function isBridgeCallError(value: unknown): value is {
|
|
3662
|
+
code: unknown;
|
|
3663
|
+
message: unknown;
|
|
3664
|
+
service: unknown;
|
|
3665
|
+
member: unknown;
|
|
3666
|
+
requestId: unknown;
|
|
3667
|
+
} {
|
|
3668
|
+
if (value === null || typeof value !== "object") return false;
|
|
3669
|
+
const row = value as Record<string, unknown>;
|
|
3670
|
+
return (
|
|
3671
|
+
Object.prototype.hasOwnProperty.call(row, "code")
|
|
3672
|
+
&& Object.prototype.hasOwnProperty.call(row, "message")
|
|
3673
|
+
&& Object.prototype.hasOwnProperty.call(row, "service")
|
|
3674
|
+
&& Object.prototype.hasOwnProperty.call(row, "member")
|
|
3675
|
+
&& Object.prototype.hasOwnProperty.call(row, "requestId")
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
class QtWebChannelAdapter implements BridgeAdapter {
|
|
3680
|
+
readonly transport = "qt-webchannel" as const;
|
|
3681
|
+
|
|
3682
|
+
private constructor(private readonly host: HostBridgeApi) {}
|
|
3683
|
+
|
|
3684
|
+
static async create(): Promise<QtWebChannelAdapter> {
|
|
3685
|
+
const anyWindow = window as unknown as {
|
|
3686
|
+
qt?: { webChannelTransport?: unknown };
|
|
3687
|
+
QWebChannel?: QWebChannelCtor;
|
|
3688
|
+
};
|
|
3689
|
+
if (typeof anyWindow.QWebChannel !== "function" || anyWindow.qt?.webChannelTransport === undefined) {
|
|
3690
|
+
throw new Error("Qt WebChannel transport is unavailable.");
|
|
3691
|
+
}
|
|
3692
|
+
return await new Promise<QtWebChannelAdapter>((resolve, reject) => {
|
|
3693
|
+
try {
|
|
3694
|
+
const QWebChannel = anyWindow.QWebChannel as QWebChannelCtor;
|
|
3695
|
+
new QWebChannel(anyWindow.qt!.webChannelTransport, (channel) => {
|
|
3696
|
+
try {
|
|
3697
|
+
const host = channel.objects["${spec.widgetName}Bridge"];
|
|
3698
|
+
if (host === undefined) {
|
|
3699
|
+
reject(new Error("${spec.widgetName}Bridge bridge object is unavailable."));
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
resolve(new QtWebChannelAdapter(host));
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3707
|
+
} catch (error) {
|
|
3708
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3714
|
+
return new Promise<T>((resolve, reject) => {
|
|
3715
|
+
this.host.anQstBridge_call(service, member, args, (result) => {
|
|
3716
|
+
if (isBridgeCallError(result)) {
|
|
3717
|
+
reject(result);
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
resolve(result as T);
|
|
3721
|
+
});
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3726
|
+
this.host.anQstBridge_emit(service, member, args);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3730
|
+
this.host.anQstBridge_setInput(service, member, value);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
registerSlot(service: string, member: string): void {
|
|
3734
|
+
this.host.anQstBridge_registerSlot(service, member);
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
3738
|
+
this.host.anQstBridge_resolveSlot(requestId, ok, payload, error);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
onOutput(handler: OutputListener): void {
|
|
3742
|
+
this.host.anQstBridge_outputUpdated.connect(handler);
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
3746
|
+
this.host.anQstBridge_slotInvocationRequested.connect(handler);
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
3750
|
+
this.host.anQstBridge_hostDiagnostic?.connect(handler);
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
onDisconnected(_handler: DisconnectListener): void {
|
|
3754
|
+
// QWebChannel does not expose a deterministic disconnect event here.
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
onDrop(handler: DropListener): void {
|
|
3758
|
+
this.host.anQstBridge_dropReceived.connect(handler);
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
onHover(handler: HoverListener): void {
|
|
3762
|
+
this.host.anQstBridge_hoverUpdated.connect(handler);
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
onHoverLeft(handler: HoverLeftListener): void {
|
|
3766
|
+
this.host.anQstBridge_hoverLeft.connect(handler);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
3771
|
+
readonly transport = "dev-websocket" as const;
|
|
3772
|
+
private readonly pending = new Map<string, {
|
|
3773
|
+
service: string;
|
|
3774
|
+
member: string;
|
|
3775
|
+
requestId: string;
|
|
3776
|
+
resolve: (result: unknown) => void;
|
|
3777
|
+
reject: (error: unknown) => void;
|
|
3778
|
+
}>();
|
|
3779
|
+
private readonly outputListeners: OutputListener[] = [];
|
|
3780
|
+
private readonly slotListeners: SlotInvocationListener[] = [];
|
|
3781
|
+
private readonly hostDiagnosticListeners: HostDiagnosticListener[] = [];
|
|
3782
|
+
private readonly disconnectListeners: DisconnectListener[] = [];
|
|
3783
|
+
private readonly dropListeners: DropListener[] = [];
|
|
3784
|
+
private readonly hoverListeners: HoverListener[] = [];
|
|
3785
|
+
private readonly hoverLeftListeners: HoverLeftListener[] = [];
|
|
3786
|
+
private requestCounter = 0;
|
|
3787
|
+
|
|
3788
|
+
private constructor(private readonly socket: WebSocket) {
|
|
3789
|
+
this.socket.addEventListener("message", (event) => {
|
|
3790
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
3791
|
+
const message = JSON.parse(raw) as Record<string, unknown>;
|
|
3792
|
+
const type = String(message["type"] ?? "");
|
|
3793
|
+
if (type === "callResult") {
|
|
3794
|
+
const requestId = String(message["requestId"] ?? "");
|
|
3795
|
+
const pending = this.pending.get(requestId);
|
|
3796
|
+
if (pending) {
|
|
3797
|
+
this.pending.delete(requestId);
|
|
3798
|
+
const result = message["result"];
|
|
3799
|
+
if (isBridgeCallError(result)) {
|
|
3800
|
+
pending.reject(result);
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
pending.resolve(result);
|
|
3804
|
+
}
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
if (type === "outputUpdated") {
|
|
3808
|
+
const service = String(message["service"] ?? "");
|
|
3809
|
+
const member = String(message["member"] ?? "");
|
|
3810
|
+
for (const listener of this.outputListeners) {
|
|
3811
|
+
listener(service, member, message["value"]);
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
if (type === "slotInvocationRequested") {
|
|
3816
|
+
const requestId = String(message["requestId"] ?? "");
|
|
3817
|
+
const service = String(message["service"] ?? "");
|
|
3818
|
+
const member = String(message["member"] ?? "");
|
|
3819
|
+
const args = Array.isArray(message["args"]) ? (message["args"] as unknown[]) : [];
|
|
3820
|
+
for (const listener of this.slotListeners) {
|
|
3821
|
+
listener(requestId, service, member, args);
|
|
3822
|
+
}
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
if (type === "dropReceived") {
|
|
3826
|
+
const service = String(message["service"] ?? "");
|
|
3827
|
+
const member = String(message["member"] ?? "");
|
|
3828
|
+
const x = Number(message["x"] ?? 0);
|
|
3829
|
+
const y = Number(message["y"] ?? 0);
|
|
3830
|
+
for (const listener of this.dropListeners) {
|
|
3831
|
+
listener(service, member, message["payload"], x, y);
|
|
3832
|
+
}
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (type === "hoverUpdated") {
|
|
3836
|
+
const service = String(message["service"] ?? "");
|
|
3837
|
+
const member = String(message["member"] ?? "");
|
|
3838
|
+
const x = Number(message["x"] ?? 0);
|
|
3839
|
+
const y = Number(message["y"] ?? 0);
|
|
3840
|
+
for (const listener of this.hoverListeners) {
|
|
3841
|
+
listener(service, member, message["payload"], x, y);
|
|
3842
|
+
}
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
if (type === "hoverLeft") {
|
|
3846
|
+
const service = String(message["service"] ?? "");
|
|
3847
|
+
const member = String(message["member"] ?? "");
|
|
3848
|
+
for (const listener of this.hoverLeftListeners) {
|
|
3849
|
+
listener(service, member);
|
|
3850
|
+
}
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
if (type === "hostError") {
|
|
3854
|
+
for (const listener of this.hostDiagnosticListeners) {
|
|
3855
|
+
listener(message["payload"]);
|
|
3856
|
+
}
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
if (type === "widgetReattached") {
|
|
3860
|
+
document.body.textContent = "Widget Reattached";
|
|
3861
|
+
this.socket.close();
|
|
3862
|
+
}
|
|
3863
|
+
});
|
|
3864
|
+
this.socket.addEventListener("close", () => {
|
|
3865
|
+
for (const pending of this.pending.values()) {
|
|
3866
|
+
pending.reject({
|
|
3867
|
+
code: "BridgeDisconnectedError",
|
|
3868
|
+
message: "Bridge disconnected before call completion.",
|
|
3869
|
+
service: pending.service,
|
|
3870
|
+
member: pending.member,
|
|
3871
|
+
requestId: pending.requestId
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
this.pending.clear();
|
|
3875
|
+
for (const listener of this.disconnectListeners) {
|
|
3876
|
+
listener();
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
static async create(): Promise<WebSocketBridgeAdapter> {
|
|
3882
|
+
const configResponse = await fetch("/anqst-dev-config.json", { cache: "no-store" });
|
|
3883
|
+
if (!configResponse.ok) {
|
|
3884
|
+
throw new Error("AnQst host bootstrap missing: unable to read /anqst-dev-config.json");
|
|
3885
|
+
}
|
|
3886
|
+
const config = (await configResponse.json()) as { wsUrl?: string; wsPath?: string };
|
|
3887
|
+
let wsUrl = config.wsUrl;
|
|
3888
|
+
if (!wsUrl && config.wsPath) {
|
|
3889
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
3890
|
+
wsUrl = protocol + "//" + window.location.host + config.wsPath;
|
|
3891
|
+
}
|
|
3892
|
+
if (!wsUrl) {
|
|
3893
|
+
throw new Error("AnQst host bootstrap missing: wsUrl/wsPath is unavailable.");
|
|
3894
|
+
}
|
|
3895
|
+
if (wsUrl.startsWith("http://")) {
|
|
3896
|
+
wsUrl = "ws://" + wsUrl.slice("http://".length);
|
|
3897
|
+
} else if (wsUrl.startsWith("https://")) {
|
|
3898
|
+
wsUrl = "wss://" + wsUrl.slice("https://".length);
|
|
3899
|
+
}
|
|
3900
|
+
return await new Promise<WebSocketBridgeAdapter>((resolve, reject) => {
|
|
3901
|
+
const socket = new WebSocket(wsUrl!);
|
|
3902
|
+
socket.addEventListener("open", () => resolve(new WebSocketBridgeAdapter(socket)));
|
|
3903
|
+
socket.addEventListener("error", () => reject(new Error("Failed to connect to AnQst WebSocket bridge.")));
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3908
|
+
const requestId = \`req-\${++this.requestCounter}\`;
|
|
3909
|
+
const payload = { type: "call", requestId, service, member, args };
|
|
3910
|
+
return await new Promise<T>((resolve, reject) => {
|
|
3911
|
+
this.pending.set(requestId, {
|
|
3912
|
+
service,
|
|
3913
|
+
member,
|
|
3914
|
+
requestId,
|
|
3915
|
+
resolve: (value) => resolve(value as T),
|
|
3916
|
+
reject
|
|
3917
|
+
});
|
|
3918
|
+
this.socket.send(JSON.stringify(payload));
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3923
|
+
this.socket.send(JSON.stringify({ type: "emit", service, member, args }));
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3927
|
+
this.socket.send(JSON.stringify({ type: "setInput", service, member, value }));
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
registerSlot(service: string, member: string): void {
|
|
3931
|
+
this.socket.send(JSON.stringify({ type: "registerSlot", service, member }));
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
3935
|
+
this.socket.send(JSON.stringify({ type: "resolveSlot", requestId, ok, payload, error }));
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
onOutput(handler: OutputListener): void {
|
|
3939
|
+
this.outputListeners.push(handler);
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
3943
|
+
this.slotListeners.push(handler);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
3947
|
+
this.hostDiagnosticListeners.push(handler);
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
onDisconnected(handler: DisconnectListener): void {
|
|
3951
|
+
this.disconnectListeners.push(handler);
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
onDrop(handler: DropListener): void {
|
|
3955
|
+
this.dropListeners.push(handler);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
onHover(handler: HoverListener): void {
|
|
3959
|
+
this.hoverListeners.push(handler);
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
onHoverLeft(handler: HoverLeftListener): void {
|
|
3963
|
+
this.hoverLeftListeners.push(handler);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
class AnQstBridgeRuntime {
|
|
3968
|
+
private static readonly maxDiagnostics = 50;
|
|
3969
|
+
private adapter: BridgeAdapter | null = null;
|
|
3970
|
+
private readonly slotHandlers = new Map<string, SlotHandler>();
|
|
3971
|
+
private readonly outputHandlers = new Map<string, OutputHandler[]>();
|
|
3972
|
+
private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
3973
|
+
private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
3974
|
+
private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
|
|
3975
|
+
private readonly diagnosticListeners = new Set<(diagnostic: AnQstBridgeDiagnostic) => void>();
|
|
3976
|
+
private readonly _diagnostics = createValueCell<readonly AnQstBridgeDiagnostic[]>([]);
|
|
3977
|
+
private readonly _state = createValueCell<AnQstBridgeState>("starting");
|
|
3978
|
+
private readonly startup = this.init().catch((error) => {
|
|
3979
|
+
this._state.set("failed");
|
|
3980
|
+
this.reportFrontendDiagnostic({
|
|
3981
|
+
code: "BridgeBootstrapError",
|
|
3982
|
+
severity: "fatal",
|
|
3983
|
+
category: "bridge",
|
|
3984
|
+
recoverable: false,
|
|
3985
|
+
message: \`Failed to initialize bridge: \${errorMessage(error)}\`
|
|
3986
|
+
});
|
|
3987
|
+
throw error;
|
|
3988
|
+
});
|
|
3989
|
+
|
|
3990
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
3991
|
+
return this._diagnostics.get();
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
state(): AnQstBridgeState {
|
|
3995
|
+
return this._state.get();
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
subscribeDiagnostics(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
3999
|
+
this.diagnosticListeners.add(listener);
|
|
4000
|
+
return () => this.diagnosticListeners.delete(listener);
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
async ready(): Promise<void> {
|
|
4004
|
+
return this.startup;
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
reportFrontendDiagnostic(diagnostic: Omit<AnQstBridgeDiagnostic, "timestamp" | "source">): void {
|
|
4008
|
+
this.pushDiagnostic({
|
|
4009
|
+
...diagnostic,
|
|
4010
|
+
source: "frontend",
|
|
4011
|
+
transport: diagnostic.transport ?? this.adapter?.transport,
|
|
4012
|
+
timestamp: new Date().toISOString()
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
4017
|
+
const adapter = await this.requireAdapter();
|
|
4018
|
+
return adapter.call<T>(service, member, args);
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
4022
|
+
this.publishNonCall("Emitter", service, member, (adapter) => adapter.emit(service, member, args));
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
4026
|
+
this.publishNonCall("Input", service, member, (adapter) => adapter.setInput(service, member, value));
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
registerSlot(service: string, member: string, handler: SlotHandler): void {
|
|
4030
|
+
const key = this.key(service, member);
|
|
4031
|
+
this.slotHandlers.set(key, handler);
|
|
4032
|
+
if (this.adapter !== null) {
|
|
4033
|
+
try {
|
|
4034
|
+
this.adapter.registerSlot(service, member);
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
this.reportFrontendDiagnostic({
|
|
4037
|
+
code: "BridgePublishError",
|
|
4038
|
+
severity: "error",
|
|
4039
|
+
category: "bridge",
|
|
4040
|
+
recoverable: true,
|
|
4041
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4042
|
+
service,
|
|
4043
|
+
member,
|
|
4044
|
+
context: { interaction: "Slot" }
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
this.ready()
|
|
4050
|
+
.then(() => {
|
|
4051
|
+
try {
|
|
4052
|
+
this.requireAdapterSync().registerSlot(service, member);
|
|
4053
|
+
} catch (error) {
|
|
4054
|
+
this.reportFrontendDiagnostic({
|
|
4055
|
+
code: "BridgePublishError",
|
|
4056
|
+
severity: "error",
|
|
4057
|
+
category: "bridge",
|
|
4058
|
+
recoverable: true,
|
|
4059
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4060
|
+
service,
|
|
4061
|
+
member,
|
|
4062
|
+
context: { interaction: "Slot" }
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
})
|
|
4066
|
+
.catch((error) => {
|
|
4067
|
+
this.reportFrontendDiagnostic({
|
|
4068
|
+
code: "BridgePublishError",
|
|
4069
|
+
severity: "error",
|
|
4070
|
+
category: "bridge",
|
|
4071
|
+
recoverable: true,
|
|
4072
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4073
|
+
service,
|
|
4074
|
+
member,
|
|
4075
|
+
context: { interaction: "Slot" }
|
|
4076
|
+
});
|
|
4077
|
+
});
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
onOutput(service: string, member: string, handler: OutputHandler): void {
|
|
4081
|
+
const key = this.key(service, member);
|
|
4082
|
+
const existing = this.outputHandlers.get(key) ?? [];
|
|
4083
|
+
existing.push(handler);
|
|
4084
|
+
this.outputHandlers.set(key, existing);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
onDrop(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
|
|
4088
|
+
const key = this.key(service, member);
|
|
4089
|
+
const existing = this.dropHandlers.get(key) ?? [];
|
|
4090
|
+
existing.push(handler);
|
|
4091
|
+
this.dropHandlers.set(key, existing);
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
onHover(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
|
|
4095
|
+
const key = this.key(service, member);
|
|
4096
|
+
const existing = this.hoverHandlers.get(key) ?? [];
|
|
4097
|
+
existing.push(handler);
|
|
4098
|
+
this.hoverHandlers.set(key, existing);
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
onHoverLeft(service: string, member: string, handler: () => void): void {
|
|
4102
|
+
const key = this.key(service, member);
|
|
4103
|
+
const existing = this.hoverLeftHandlers.get(key) ?? [];
|
|
4104
|
+
existing.push(handler);
|
|
4105
|
+
this.hoverLeftHandlers.set(key, existing);
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
private requireAdapterSync(): BridgeAdapter {
|
|
4109
|
+
if (this.adapter === null) {
|
|
4110
|
+
throw new Error("AnQst bridge is not ready.");
|
|
4111
|
+
}
|
|
4112
|
+
return this.adapter;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
private async requireAdapter(): Promise<BridgeAdapter> {
|
|
4116
|
+
await this.startup;
|
|
4117
|
+
return this.requireAdapterSync();
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
private pushDiagnostic(diagnostic: AnQstBridgeDiagnostic): void {
|
|
4121
|
+
const previous = this._diagnostics.get();
|
|
4122
|
+
const trimmed = previous.length >= AnQstBridgeRuntime.maxDiagnostics
|
|
4123
|
+
? previous.slice(previous.length - (AnQstBridgeRuntime.maxDiagnostics - 1))
|
|
4124
|
+
: previous;
|
|
4125
|
+
const next = [...trimmed, diagnostic];
|
|
4126
|
+
this._diagnostics.set(next);
|
|
4127
|
+
for (const listener of this.diagnosticListeners) {
|
|
4128
|
+
listener(diagnostic);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
private publishNonCall(
|
|
4133
|
+
interaction: "Emitter" | "Input",
|
|
4134
|
+
service: string,
|
|
4135
|
+
member: string,
|
|
4136
|
+
publish: (adapter: BridgeAdapter) => void
|
|
4137
|
+
): void {
|
|
4138
|
+
if (this.adapter !== null) {
|
|
4139
|
+
try {
|
|
4140
|
+
publish(this.adapter);
|
|
4141
|
+
} catch (error) {
|
|
4142
|
+
this.reportFrontendDiagnostic({
|
|
4143
|
+
code: "BridgePublishError",
|
|
4144
|
+
severity: "error",
|
|
4145
|
+
category: "bridge",
|
|
4146
|
+
recoverable: true,
|
|
4147
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4148
|
+
service,
|
|
4149
|
+
member,
|
|
4150
|
+
context: { interaction }
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
this.ready()
|
|
4157
|
+
.then(() => {
|
|
4158
|
+
try {
|
|
4159
|
+
publish(this.requireAdapterSync());
|
|
4160
|
+
} catch (error) {
|
|
4161
|
+
this.reportFrontendDiagnostic({
|
|
4162
|
+
code: "BridgePublishError",
|
|
4163
|
+
severity: "error",
|
|
4164
|
+
category: "bridge",
|
|
4165
|
+
recoverable: true,
|
|
4166
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4167
|
+
service,
|
|
4168
|
+
member,
|
|
4169
|
+
context: { interaction }
|
|
4170
|
+
});
|
|
4171
|
+
}
|
|
4172
|
+
})
|
|
4173
|
+
.catch((error) => {
|
|
4174
|
+
this.reportFrontendDiagnostic({
|
|
4175
|
+
code: "BridgePublishError",
|
|
4176
|
+
severity: "error",
|
|
4177
|
+
category: "bridge",
|
|
4178
|
+
recoverable: true,
|
|
4179
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4180
|
+
service,
|
|
4181
|
+
member,
|
|
4182
|
+
context: { interaction }
|
|
4183
|
+
});
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
private async init(): Promise<void> {
|
|
4188
|
+
const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
|
|
4189
|
+
if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
|
|
4190
|
+
this.adapter = await QtWebChannelAdapter.create();
|
|
4191
|
+
} else {
|
|
4192
|
+
this.adapter = await WebSocketBridgeAdapter.create();
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
const adapter = this.adapter;
|
|
4196
|
+
adapter.onHostDiagnostic((payload) => {
|
|
4197
|
+
this.pushDiagnostic({
|
|
4198
|
+
...normalizeHostDiagnostic(payload, adapter.transport),
|
|
4199
|
+
timestamp: new Date().toISOString()
|
|
4200
|
+
});
|
|
4201
|
+
});
|
|
4202
|
+
adapter.onDisconnected(() => {
|
|
4203
|
+
this._state.set("disconnected");
|
|
4204
|
+
this.reportFrontendDiagnostic({
|
|
4205
|
+
code: "BridgeDisconnectedError",
|
|
4206
|
+
severity: "error",
|
|
4207
|
+
category: "bridge",
|
|
4208
|
+
recoverable: true,
|
|
4209
|
+
message: "Bridge disconnected.",
|
|
4210
|
+
transport: adapter.transport
|
|
4211
|
+
});
|
|
4212
|
+
});
|
|
4213
|
+
|
|
4214
|
+
adapter.onOutput((service, member, value) => {
|
|
4215
|
+
const key = this.key(service, member);
|
|
4216
|
+
for (const outputHandler of this.outputHandlers.get(key) ?? []) {
|
|
4217
|
+
outputHandler(value);
|
|
4218
|
+
}
|
|
4219
|
+
});
|
|
4220
|
+
adapter.onSlotInvocation(async (requestId, service, member, args) => {
|
|
4221
|
+
const key = this.key(service, member);
|
|
4222
|
+
const handler = this.slotHandlers.get(key);
|
|
4223
|
+
if (handler === undefined) {
|
|
4224
|
+
this.reportFrontendDiagnostic({
|
|
4225
|
+
code: "HandlerNotRegisteredError",
|
|
4226
|
+
severity: "error",
|
|
4227
|
+
category: "bridge",
|
|
4228
|
+
recoverable: true,
|
|
4229
|
+
message: \`No slot handler registered for \${service}.\${member}.\`,
|
|
4230
|
+
service,
|
|
4231
|
+
member,
|
|
4232
|
+
requestId,
|
|
4233
|
+
context: { interaction: "Slot" }
|
|
4234
|
+
});
|
|
4235
|
+
adapter.resolveSlot(requestId, false, undefined, "No slot handler registered.");
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
try {
|
|
4239
|
+
const result = await Promise.resolve(handler(...args));
|
|
4240
|
+
if (result instanceof Error) {
|
|
4241
|
+
this.reportFrontendDiagnostic({
|
|
4242
|
+
code: "SlotRequestFailed",
|
|
4243
|
+
severity: "error",
|
|
4244
|
+
category: "bridge",
|
|
4245
|
+
recoverable: true,
|
|
4246
|
+
message: result.message.length > 0
|
|
4247
|
+
? result.message
|
|
4248
|
+
: \`Slot \${service}.\${member} returned an Error.\`,
|
|
4249
|
+
service,
|
|
4250
|
+
member,
|
|
4251
|
+
requestId,
|
|
4252
|
+
context: { interaction: "Slot" }
|
|
4253
|
+
});
|
|
4254
|
+
adapter.resolveSlot(requestId, false, undefined, result.message);
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
adapter.resolveSlot(requestId, true, result, "");
|
|
4258
|
+
} catch (error) {
|
|
4259
|
+
const message = errorMessage(error);
|
|
4260
|
+
this.reportFrontendDiagnostic({
|
|
4261
|
+
code: "SlotHandlerError",
|
|
4262
|
+
severity: "error",
|
|
4263
|
+
category: "bridge",
|
|
4264
|
+
recoverable: true,
|
|
4265
|
+
message: \`Slot handler \${service}.\${member} threw: \${message}\`,
|
|
4266
|
+
service,
|
|
4267
|
+
member,
|
|
4268
|
+
requestId,
|
|
4269
|
+
context: { interaction: "Slot" }
|
|
4270
|
+
});
|
|
4271
|
+
adapter.resolveSlot(requestId, false, undefined, message);
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
adapter.onDrop((service, member, payload, x, y) => {
|
|
4275
|
+
const key = this.key(service, member);
|
|
4276
|
+
for (const handler of this.dropHandlers.get(key) ?? []) {
|
|
4277
|
+
handler(payload, x, y);
|
|
4278
|
+
}
|
|
4279
|
+
});
|
|
4280
|
+
adapter.onHover((service, member, payload, x, y) => {
|
|
4281
|
+
const key = this.key(service, member);
|
|
4282
|
+
for (const handler of this.hoverHandlers.get(key) ?? []) {
|
|
4283
|
+
handler(payload, x, y);
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
4286
|
+
adapter.onHoverLeft((service, member) => {
|
|
4287
|
+
const key = this.key(service, member);
|
|
4288
|
+
for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
|
|
4289
|
+
handler();
|
|
4290
|
+
}
|
|
4291
|
+
});
|
|
4292
|
+
for (const key of this.slotHandlers.keys()) {
|
|
4293
|
+
const parts = key.split("::");
|
|
4294
|
+
if (parts.length === 2) {
|
|
4295
|
+
adapter.registerSlot(parts[0], parts[1]);
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
this._state.set("ready");
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
private key(service: string, member: string): string {
|
|
4302
|
+
return \`\${service}::\${member}\`;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
class AnQstBridgeDiagnostics {
|
|
4307
|
+
constructor(private readonly _bridge: AnQstBridgeRuntime) {}
|
|
4308
|
+
|
|
4309
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
4310
|
+
return this._bridge.diagnostics();
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
state(): AnQstBridgeState {
|
|
4314
|
+
return this._bridge.state();
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
4318
|
+
return this._bridge.subscribeDiagnostics(listener);
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
${serviceClasses}
|
|
4323
|
+
interface ${spec.widgetName}Frontend {
|
|
4324
|
+
${frontendShapeLines.join("\n")}
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
async function createFrontend(): Promise<${spec.widgetName}Frontend> {
|
|
4328
|
+
const bridge = new AnQstBridgeRuntime();
|
|
4329
|
+
await bridge.ready();
|
|
4330
|
+
return {
|
|
4331
|
+
${frontendFactoryLines.join(",\n")}
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
(function bootstrapAnQstGenerated(global: typeof globalThis & { AnQstGenerated?: Record<string, unknown> }) {
|
|
4336
|
+
const root = global.AnQstGenerated ?? (global.AnQstGenerated = {});
|
|
4337
|
+
root["${spec.widgetName}"] = {
|
|
4338
|
+
createFrontend
|
|
4339
|
+
};
|
|
4340
|
+
})(window as typeof globalThis & { AnQstGenerated?: Record<string, unknown> });
|
|
4341
|
+
${exportsBlock}`;
|
|
4342
|
+
}
|
|
4343
|
+
function renderVanillaServiceDts(spec, serviceName) {
|
|
4344
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
4345
|
+
const setMembers = [];
|
|
4346
|
+
const onSlotMembers = [];
|
|
4347
|
+
const classMembers = [];
|
|
4348
|
+
const setInterfaceName = `${serviceName}Set`;
|
|
4349
|
+
const onSlotInterfaceName = `${serviceName}OnSlot`;
|
|
4350
|
+
for (const m of members) {
|
|
4351
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
4352
|
+
if (m.kind === "Call") {
|
|
4353
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
4354
|
+
classMembers.push(` ${m.name}(${args}): Promise<${ret}>;`);
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
if (m.kind === "Emitter") {
|
|
4358
|
+
classMembers.push(` ${m.name}(${args}): void;`);
|
|
4359
|
+
continue;
|
|
4360
|
+
}
|
|
4361
|
+
if (m.kind === "Slot") {
|
|
4362
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
4363
|
+
onSlotMembers.push(` ${m.name}(handler: (${args}) => ${slotHandlerReturnType(ret)}): void;`);
|
|
4364
|
+
continue;
|
|
4365
|
+
}
|
|
4366
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
4367
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4368
|
+
classMembers.push(` ${m.name}(): ${tsType} | undefined;`);
|
|
4369
|
+
if (m.kind === "Input") {
|
|
4370
|
+
setMembers.push(` ${m.name}(value: ${tsType}): void;`);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
4374
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4375
|
+
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
4376
|
+
}
|
|
4377
|
+
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
4378
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4379
|
+
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
const interfaceBlocks = [];
|
|
4383
|
+
if (setMembers.length > 0) {
|
|
4384
|
+
interfaceBlocks.push(`interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`);
|
|
4385
|
+
}
|
|
4386
|
+
if (onSlotMembers.length > 0) {
|
|
4387
|
+
interfaceBlocks.push(`interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`);
|
|
4388
|
+
}
|
|
4389
|
+
const interfaceSection = interfaceBlocks.length > 0 ? `${interfaceBlocks.join("\n\n")}\n\n` : "";
|
|
4390
|
+
const namespaceLines = [];
|
|
4391
|
+
if (setMembers.length > 0) {
|
|
4392
|
+
namespaceLines.push(` readonly set: ${setInterfaceName};`);
|
|
4393
|
+
}
|
|
4394
|
+
if (onSlotMembers.length > 0) {
|
|
4395
|
+
namespaceLines.push(` readonly onSlot: ${onSlotInterfaceName};`);
|
|
4396
|
+
}
|
|
4397
|
+
const declareBodyLines = [...namespaceLines, ...classMembers];
|
|
4398
|
+
return `${interfaceSection}declare class ${serviceName} {
|
|
4399
|
+
${declareBodyLines.join("\n")}
|
|
4400
|
+
}`;
|
|
4401
|
+
}
|
|
4402
|
+
function renderVanillaIndexDts(spec) {
|
|
4403
|
+
const valueClasses = collectVanillaValueClasses(spec);
|
|
4404
|
+
const valueClassNames = new Set(valueClasses.map((model) => model.name));
|
|
4405
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}/index.d.ts`, valueClassNames).trim();
|
|
4406
|
+
// Export widget namespace types so other packages can `import type { ... }` from this declaration file
|
|
4407
|
+
// (e.g. a second widget spec that reuses structs generated for the first widget).
|
|
4408
|
+
const localTypeDecls = renderTypeDeclarations(spec, true).trim();
|
|
4409
|
+
const valueClassDecls = valueClasses.map((model) => renderVanillaValueClassDts(model)).join("\n\n");
|
|
4410
|
+
const serviceDecls = spec.services.map((s) => renderVanillaServiceDts(spec, s.name)).join("\n\n");
|
|
4411
|
+
const frontendShapeLines = [
|
|
4412
|
+
" diagnostics: AnQstBridgeDiagnostics;",
|
|
4413
|
+
...spec.services.map((s) => ` ${s.name}: ${s.name};`),
|
|
4414
|
+
...valueClasses.map((model) => ` ${model.name}: typeof ${model.name};`)
|
|
4415
|
+
];
|
|
4416
|
+
const sections = [externalTypeImports, localTypeDecls, valueClassDecls].filter((s) => s.length > 0);
|
|
4417
|
+
const prelude = sections.length > 0 ? `${sections.join("\n\n")}\n\n` : "";
|
|
4418
|
+
const exportedRuntimeSymbols = [
|
|
4419
|
+
"AnQstBridgeDiagnostics",
|
|
4420
|
+
...spec.services.map((s) => s.name),
|
|
4421
|
+
...valueClasses.map((model) => model.name),
|
|
4422
|
+
"createFrontend"
|
|
4423
|
+
];
|
|
4424
|
+
const exportedTypeSymbols = [
|
|
4425
|
+
"AnQstBridgeDiagnostic",
|
|
4426
|
+
"AnQstBridgeState",
|
|
4427
|
+
`${spec.widgetName}Frontend`,
|
|
4428
|
+
`${spec.widgetName}Global`,
|
|
4429
|
+
"AnQstGeneratedRoot"
|
|
4430
|
+
];
|
|
4431
|
+
return `export {};
|
|
4432
|
+
${prelude}type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
4433
|
+
|
|
4434
|
+
type AnQstBridgeSource = "frontend" | "host";
|
|
4435
|
+
|
|
4436
|
+
type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
4437
|
+
|
|
4438
|
+
type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
4439
|
+
|
|
4440
|
+
interface AnQstBridgeDiagnostic {
|
|
4441
|
+
code: string;
|
|
4442
|
+
severity: AnQstBridgeSeverity;
|
|
4443
|
+
category: string;
|
|
4444
|
+
recoverable: boolean;
|
|
4445
|
+
message: string;
|
|
4446
|
+
timestamp: string;
|
|
4447
|
+
source: AnQstBridgeSource;
|
|
4448
|
+
transport?: AnQstBridgeTransport;
|
|
4449
|
+
service?: string;
|
|
4450
|
+
member?: string;
|
|
4451
|
+
requestId?: string;
|
|
4452
|
+
context?: Record<string, unknown>;
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
declare class AnQstBridgeDiagnostics {
|
|
4456
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[];
|
|
4457
|
+
state(): AnQstBridgeState;
|
|
4458
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void;
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
${serviceDecls}
|
|
4462
|
+
|
|
4463
|
+
interface ${spec.widgetName}Frontend {
|
|
4464
|
+
${frontendShapeLines.join("\n")}
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
declare function createFrontend(): Promise<${spec.widgetName}Frontend>;
|
|
4468
|
+
|
|
4469
|
+
interface ${spec.widgetName}Global {
|
|
4470
|
+
createFrontend(): Promise<${spec.widgetName}Frontend>;
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
interface AnQstGeneratedRoot {
|
|
4474
|
+
${spec.widgetName}: ${spec.widgetName}Global;
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
declare global {
|
|
4478
|
+
interface Window {
|
|
4479
|
+
AnQstGenerated: AnQstGeneratedRoot;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
var AnQstGenerated: AnQstGeneratedRoot;
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
export { ${exportedRuntimeSymbols.join(", ")} };
|
|
4486
|
+
export type { ${exportedTypeSymbols.join(", ")} };
|
|
4487
|
+
`;
|
|
4488
|
+
}
|
|
4489
|
+
function transpileBrowserTsToJs(source) {
|
|
4490
|
+
return typescript_1.default.transpileModule(source, {
|
|
4491
|
+
compilerOptions: {
|
|
4492
|
+
target: typescript_1.default.ScriptTarget.ES2018,
|
|
4493
|
+
module: typescript_1.default.ModuleKind.None,
|
|
4494
|
+
importsNotUsedAsValues: typescript_1.default.ImportsNotUsedAsValues.Remove
|
|
4495
|
+
}
|
|
4496
|
+
}).outputText;
|
|
4497
|
+
}
|
|
4498
|
+
function renderVanillaPackage(spec, target) {
|
|
4499
|
+
const packageJson = {
|
|
4500
|
+
name: `${spec.widgetName.toLowerCase()}-${target.toLowerCase()}-generated`,
|
|
4501
|
+
version: "0.1.0",
|
|
4502
|
+
private: true,
|
|
4503
|
+
main: "index.js",
|
|
4504
|
+
anqst: {
|
|
4505
|
+
widget: spec.widgetName,
|
|
4506
|
+
services: spec.services.map((s) => s.name),
|
|
4507
|
+
target,
|
|
4508
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport,
|
|
4509
|
+
outputContractVersion: 2
|
|
4510
|
+
}
|
|
4511
|
+
};
|
|
4512
|
+
if (target === "VanillaTS") {
|
|
4513
|
+
packageJson.types = "index.d.ts";
|
|
4514
|
+
}
|
|
4515
|
+
return JSON.stringify(packageJson, null, 2);
|
|
4516
|
+
}
|
|
4517
|
+
function renderNodeExpressWsPackage(spec) {
|
|
4518
|
+
return JSON.stringify({
|
|
4519
|
+
name: `${spec.widgetName.toLowerCase()}-node-express-ws-generated`,
|
|
4520
|
+
version: "0.1.0",
|
|
4521
|
+
private: true,
|
|
4522
|
+
types: "types/index.d.ts",
|
|
4523
|
+
main: "index.ts",
|
|
4524
|
+
exports: {
|
|
4525
|
+
".": {
|
|
4526
|
+
types: "./types/index.d.ts",
|
|
4527
|
+
default: "./index.ts"
|
|
4528
|
+
}
|
|
4529
|
+
},
|
|
4530
|
+
anqst: {
|
|
4531
|
+
widget: spec.widgetName,
|
|
4532
|
+
services: spec.services.map((s) => s.name),
|
|
4533
|
+
target: "node_express_ws"
|
|
4534
|
+
}
|
|
4535
|
+
}, null, 2);
|
|
4536
|
+
}
|
|
4537
|
+
function nodeParamTuple(member) {
|
|
4538
|
+
if (member.parameters.length === 0)
|
|
4539
|
+
return "[]";
|
|
4540
|
+
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
4541
|
+
}
|
|
4542
|
+
function nodeParamArgs(member) {
|
|
4543
|
+
return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
4544
|
+
}
|
|
4545
|
+
function nodeParamValues(member) {
|
|
4546
|
+
if (member.parameters.length === 0)
|
|
4547
|
+
return "[]";
|
|
4548
|
+
return `[${member.parameters.map((p) => p.name).join(", ")}]`;
|
|
4549
|
+
}
|
|
4550
|
+
function nodeCap(value) {
|
|
4551
|
+
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
4552
|
+
}
|
|
4553
|
+
function renderNodeExpressWsTypes(spec) {
|
|
4554
|
+
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
|
|
4555
|
+
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
4556
|
+
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
4557
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
4558
|
+
}
|
|
4559
|
+
function renderNodeExpressWsIndex(spec, codecCatalog) {
|
|
4560
|
+
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
4561
|
+
const typeDecls = renderTypeDeclarations(spec, true);
|
|
4562
|
+
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
4563
|
+
const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
|
|
4564
|
+
const handlerInterfaces = spec.services
|
|
4565
|
+
.map((service) => {
|
|
4566
|
+
const lines = [];
|
|
4567
|
+
for (const member of service.members) {
|
|
4568
|
+
const args = nodeParamArgs(member);
|
|
4569
|
+
const prefixedArgs = args.length > 0 ? `, ${args}` : "";
|
|
4570
|
+
if (member.kind === "Call" && member.payloadTypeText) {
|
|
4571
|
+
const ret = mapTypeTextToTs(member.payloadTypeText);
|
|
4572
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
|
|
4573
|
+
}
|
|
4574
|
+
else if (member.kind === "Emitter") {
|
|
4575
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
|
|
4576
|
+
}
|
|
4577
|
+
else if (member.kind === "Input" && member.payloadTypeText) {
|
|
4578
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}, value: ${mapTypeTextToTs(member.payloadTypeText)}): void | Promise<void>;`);
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
return `export interface ${service.name}NodeHandlers {\n${lines.join("\n")}\n}`;
|
|
4582
|
+
})
|
|
4583
|
+
.join("\n\n");
|
|
4584
|
+
const implementationFields = spec.services.map((service) => ` ${service.name}: ${service.name}NodeHandlers;`).join("\n");
|
|
3220
4585
|
const slotHelpers = spec.services
|
|
3221
4586
|
.flatMap((service) => service.members
|
|
3222
4587
|
.filter((member) => member.kind === "Slot")
|
|
@@ -3936,25 +5301,50 @@ function generatedCppLibraryDirName(widgetName) {
|
|
|
3936
5301
|
function generatedNodeExpressWsDirName(widgetName) {
|
|
3937
5302
|
return (0, layout_1.generatedNodeExpressDirName)(widgetName);
|
|
3938
5303
|
}
|
|
3939
|
-
function generateOutputs(spec, options = {
|
|
3940
|
-
const
|
|
5304
|
+
function generateOutputs(spec, options = {}) {
|
|
5305
|
+
const useDefaultBrowserTargets = Object.keys(options).length === 0;
|
|
5306
|
+
const normalizedOptions = {
|
|
5307
|
+
emitQWidget: options.emitQWidget ?? true,
|
|
5308
|
+
emitAngularService: options.emitAngularService ?? true,
|
|
5309
|
+
emitVanillaTS: options.emitVanillaTS ?? useDefaultBrowserTargets,
|
|
5310
|
+
emitVanillaJS: options.emitVanillaJS ?? useDefaultBrowserTargets,
|
|
5311
|
+
emitNodeExpressWs: options.emitNodeExpressWs ?? false
|
|
5312
|
+
};
|
|
5313
|
+
const angularFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}`;
|
|
5314
|
+
const vanillaTsFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}`;
|
|
5315
|
+
const vanillaJsFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaJS")}`;
|
|
3941
5316
|
const cppDir = `backend/cpp/qt/${generatedCppLibraryDirName(spec.widgetName)}`;
|
|
3942
5317
|
const nodeDir = `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}`;
|
|
3943
5318
|
const outputs = {};
|
|
3944
5319
|
const codecCatalog = (0, boundary_codecs_1.buildBoundaryCodecCatalog)(spec);
|
|
3945
|
-
if (
|
|
3946
|
-
outputs[`${
|
|
3947
|
-
outputs[`${
|
|
3948
|
-
outputs[`${
|
|
3949
|
-
outputs[`${
|
|
3950
|
-
outputs[`${
|
|
3951
|
-
outputs[`${
|
|
3952
|
-
outputs[`${
|
|
3953
|
-
outputs[`${
|
|
3954
|
-
outputs[`${
|
|
3955
|
-
outputs[`${
|
|
3956
|
-
}
|
|
3957
|
-
if (
|
|
5320
|
+
if (normalizedOptions.emitAngularService) {
|
|
5321
|
+
outputs[`${angularFrontendDir}/package.json`] = renderNpmPackage(spec);
|
|
5322
|
+
outputs[`${angularFrontendDir}/index.ts`] = renderTsIndex();
|
|
5323
|
+
outputs[`${angularFrontendDir}/services.ts`] = renderTsServices(spec, codecCatalog);
|
|
5324
|
+
outputs[`${angularFrontendDir}/types.ts`] = renderTsTypes(spec);
|
|
5325
|
+
outputs[`${angularFrontendDir}/index.js`] = renderJsIndex();
|
|
5326
|
+
outputs[`${angularFrontendDir}/services.js`] = renderJsServices();
|
|
5327
|
+
outputs[`${angularFrontendDir}/types.js`] = renderJsTypes();
|
|
5328
|
+
outputs[`${angularFrontendDir}/types/index.d.ts`] = renderTypeRootIndexDts(spec);
|
|
5329
|
+
outputs[`${angularFrontendDir}/types/services.d.ts`] = renderTypeServicesDts(spec);
|
|
5330
|
+
outputs[`${angularFrontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
|
|
5331
|
+
}
|
|
5332
|
+
if (normalizedOptions.emitVanillaTS || normalizedOptions.emitVanillaJS) {
|
|
5333
|
+
const vanillaBrowserTs = renderVanillaBrowserTs(spec, codecCatalog, true);
|
|
5334
|
+
const vanillaBrowserScriptTs = renderVanillaBrowserTs(spec, codecCatalog, false);
|
|
5335
|
+
const vanillaBrowserJs = transpileBrowserTsToJs(vanillaBrowserScriptTs);
|
|
5336
|
+
if (normalizedOptions.emitVanillaTS) {
|
|
5337
|
+
outputs[`${vanillaTsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaTS");
|
|
5338
|
+
outputs[`${vanillaTsFrontendDir}/index.ts`] = vanillaBrowserTs;
|
|
5339
|
+
outputs[`${vanillaTsFrontendDir}/index.js`] = vanillaBrowserJs;
|
|
5340
|
+
outputs[`${vanillaTsFrontendDir}/index.d.ts`] = renderVanillaIndexDts(spec);
|
|
5341
|
+
}
|
|
5342
|
+
if (normalizedOptions.emitVanillaJS) {
|
|
5343
|
+
outputs[`${vanillaJsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaJS");
|
|
5344
|
+
outputs[`${vanillaJsFrontendDir}/index.js`] = vanillaBrowserJs;
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
if (normalizedOptions.emitQWidget) {
|
|
3958
5348
|
const cppTypes = buildCppTypeContext(spec);
|
|
3959
5349
|
outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
|
|
3960
5350
|
outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
|
|
@@ -3963,7 +5353,7 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
|
|
|
3963
5353
|
outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
|
|
3964
5354
|
outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes, codecCatalog);
|
|
3965
5355
|
}
|
|
3966
|
-
if (
|
|
5356
|
+
if (normalizedOptions.emitNodeExpressWs) {
|
|
3967
5357
|
outputs[`${nodeDir}/package.json`] = renderNodeExpressWsPackage(spec);
|
|
3968
5358
|
outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec, codecCatalog);
|
|
3969
5359
|
outputs[`${nodeDir}/types/index.d.ts`] = renderNodeExpressWsTypes(spec);
|
|
@@ -3978,6 +5368,52 @@ function writeGeneratedOutputs(cwd, outputs) {
|
|
|
3978
5368
|
node_fs_1.default.writeFileSync(filePath, withBuildStamp(relPath, content), "utf8");
|
|
3979
5369
|
}
|
|
3980
5370
|
}
|
|
5371
|
+
function normalizeVanillaJsIndexHtml(html) {
|
|
5372
|
+
let normalized = html.replace(/<script\b[^>]*src=["'](?:\.\/)?main\.js["'][^>]*>\s*<\/script>\s*/gi, "");
|
|
5373
|
+
const bundleScriptTag = ' <script defer src="./main.js"></script>\n';
|
|
5374
|
+
if (/<\/head>/i.test(normalized)) {
|
|
5375
|
+
normalized = normalized.replace(/<\/head>/i, `${bundleScriptTag}</head>`);
|
|
5376
|
+
}
|
|
5377
|
+
else if (/<\/body>/i.test(normalized)) {
|
|
5378
|
+
normalized = normalized.replace(/<\/body>/i, `${bundleScriptTag}</body>`);
|
|
5379
|
+
}
|
|
5380
|
+
else {
|
|
5381
|
+
normalized = `${normalized}\n${bundleScriptTag}`;
|
|
5382
|
+
}
|
|
5383
|
+
return normalized;
|
|
5384
|
+
}
|
|
5385
|
+
function buildVanillaJsBrowserBundle(cwd, widgetName) {
|
|
5386
|
+
const srcRoot = node_path_1.default.join(cwd, "src");
|
|
5387
|
+
const srcIndexPath = node_path_1.default.join(srcRoot, "index.html");
|
|
5388
|
+
const srcMainPath = node_path_1.default.join(srcRoot, "main.js");
|
|
5389
|
+
const generatedRuntimePath = node_path_1.default.join((0, layout_1.resolveGeneratedLayoutPaths)(cwd, widgetName).vanillaJsFrontendRoot, "index.js");
|
|
5390
|
+
if (!node_fs_1.default.existsSync(srcIndexPath)) {
|
|
5391
|
+
throw new Error("VanillaJS build requires src/index.html.");
|
|
5392
|
+
}
|
|
5393
|
+
if (!node_fs_1.default.existsSync(srcMainPath)) {
|
|
5394
|
+
throw new Error("VanillaJS build requires src/main.js.");
|
|
5395
|
+
}
|
|
5396
|
+
if (!node_fs_1.default.existsSync(generatedRuntimePath)) {
|
|
5397
|
+
throw new Error("VanillaJS generated runtime is missing. Run generation before packaging the browser bundle.");
|
|
5398
|
+
}
|
|
5399
|
+
const distWebRoot = node_path_1.default.join(cwd, "dist", "browser");
|
|
5400
|
+
node_fs_1.default.rmSync(distWebRoot, { recursive: true, force: true });
|
|
5401
|
+
node_fs_1.default.mkdirSync(distWebRoot, { recursive: true });
|
|
5402
|
+
copyDirectoryRecursive(srcRoot, distWebRoot);
|
|
5403
|
+
const generatedRuntime = node_fs_1.default.readFileSync(generatedRuntimePath, "utf8").trimEnd();
|
|
5404
|
+
const appMain = node_fs_1.default.readFileSync(srcMainPath, "utf8").trim();
|
|
5405
|
+
const bundleSource = `${generatedRuntime}
|
|
5406
|
+
|
|
5407
|
+
${appMain}
|
|
5408
|
+
|
|
5409
|
+
void main(window, document, window.AnQstGenerated);
|
|
5410
|
+
`;
|
|
5411
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(distWebRoot, "main.js"), withBuildStamp("dist/browser/main.js", bundleSource), "utf8");
|
|
5412
|
+
const sourceIndexHtml = node_fs_1.default.readFileSync(srcIndexPath, "utf8");
|
|
5413
|
+
const normalizedIndexHtml = normalizeVanillaJsIndexHtml(sourceIndexHtml);
|
|
5414
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(distWebRoot, "index.html"), withBuildStamp("dist/browser/index.html", normalizedIndexHtml), "utf8");
|
|
5415
|
+
return distWebRoot;
|
|
5416
|
+
}
|
|
3981
5417
|
function listFilesRecursively(rootDir) {
|
|
3982
5418
|
const output = [];
|
|
3983
5419
|
const queue = [rootDir];
|