@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/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.set(d.name, d);
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 && requiredSymbols.has(imp.defaultImport) ? imp.defaultImport : null;
538
- const named = imp.namedImports.filter((n) => requiredSymbols.has(n.localName));
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
- `static QByteArray encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload);`,
1188
- `static std::optional<${helper.cppType}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
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.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
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.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
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 ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
1207
- const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
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 setInterfaceDecl = setMembers.length > 0
2245
- ? `export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`
2246
- : `export interface ${setInterfaceName} {}`;
2247
- const onSlotInterfaceDecl = onSlotMembers.length > 0
2248
- ? `export interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`
2249
- : `export interface ${onSlotInterfaceName} {}`;
2250
- const classMemberBlock = classMembers.length > 0 ? `\n${classMembers.join("\n")}` : "";
2251
- return `${setInterfaceDecl}
2252
-
2253
- ${onSlotInterfaceDecl}
2254
-
2255
- export declare class ${serviceName} {
2256
- readonly set: ${setInterfaceName};
2257
- readonly onSlot: ${onSlotInterfaceName};${classMemberBlock}
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 renderNodeExpressWsPackage(spec) {
3153
- return JSON.stringify({
3154
- name: `${spec.widgetName.toLowerCase()}-node-express-ws-generated`,
3155
- version: "0.1.0",
3156
- private: true,
3157
- types: "types/index.d.ts",
3158
- main: "index.ts",
3159
- exports: {
3160
- ".": {
3161
- types: "./types/index.d.ts",
3162
- default: "./index.ts"
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
- anqst: {
3166
- widget: spec.widgetName,
3167
- services: spec.services.map((s) => s.name),
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
- }, null, 2);
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
- function nodeParamTuple(member) {
3173
- if (member.parameters.length === 0)
3174
- return "[]";
3175
- return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
3378
+ `;
3176
3379
  }
3177
- function nodeParamArgs(member) {
3178
- return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
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 nodeParamValues(member) {
3181
- if (member.parameters.length === 0)
3182
- return "[]";
3183
- return `[${member.parameters.map((p) => p.name).join(", ")}]`;
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 nodeCap(value) {
3186
- return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
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
- function renderNodeExpressWsTypes(spec) {
3189
- const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
3190
- const typeDecls = renderTypeDeclarations(spec, true).trim();
3191
- const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
3192
- return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
3464
+
3465
+ declare const ${model.name}: {
3466
+ new (${constructorArgs}): ${model.name};
3467
+ prototype: ${model.name};
3468
+ };`;
3193
3469
  }
3194
- function renderNodeExpressWsIndex(spec, codecCatalog) {
3195
- const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
3196
- const typeDecls = renderTypeDeclarations(spec, true);
3197
- const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
3198
- const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
3199
- const handlerInterfaces = spec.services
3200
- .map((service) => {
3201
- const lines = [];
3202
- for (const member of service.members) {
3203
- const args = nodeParamArgs(member);
3204
- const prefixedArgs = args.length > 0 ? `, ${args}` : "";
3205
- if (member.kind === "Call" && member.payloadTypeText) {
3206
- const ret = mapTypeTextToTs(member.payloadTypeText);
3207
- lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
3208
- }
3209
- else if (member.kind === "Emitter") {
3210
- lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
3211
- }
3212
- else if (member.kind === "Input" && member.payloadTypeText) {
3213
- lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}, value: ${mapTypeTextToTs(member.payloadTypeText)}): void | Promise<void>;`);
3214
- }
3215
- }
3216
- return `export interface ${service.name}NodeHandlers {\n${lines.join("\n")}\n}`;
3217
- })
3218
- .join("\n\n");
3219
- const implementationFields = spec.services.map((service) => ` ${service.name}: ${service.name}NodeHandlers;`).join("\n");
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 = { emitQWidget: true, emitAngularService: true, emitNodeExpressWs: false }) {
3940
- const frontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}`;
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 (options.emitAngularService) {
3946
- outputs[`${frontendDir}/package.json`] = renderNpmPackage(spec);
3947
- outputs[`${frontendDir}/index.ts`] = renderTsIndex();
3948
- outputs[`${frontendDir}/services.ts`] = renderTsServices(spec, codecCatalog);
3949
- outputs[`${frontendDir}/types.ts`] = renderTsTypes(spec);
3950
- outputs[`${frontendDir}/index.js`] = renderJsIndex();
3951
- outputs[`${frontendDir}/services.js`] = renderJsServices();
3952
- outputs[`${frontendDir}/types.js`] = renderJsTypes();
3953
- outputs[`${frontendDir}/types/index.d.ts`] = renderTypeRootIndexDts(spec);
3954
- outputs[`${frontendDir}/types/services.d.ts`] = renderTypeServicesDts(spec);
3955
- outputs[`${frontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
3956
- }
3957
- if (options.emitQWidget) {
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 (options.emitNodeExpressWs) {
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];