@dusted/anqst 0.1.0 → 0.1.2

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,12 +5,14 @@ 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.installTypeScriptOutputs = installTypeScriptOutputs;
9
8
  exports.installEmbeddedWebBundle = installEmbeddedWebBundle;
10
9
  exports.installQtIntegrationCMake = installQtIntegrationCMake;
10
+ exports.installQtDesignerPluginCMake = installQtDesignerPluginCMake;
11
11
  const node_fs_1 = __importDefault(require("node:fs"));
12
12
  const node_path_1 = __importDefault(require("node:path"));
13
13
  const typescript_1 = __importDefault(require("typescript"));
14
+ const pngjs_1 = require("pngjs");
15
+ const layout_1 = require("./layout");
14
16
  function stripAnQstType(typeText) {
15
17
  return typeText
16
18
  .replace(/\bAnQst\.Type\.stringArray\b/g, "string[]")
@@ -158,6 +160,87 @@ function cppToVariantExpression(cppType, expr) {
158
160
  }
159
161
  return `QVariant::fromValue(${expr})`;
160
162
  }
163
+ function splitTopLevelTemplateArgs(text) {
164
+ const args = [];
165
+ let start = 0;
166
+ let depth = 0;
167
+ for (let i = 0; i < text.length; i++) {
168
+ const ch = text[i];
169
+ if (ch === "<") {
170
+ depth++;
171
+ continue;
172
+ }
173
+ if (ch === ">") {
174
+ depth--;
175
+ continue;
176
+ }
177
+ if (ch === "," && depth === 0) {
178
+ args.push(text.slice(start, i).trim());
179
+ start = i + 1;
180
+ }
181
+ }
182
+ const tail = text.slice(start).trim();
183
+ if (tail.length > 0) {
184
+ args.push(tail);
185
+ }
186
+ return args;
187
+ }
188
+ function templateTypeArgs(cppType, containerName) {
189
+ const trimmed = cppType.trim();
190
+ const prefix = `${containerName}<`;
191
+ if (!trimmed.startsWith(prefix) || !trimmed.endsWith(">")) {
192
+ return null;
193
+ }
194
+ const inner = trimmed.slice(prefix.length, -1).trim();
195
+ if (inner.length === 0)
196
+ return [];
197
+ return splitTopLevelTemplateArgs(inner);
198
+ }
199
+ function isNumericCppType(cppType) {
200
+ return [
201
+ "double",
202
+ "qint64",
203
+ "quint64",
204
+ "qint32",
205
+ "quint32",
206
+ "qint16",
207
+ "quint16",
208
+ "qint8",
209
+ "quint8",
210
+ "int8_t",
211
+ "uint8_t",
212
+ "int16_t",
213
+ "uint16_t",
214
+ "int32_t",
215
+ "uint32_t"
216
+ ].includes(cppType);
217
+ }
218
+ function designerPlaceholderCppExpression(cppType, memberName) {
219
+ const escapedMember = escapeCppStringLiteral(memberName);
220
+ const stringLiteral = `QStringLiteral("${escapedMember} value")`;
221
+ if (cppType === "QString")
222
+ return stringLiteral;
223
+ if (cppType === "bool")
224
+ return "true";
225
+ if (isNumericCppType(cppType))
226
+ return `static_cast<${cppType}>(1)`;
227
+ if (cppType === "QStringList")
228
+ return `QStringList{${stringLiteral}}`;
229
+ if (cppType === "QVariantMap") {
230
+ return `QVariantMap{{QStringLiteral("${escapedMember}"), QVariant(${stringLiteral})}}`;
231
+ }
232
+ const optionalArgs = templateTypeArgs(cppType, "std::optional");
233
+ if (optionalArgs && optionalArgs.length === 1) {
234
+ const inner = optionalArgs[0];
235
+ return `std::optional<${inner}>{${designerPlaceholderCppExpression(inner, memberName)}}`;
236
+ }
237
+ const listArgs = templateTypeArgs(cppType, "QList");
238
+ if (listArgs && listArgs.length === 1) {
239
+ const inner = listArgs[0];
240
+ return `QList<${inner}>{${designerPlaceholderCppExpression(inner, memberName)}}`;
241
+ }
242
+ return `${cppType}{}`;
243
+ }
161
244
  function collectStructDecls(spec) {
162
245
  const out = new Map();
163
246
  for (const d of spec.namespaceTypeDecls)
@@ -266,7 +349,7 @@ function normalizeImportPathForGenerated(specFilePath, generatedFileRelPath, mod
266
349
  return moduleSpecifier;
267
350
  }
268
351
  const specDir = node_path_1.default.dirname(specFilePath);
269
- const generatedAbs = node_path_1.default.resolve(node_path_1.default.dirname(specFilePath), "generated_output", generatedFileRelPath);
352
+ const generatedAbs = node_path_1.default.resolve(node_path_1.default.dirname(specFilePath), "generated", generatedFileRelPath);
270
353
  const generatedDir = node_path_1.default.dirname(generatedAbs);
271
354
  const resolvedModulePath = node_path_1.default.resolve(specDir, moduleSpecifier);
272
355
  const relative = node_path_1.default.relative(generatedDir, resolvedModulePath);
@@ -746,7 +829,12 @@ function renderCppStub(spec, cppTypes) {
746
829
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
747
830
  const pascal = pascalCase(member.name);
748
831
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
749
- lines.push(` if (!m_${member.name}Handler) return QVariant();`);
832
+ lines.push(` if (!m_${member.name}Handler) {`);
833
+ lines.push(` if (property("anqstDesignerContext").toBool()) {`);
834
+ lines.push(` return ${cppToVariantExpression(cppType, designerPlaceholderCppExpression(cppType, member.name))};`);
835
+ lines.push(` }`);
836
+ lines.push(` return QVariant();`);
837
+ lines.push(` }`);
750
838
  for (let i = 0; i < member.parameters.length; i++) {
751
839
  const p = member.parameters[i];
752
840
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
@@ -913,6 +1001,57 @@ function escapeXml(text) {
913
1001
  function normalizeSlashes(value) {
914
1002
  return value.split(node_path_1.default.sep).join("/");
915
1003
  }
1004
+ function resolveActiveBuildStamp() {
1005
+ const fromEnv = process.env.ANQST_BUILD_STAMP?.trim();
1006
+ if (fromEnv && fromEnv.length > 0) {
1007
+ return fromEnv;
1008
+ }
1009
+ const activePath = node_path_1.default.resolve(__dirname, "..", "..", ".anqstgen-version-active.json");
1010
+ if (!node_fs_1.default.existsSync(activePath)) {
1011
+ return "";
1012
+ }
1013
+ try {
1014
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(activePath, "utf8"));
1015
+ if (typeof parsed.active === "string" && parsed.active.trim().length > 0) {
1016
+ return parsed.active.trim();
1017
+ }
1018
+ }
1019
+ catch {
1020
+ return "";
1021
+ }
1022
+ return "";
1023
+ }
1024
+ function withBuildStamp(relativePath, content) {
1025
+ const stamp = resolveActiveBuildStamp();
1026
+ if (!stamp) {
1027
+ return content;
1028
+ }
1029
+ const marker = `Built by AnQst ${stamp}`;
1030
+ const rel = normalizeSlashes(relativePath);
1031
+ const lower = rel.toLowerCase();
1032
+ if (lower.endsWith(".json")) {
1033
+ try {
1034
+ const parsed = JSON.parse(content);
1035
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1036
+ const next = { "//": marker, ...parsed };
1037
+ return `${JSON.stringify(next, null, 2)}\n`;
1038
+ }
1039
+ }
1040
+ catch {
1041
+ // If JSON parsing fails, fall through to plain comment prefix.
1042
+ }
1043
+ }
1044
+ if (lower.endsWith(".qrc") || lower.endsWith(".xml") || lower.endsWith(".html")) {
1045
+ return `<!-- ${marker} -->\n${content}`;
1046
+ }
1047
+ if (lower.endsWith(".cmake")) {
1048
+ return `# ${marker}\n${content}`;
1049
+ }
1050
+ if (lower.endsWith(".h") || lower.endsWith(".cpp") || lower.endsWith(".ts") || lower.endsWith(".js") || lower.endsWith(".d.ts")) {
1051
+ return `// ${marker}\n${content}`;
1052
+ }
1053
+ return `# ${marker}\n${content}`;
1054
+ }
916
1055
  function renderEmbeddedQrc(widgetName, embeddedWebFiles) {
917
1056
  const files = [...embeddedWebFiles].sort();
918
1057
  const lines = [];
@@ -1087,7 +1226,7 @@ export declare class ${serviceName} {
1087
1226
  }
1088
1227
  function renderTsServices(spec) {
1089
1228
  const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name)).join("\n");
1090
- const externalTypeImports = renderRequiredTypeImports(spec, "npmpackage/services.ts").trim();
1229
+ const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
1091
1230
  const localTypeImports = renderLocalTypeImports(spec).trim();
1092
1231
  const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
1093
1232
  const typeImportsBlock = typeImports.length > 0 ? `${typeImports}\n\n` : "";
@@ -1231,6 +1370,11 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1231
1370
  }
1232
1371
  if (type === "hostError") {
1233
1372
  console.error("AnQst host error:", message["payload"]);
1373
+ return;
1374
+ }
1375
+ if (type === "widgetReattached") {
1376
+ document.body.textContent = "Widget Reattached";
1377
+ this.socket.close();
1234
1378
  }
1235
1379
  });
1236
1380
  }
@@ -1240,12 +1384,22 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1240
1384
  if (!configResponse.ok) {
1241
1385
  throw new Error("AnQst host bootstrap missing: unable to read /anqst-dev-config.json");
1242
1386
  }
1243
- const config = (await configResponse.json()) as { wsUrl?: string };
1244
- if (!config.wsUrl) {
1245
- throw new Error("AnQst host bootstrap missing: wsUrl is unavailable.");
1387
+ const config = (await configResponse.json()) as { wsUrl?: string; wsPath?: string };
1388
+ let wsUrl = config.wsUrl;
1389
+ if (!wsUrl && config.wsPath) {
1390
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
1391
+ wsUrl = protocol + "//" + window.location.host + config.wsPath;
1392
+ }
1393
+ if (!wsUrl) {
1394
+ throw new Error("AnQst host bootstrap missing: wsUrl/wsPath is unavailable.");
1395
+ }
1396
+ if (wsUrl.startsWith("http://")) {
1397
+ wsUrl = "ws://" + wsUrl.slice("http://".length);
1398
+ } else if (wsUrl.startsWith("https://")) {
1399
+ wsUrl = "wss://" + wsUrl.slice("https://".length);
1246
1400
  }
1247
1401
  return await new Promise<WebSocketBridgeAdapter>((resolve, reject) => {
1248
- const socket = new WebSocket(config.wsUrl!);
1402
+ const socket = new WebSocket(wsUrl!);
1249
1403
  socket.addEventListener("open", () => resolve(new WebSocketBridgeAdapter(socket)));
1250
1404
  socket.addEventListener("error", () => reject(new Error("Failed to connect to AnQst WebSocket bridge.")));
1251
1405
  });
@@ -1397,13 +1551,13 @@ ${serviceClasses}
1397
1551
  `;
1398
1552
  }
1399
1553
  function renderTsTypes(spec) {
1400
- const typeImports = renderRequiredTypeImports(spec, "npmpackage/types.ts").trim();
1554
+ const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types.ts`).trim();
1401
1555
  const typeDecls = renderTypeDeclarations(spec, true).trim();
1402
1556
  const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
1403
1557
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
1404
1558
  }
1405
1559
  function renderTypeServicesDts(spec) {
1406
- const externalTypeImports = renderRequiredTypeImports(spec, "npmpackage/types/services.d.ts").trim();
1560
+ const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
1407
1561
  const localTypeImports = renderLocalTypeImports(spec).trim();
1408
1562
  const serviceDecls = spec.services
1409
1563
  .map((s) => renderTsServiceDts(spec, s.name))
@@ -1412,7 +1566,7 @@ function renderTypeServicesDts(spec) {
1412
1566
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
1413
1567
  }
1414
1568
  function renderTypeTypesDts(spec) {
1415
- const typeImports = renderRequiredTypeImports(spec, "npmpackage/types/types.d.ts").trim();
1569
+ const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/types.d.ts`).trim();
1416
1570
  const typeDecls = renderTypeDeclarations(spec, true).trim();
1417
1571
  const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
1418
1572
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
@@ -1478,13 +1632,13 @@ function nodeCap(value) {
1478
1632
  return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
1479
1633
  }
1480
1634
  function renderNodeExpressWsTypes(spec) {
1481
- const typeImports = renderRequiredTypeImports(spec, `${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
1635
+ const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
1482
1636
  const typeDecls = renderTypeDeclarations(spec, true).trim();
1483
1637
  const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
1484
1638
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
1485
1639
  }
1486
1640
  function renderNodeExpressWsIndex(spec) {
1487
- const typeImports = renderRequiredTypeImports(spec, `${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
1641
+ const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
1488
1642
  const typeDecls = renderTypeDeclarations(spec, true);
1489
1643
  const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
1490
1644
  const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
@@ -2183,26 +2337,27 @@ function renderTypeRootIndexDts(spec) {
2183
2337
  return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
2184
2338
  }
2185
2339
  function generatedCppLibraryDirName(widgetName) {
2186
- return `${widgetName}_QtWidget`;
2340
+ return (0, layout_1.generatedQtWidgetDirName)(widgetName);
2187
2341
  }
2188
2342
  function generatedNodeExpressWsDirName(widgetName) {
2189
- return `${widgetName}_node_express_ws`;
2343
+ return (0, layout_1.generatedNodeExpressDirName)(widgetName);
2190
2344
  }
2191
2345
  function generateOutputs(spec, options = { emitQWidget: true, emitAngularService: true, emitNodeExpressWs: false }) {
2192
- const cppDir = generatedCppLibraryDirName(spec.widgetName);
2193
- const nodeDir = generatedNodeExpressWsDirName(spec.widgetName);
2346
+ const frontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}`;
2347
+ const cppDir = `backend/cpp/qt/${generatedCppLibraryDirName(spec.widgetName)}`;
2348
+ const nodeDir = `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}`;
2194
2349
  const outputs = {};
2195
2350
  if (options.emitAngularService) {
2196
- outputs["npmpackage/package.json"] = renderNpmPackage(spec);
2197
- outputs["npmpackage/index.ts"] = renderTsIndex();
2198
- outputs["npmpackage/services.ts"] = renderTsServices(spec);
2199
- outputs["npmpackage/types.ts"] = renderTsTypes(spec);
2200
- outputs["npmpackage/index.js"] = renderJsIndex();
2201
- outputs["npmpackage/services.js"] = renderJsServices();
2202
- outputs["npmpackage/types.js"] = renderJsTypes();
2203
- outputs["npmpackage/types/index.d.ts"] = renderTypeRootIndexDts(spec);
2204
- outputs["npmpackage/types/services.d.ts"] = renderTypeServicesDts(spec);
2205
- outputs["npmpackage/types/types.d.ts"] = renderTypeTypesDts(spec);
2351
+ outputs[`${frontendDir}/package.json`] = renderNpmPackage(spec);
2352
+ outputs[`${frontendDir}/index.ts`] = renderTsIndex();
2353
+ outputs[`${frontendDir}/services.ts`] = renderTsServices(spec);
2354
+ outputs[`${frontendDir}/types.ts`] = renderTsTypes(spec);
2355
+ outputs[`${frontendDir}/index.js`] = renderJsIndex();
2356
+ outputs[`${frontendDir}/services.js`] = renderJsServices();
2357
+ outputs[`${frontendDir}/types.js`] = renderJsTypes();
2358
+ outputs[`${frontendDir}/types/index.d.ts`] = renderTypeRootIndexDts(spec);
2359
+ outputs[`${frontendDir}/types/services.d.ts`] = renderTypeServicesDts(spec);
2360
+ outputs[`${frontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
2206
2361
  }
2207
2362
  if (options.emitQWidget) {
2208
2363
  const cppTypes = buildCppTypeContext(spec);
@@ -2220,36 +2375,11 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
2220
2375
  return outputs;
2221
2376
  }
2222
2377
  function writeGeneratedOutputs(cwd, outputs) {
2223
- const outputRoot = node_path_1.default.join(cwd, "generated_output");
2378
+ const outputRoot = (0, layout_1.anqstGeneratedRootDir)(cwd);
2224
2379
  for (const [relPath, content] of Object.entries(outputs)) {
2225
2380
  const filePath = node_path_1.default.join(outputRoot, relPath);
2226
2381
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
2227
- node_fs_1.default.writeFileSync(filePath, content, "utf8");
2228
- }
2229
- }
2230
- function installTypeScriptOutputs(cwd) {
2231
- const sourceDir = node_path_1.default.join(cwd, "generated_output", "npmpackage");
2232
- const targetDir = node_path_1.default.join(cwd, "src", "anqst-generated");
2233
- if (!node_fs_1.default.existsSync(sourceDir))
2234
- return;
2235
- node_fs_1.default.rmSync(targetDir, { recursive: true, force: true });
2236
- node_fs_1.default.mkdirSync(targetDir, { recursive: true });
2237
- const queue = [sourceDir];
2238
- while (queue.length > 0) {
2239
- const current = queue.shift();
2240
- for (const entry of node_fs_1.default.readdirSync(current, { withFileTypes: true })) {
2241
- const abs = node_path_1.default.join(current, entry.name);
2242
- const rel = node_path_1.default.relative(sourceDir, abs);
2243
- const dst = node_path_1.default.join(targetDir, rel);
2244
- if (entry.isDirectory()) {
2245
- node_fs_1.default.mkdirSync(dst, { recursive: true });
2246
- queue.push(abs);
2247
- }
2248
- else if (entry.isFile()) {
2249
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(dst), { recursive: true });
2250
- node_fs_1.default.copyFileSync(abs, dst);
2251
- }
2252
- }
2382
+ node_fs_1.default.writeFileSync(filePath, withBuildStamp(relPath, content), "utf8");
2253
2383
  }
2254
2384
  }
2255
2385
  function listFilesRecursively(rootDir) {
@@ -2385,7 +2515,7 @@ function installEmbeddedWebBundle(cwd, widgetName) {
2385
2515
  if (!node_fs_1.default.existsSync(node_path_1.default.join(distWebRoot, "index.html"))) {
2386
2516
  return false;
2387
2517
  }
2388
- const cppLibraryRoot = node_path_1.default.join(cwd, "generated_output", generatedCppLibraryDirName(widgetName));
2518
+ const cppLibraryRoot = (0, layout_1.resolveGeneratedLayoutPaths)(cwd, widgetName).cppQtWidgetRoot;
2389
2519
  const cppLibraryWebRoot = node_path_1.default.join(cppLibraryRoot, "webapp");
2390
2520
  node_fs_1.default.rmSync(cppLibraryWebRoot, { recursive: true, force: true });
2391
2521
  node_fs_1.default.mkdirSync(cppLibraryWebRoot, { recursive: true });
@@ -2393,7 +2523,7 @@ function installEmbeddedWebBundle(cwd, widgetName) {
2393
2523
  normalizeEmbeddedIndexHtml(node_path_1.default.join(cppLibraryWebRoot, "index.html"), cppLibraryWebRoot);
2394
2524
  const embeddedFiles = listFilesRecursively(cppLibraryWebRoot);
2395
2525
  const qrcPath = node_path_1.default.join(cppLibraryRoot, `${widgetName}.qrc`);
2396
- node_fs_1.default.writeFileSync(qrcPath, renderEmbeddedQrc(widgetName, embeddedFiles), "utf8");
2526
+ node_fs_1.default.writeFileSync(qrcPath, withBuildStamp(`${widgetName}.qrc`, renderEmbeddedQrc(widgetName, embeddedFiles)), "utf8");
2397
2527
  return true;
2398
2528
  }
2399
2529
  function normalizeEmbeddedIndexHtml(indexPath, webRoot) {
@@ -2417,15 +2547,15 @@ function normalizeEmbeddedIndexHtml(indexPath, webRoot) {
2417
2547
  node_fs_1.default.writeFileSync(indexPath, html, "utf8");
2418
2548
  }
2419
2549
  function renderQtIntegrationCMake(widgetName) {
2420
- const generatedRootVar = "ANQST_GENERATED_CPP_DIR";
2550
+ const generatedRootVar = "ANQST_GENERATED_WIDGET_DIR";
2421
2551
  const generatedIncludeVar = "ANQST_GENERATED_INCLUDE_DIR";
2422
- const webappRootVar = "ANQST_WEBAPP_ROOT";
2552
+ const projectRootVar = "ANQST_PROJECT_ROOT";
2423
2553
  const widgetTarget = `${widgetName}Widget`;
2424
2554
  const autogenTarget = `${widgetTarget}_anqst_codegen`;
2425
2555
  return `cmake_minimum_required(VERSION 3.21)
2426
2556
 
2427
- set(${webappRootVar} "\${CMAKE_CURRENT_LIST_DIR}/..")
2428
- set(${generatedRootVar} "\${${webappRootVar}}/generated_output/${generatedCppLibraryDirName(widgetName)}")
2557
+ set(${projectRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../../../../..")
2558
+ set(${generatedRootVar} "\${CMAKE_CURRENT_LIST_DIR}/../qt/${generatedCppLibraryDirName(widgetName)}")
2429
2559
  set(${generatedIncludeVar} "\${${generatedRootVar}}/include")
2430
2560
 
2431
2561
  if(TARGET ${widgetTarget})
@@ -2433,7 +2563,7 @@ if(TARGET ${widgetTarget})
2433
2563
  endif()
2434
2564
 
2435
2565
  if(NOT TARGET anqstwebhostbase)
2436
- message(FATAL_ERROR "Target 'anqstwebhostbase' must exist before including anqst-cmake for ${widgetName}.")
2566
+ message(FATAL_ERROR "Target 'anqstwebhostbase' must exist before including generated AnQst CMake for ${widgetName}.")
2437
2567
  endif()
2438
2568
 
2439
2569
  find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
@@ -2453,7 +2583,7 @@ add_custom_command(
2453
2583
  "\${${generatedRootVar}}/webapp/index.html"
2454
2584
  COMMAND "\${ANQST_NPM_EXECUTABLE}" install
2455
2585
  COMMAND "\${ANQST_NPM_EXECUTABLE}" run anqst:build
2456
- WORKING_DIRECTORY "\${${webappRootVar}}"
2586
+ WORKING_DIRECTORY "\${${projectRootVar}}"
2457
2587
  COMMENT "Generating AnQst widget library (${widgetTarget}) from Angular project"
2458
2588
  VERBATIM
2459
2589
  )
@@ -2494,7 +2624,301 @@ target_link_libraries(${widgetTarget}
2494
2624
  `;
2495
2625
  }
2496
2626
  function installQtIntegrationCMake(cwd, widgetName) {
2497
- const integrationDir = node_path_1.default.join(cwd, "anqst-cmake");
2627
+ const integrationDir = (0, layout_1.resolveGeneratedLayoutPaths)(cwd, widgetName).cppCmakeRoot;
2498
2628
  node_fs_1.default.mkdirSync(integrationDir, { recursive: true });
2499
- node_fs_1.default.writeFileSync(node_path_1.default.join(integrationDir, "CMakeLists.txt"), renderQtIntegrationCMake(widgetName), "utf8");
2629
+ node_fs_1.default.writeFileSync(node_path_1.default.join(integrationDir, "CMakeLists.txt"), withBuildStamp("backend/cpp/cmake/CMakeLists.txt", renderQtIntegrationCMake(widgetName)), "utf8");
2630
+ }
2631
+ function normalizeIcoSize(dim) {
2632
+ return dim === 0 ? 256 : dim;
2633
+ }
2634
+ function escapeCppStringLiteral(value) {
2635
+ return value
2636
+ .replace(/\\/g, "\\\\")
2637
+ .replace(/"/g, '\\"')
2638
+ .replace(/\r/g, "\\r")
2639
+ .replace(/\n/g, "\\n");
2640
+ }
2641
+ function readDistFavicon(cwd) {
2642
+ const distRoot = node_path_1.default.join(cwd, "dist");
2643
+ if (!node_fs_1.default.existsSync(distRoot) || !node_fs_1.default.statSync(distRoot).isDirectory()) {
2644
+ return null;
2645
+ }
2646
+ const stack = [distRoot];
2647
+ while (stack.length > 0) {
2648
+ const current = stack.shift();
2649
+ const entries = node_fs_1.default.readdirSync(current, { withFileTypes: true })
2650
+ .sort((a, b) => a.name.localeCompare(b.name));
2651
+ for (const entry of entries) {
2652
+ const fullPath = node_path_1.default.join(current, entry.name);
2653
+ if (entry.isDirectory()) {
2654
+ stack.push(fullPath);
2655
+ continue;
2656
+ }
2657
+ if (entry.isFile() && entry.name.toLowerCase() === "favicon.ico") {
2658
+ return node_fs_1.default.readFileSync(fullPath);
2659
+ }
2660
+ }
2661
+ }
2662
+ return null;
2663
+ }
2664
+ function resolveFaviconIcoBuffer(cwd) {
2665
+ const distFavicon = readDistFavicon(cwd);
2666
+ if (distFavicon !== null) {
2667
+ return distFavicon;
2668
+ }
2669
+ const fallbackFiles = [
2670
+ node_path_1.default.join(cwd, "res", "favicon.ico"),
2671
+ node_path_1.default.join(cwd, "src", "favicon.ico"),
2672
+ node_path_1.default.join(cwd, "favicon.ico")
2673
+ ];
2674
+ for (const filePath of fallbackFiles) {
2675
+ if (node_fs_1.default.existsSync(filePath) && node_fs_1.default.statSync(filePath).isFile()) {
2676
+ return node_fs_1.default.readFileSync(filePath);
2677
+ }
2678
+ }
2679
+ return null;
2680
+ }
2681
+ function decodeIcoBmpToPng(imageData) {
2682
+ if (imageData.length < 40) {
2683
+ throw new Error("ICO BMP frame too small.");
2684
+ }
2685
+ const headerSize = imageData.readUInt32LE(0);
2686
+ if (headerSize < 40 || imageData.length < headerSize) {
2687
+ throw new Error("ICO BMP frame has unsupported DIB header.");
2688
+ }
2689
+ const width = imageData.readInt32LE(4);
2690
+ const heightTotal = imageData.readInt32LE(8);
2691
+ const planes = imageData.readUInt16LE(12);
2692
+ const bitCount = imageData.readUInt16LE(14);
2693
+ const compression = imageData.readUInt32LE(16);
2694
+ if (width <= 0 || heightTotal <= 0) {
2695
+ throw new Error("ICO BMP frame has invalid dimensions.");
2696
+ }
2697
+ const height = Math.floor(heightTotal / 2);
2698
+ if (height <= 0) {
2699
+ throw new Error("ICO BMP frame has invalid mask height.");
2700
+ }
2701
+ if (planes !== 1 || bitCount !== 32 || compression !== 0) {
2702
+ throw new Error("ICO BMP frame format unsupported; expected 32-bit BI_RGB.");
2703
+ }
2704
+ const pixelOffset = headerSize;
2705
+ const rowBytes = width * 4;
2706
+ const pixelBytes = rowBytes * height;
2707
+ if (imageData.length < pixelOffset + pixelBytes) {
2708
+ throw new Error("ICO BMP frame is truncated.");
2709
+ }
2710
+ const png = new pngjs_1.PNG({ width, height });
2711
+ for (let y = 0; y < height; y += 1) {
2712
+ const srcY = height - 1 - y;
2713
+ const srcRow = pixelOffset + srcY * rowBytes;
2714
+ const dstRow = y * rowBytes;
2715
+ for (let x = 0; x < width; x += 1) {
2716
+ const src = srcRow + x * 4;
2717
+ const dst = dstRow + x * 4;
2718
+ const b = imageData[src];
2719
+ const g = imageData[src + 1];
2720
+ const r = imageData[src + 2];
2721
+ const a = imageData[src + 3];
2722
+ png.data[dst] = r;
2723
+ png.data[dst + 1] = g;
2724
+ png.data[dst + 2] = b;
2725
+ png.data[dst + 3] = a;
2726
+ }
2727
+ }
2728
+ return pngjs_1.PNG.sync.write(png);
2729
+ }
2730
+ function convertIcoToPngBuffer(icoBytes) {
2731
+ if (icoBytes.length < 6) {
2732
+ throw new Error("favicon.ico is too small.");
2733
+ }
2734
+ const reserved = icoBytes.readUInt16LE(0);
2735
+ const iconType = icoBytes.readUInt16LE(2);
2736
+ const count = icoBytes.readUInt16LE(4);
2737
+ if (reserved !== 0 || iconType !== 1 || count === 0) {
2738
+ throw new Error("favicon.ico has invalid ICO header.");
2739
+ }
2740
+ if (icoBytes.length < 6 + count * 16) {
2741
+ throw new Error("favicon.ico has truncated directory entries.");
2742
+ }
2743
+ const frames = [];
2744
+ for (let i = 0; i < count; i += 1) {
2745
+ const entryOffset = 6 + i * 16;
2746
+ const width = normalizeIcoSize(icoBytes[entryOffset]);
2747
+ const height = normalizeIcoSize(icoBytes[entryOffset + 1]);
2748
+ const bytesInRes = icoBytes.readUInt32LE(entryOffset + 8);
2749
+ const imageOffset = icoBytes.readUInt32LE(entryOffset + 12);
2750
+ if (bytesInRes === 0)
2751
+ continue;
2752
+ if (imageOffset + bytesInRes > icoBytes.length)
2753
+ continue;
2754
+ frames.push({ width, height, bytesInRes, imageOffset });
2755
+ }
2756
+ if (frames.length === 0) {
2757
+ throw new Error("favicon.ico contains no readable image frames.");
2758
+ }
2759
+ frames.sort((a, b) => {
2760
+ const areaDiff = b.width * b.height - a.width * a.height;
2761
+ if (areaDiff !== 0)
2762
+ return areaDiff;
2763
+ return b.bytesInRes - a.bytesInRes;
2764
+ });
2765
+ const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
2766
+ for (const frame of frames) {
2767
+ const imageData = icoBytes.subarray(frame.imageOffset, frame.imageOffset + frame.bytesInRes);
2768
+ if (imageData.subarray(0, 8).equals(pngSignature)) {
2769
+ return Buffer.from(imageData);
2770
+ }
2771
+ }
2772
+ return decodeIcoBmpToPng(icoBytes.subarray(frames[0].imageOffset, frames[0].imageOffset + frames[0].bytesInRes));
2773
+ }
2774
+ function renderDesignerPluginQrc() {
2775
+ return `<RCC>
2776
+ <qresource prefix="/anqstdesignerplugin">
2777
+ <file>plugin-icon.png</file>
2778
+ </qresource>
2779
+ </RCC>
2780
+ `;
2781
+ }
2782
+ function installDesignerPluginIconAssets(cwd, pluginDir) {
2783
+ const iconTargetPath = node_path_1.default.join(pluginDir, "plugin-icon.png");
2784
+ const qrcTargetPath = node_path_1.default.join(pluginDir, "designerplugin.qrc");
2785
+ const icoBytes = resolveFaviconIcoBuffer(cwd);
2786
+ if (icoBytes === null) {
2787
+ if (node_fs_1.default.existsSync(iconTargetPath))
2788
+ node_fs_1.default.rmSync(iconTargetPath, { force: true });
2789
+ if (node_fs_1.default.existsSync(qrcTargetPath))
2790
+ node_fs_1.default.rmSync(qrcTargetPath, { force: true });
2791
+ return { hasIcon: false };
2792
+ }
2793
+ const pngBytes = convertIcoToPngBuffer(icoBytes);
2794
+ node_fs_1.default.writeFileSync(iconTargetPath, pngBytes);
2795
+ node_fs_1.default.writeFileSync(qrcTargetPath, renderDesignerPluginQrc(), "utf8");
2796
+ return { hasIcon: true };
2797
+ }
2798
+ function renderQtDesignerPluginCpp(widgetName, widgetCategory, hasIcon) {
2799
+ const pluginClass = `${widgetName}DesignerPlugin`;
2800
+ const widgetClass = `${widgetName}::${widgetName}`;
2801
+ const groupName = escapeCppStringLiteral(widgetCategory);
2802
+ const iconExpression = hasIcon
2803
+ ? 'QIcon(QStringLiteral(":/anqstdesignerplugin/plugin-icon.png"))'
2804
+ : "QIcon()";
2805
+ return `#include <QtUiPlugin/QDesignerCustomWidgetInterface>
2806
+ #include <QIcon>
2807
+ #include <QObject>
2808
+ #include <QString>
2809
+ #include <QWidget>
2810
+ #include "include/${widgetName}.h"
2811
+
2812
+ class ${pluginClass} final : public QObject, public QDesignerCustomWidgetInterface {
2813
+ Q_OBJECT
2814
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidgetInterface")
2815
+ Q_INTERFACES(QDesignerCustomWidgetInterface)
2816
+
2817
+ public:
2818
+ explicit ${pluginClass}(QObject* parent = nullptr) : QObject(parent) {}
2819
+
2820
+ QString name() const override { return QStringLiteral("${widgetClass}"); }
2821
+ QString group() const override { return QStringLiteral("${groupName}"); }
2822
+ QIcon icon() const override { return ${iconExpression}; }
2823
+ QString toolTip() const override { return QStringLiteral("${widgetName} generated by AnQst."); }
2824
+ QString whatsThis() const override { return QStringLiteral("${widgetName} generated by AnQst."); }
2825
+ bool isContainer() const override { return false; }
2826
+ QString includeFile() const override { return QStringLiteral("include/${widgetName}.h"); }
2827
+ QWidget* createWidget(QWidget* parent) override {
2828
+ auto* widget = new ${widgetClass}(parent);
2829
+ widget->setProperty("anqstDesignerContext", true);
2830
+ return widget;
2831
+ }
2832
+ bool isInitialized() const override { return true; }
2833
+ void initialize(QDesignerFormEditorInterface*) override {}
2834
+
2835
+ QString domXml() const override {
2836
+ return QStringLiteral(
2837
+ "<ui language=\\"c++\\">\\n"
2838
+ " <widget class=\\"${widgetClass}\\" name=\\"${widgetName.toLowerCase()}\\">\\n"
2839
+ " </widget>\\n"
2840
+ "</ui>\\n");
2841
+ }
2842
+ };
2843
+
2844
+ #include "${pluginClass}.moc"
2845
+ `;
2846
+ }
2847
+ function renderQtDesignerPluginCMake(widgetName, hasIcon) {
2848
+ const widgetTarget = `${widgetName}Widget`;
2849
+ const pluginTarget = `${widgetName}DesignerPlugin`;
2850
+ const resourceLine = hasIcon ? " \"${CMAKE_CURRENT_LIST_DIR}/designerplugin.qrc\"\n" : "";
2851
+ return `cmake_minimum_required(VERSION 3.21)
2852
+ project(${pluginTarget} LANGUAGES CXX)
2853
+
2854
+ set(CMAKE_CXX_STANDARD 17)
2855
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
2856
+ set(CMAKE_AUTOMOC ON)
2857
+ set(CMAKE_AUTOUIC ON)
2858
+ set(CMAKE_AUTORCC ON)
2859
+
2860
+ set(ANQST_PROJECT_ROOT "\${CMAKE_CURRENT_LIST_DIR}/../../../../../../..")
2861
+ set(ANQST_WIDGET_DIR "\${CMAKE_CURRENT_LIST_DIR}/..")
2862
+ set(ANQST_WEBBASE_DIR "" CACHE PATH "Path to AnQstWebBase source directory")
2863
+
2864
+ if(NOT EXISTS "\${ANQST_WIDGET_DIR}/CMakeLists.txt")
2865
+ message(FATAL_ERROR "Missing generated widget CMake project at \${ANQST_WIDGET_DIR}. Run 'anqst build' first.")
2866
+ endif()
2867
+
2868
+ if(NOT ANQST_WEBBASE_DIR)
2869
+ foreach(candidate
2870
+ "\${ANQST_PROJECT_ROOT}/AnQstWidget/AnQstWebBase"
2871
+ "\${ANQST_PROJECT_ROOT}/../AnQstWidget/AnQstWebBase"
2872
+ "\${ANQST_PROJECT_ROOT}/../../AnQstWidget/AnQstWebBase"
2873
+ "\${ANQST_PROJECT_ROOT}/../../../AnQstWidget/AnQstWebBase")
2874
+ if(EXISTS "\${candidate}/CMakeLists.txt")
2875
+ set(ANQST_WEBBASE_DIR "\${candidate}")
2876
+ break()
2877
+ endif()
2878
+ endforeach()
2879
+ endif()
2880
+
2881
+ if(NOT ANQST_WEBBASE_DIR OR NOT EXISTS "\${ANQST_WEBBASE_DIR}/CMakeLists.txt")
2882
+ message(FATAL_ERROR "Unable to locate AnQstWebBase sources. Set -DANQST_WEBBASE_DIR=<path/to/AnQstWidget/AnQstWebBase>.")
2883
+ endif()
2884
+
2885
+ find_package(Qt5 REQUIRED COMPONENTS Core Widgets UiPlugin)
2886
+
2887
+ set(ANQSTWEBBASE_BUILD_TESTS OFF CACHE BOOL "Build AnQstWebBase unit tests" FORCE)
2888
+ if(NOT TARGET anqstwebhostbase)
2889
+ add_subdirectory("\${ANQST_WEBBASE_DIR}" "\${CMAKE_CURRENT_BINARY_DIR}/anqstwebbase")
2890
+ endif()
2891
+
2892
+ if(NOT TARGET ${widgetTarget})
2893
+ add_subdirectory("\${ANQST_WIDGET_DIR}" "\${CMAKE_CURRENT_BINARY_DIR}/generated-widget")
2894
+ endif()
2895
+
2896
+ add_library(${pluginTarget} MODULE
2897
+ "\${CMAKE_CURRENT_LIST_DIR}/${pluginTarget}.cpp"
2898
+ ${resourceLine})
2899
+ target_include_directories(${pluginTarget}
2900
+ PRIVATE
2901
+ "\${ANQST_WIDGET_DIR}"
2902
+ "\${ANQST_WIDGET_DIR}/include"
2903
+ )
2904
+ target_link_libraries(${pluginTarget}
2905
+ PRIVATE
2906
+ ${widgetTarget}
2907
+ Qt5::Core
2908
+ Qt5::Widgets
2909
+ Qt5::UiPlugin
2910
+ )
2911
+ set_target_properties(${pluginTarget} PROPERTIES
2912
+ PREFIX ""
2913
+ )
2914
+ `;
2915
+ }
2916
+ function installQtDesignerPluginCMake(cwd, widgetName, options = {}) {
2917
+ const pluginDir = (0, layout_1.resolveGeneratedLayoutPaths)(cwd, widgetName).designerPluginRoot;
2918
+ node_fs_1.default.mkdirSync(pluginDir, { recursive: true });
2919
+ const assets = installDesignerPluginIconAssets(cwd, pluginDir);
2920
+ const pluginTarget = `${widgetName}DesignerPlugin`;
2921
+ const widgetCategory = options.widgetCategory ?? "AnQst Widgets";
2922
+ node_fs_1.default.writeFileSync(node_path_1.default.join(pluginDir, "CMakeLists.txt"), withBuildStamp(`backend/cpp/qt/${generatedCppLibraryDirName(widgetName)}/designerPlugin/CMakeLists.txt`, renderQtDesignerPluginCMake(widgetName, assets.hasIcon)), "utf8");
2923
+ node_fs_1.default.writeFileSync(node_path_1.default.join(pluginDir, `${pluginTarget}.cpp`), withBuildStamp(`backend/cpp/qt/${generatedCppLibraryDirName(widgetName)}/designerPlugin/${pluginTarget}.cpp`, renderQtDesignerPluginCpp(widgetName, widgetCategory, assets.hasIcon)), "utf8");
2500
2924
  }