@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.
@@ -41,8 +41,12 @@ function anqstSettingsFileName(widgetName) {
41
41
  function anqstSettingsRelativePath(widgetName) {
42
42
  return `./${exports.ANQST_ROOT_DIRNAME}/${anqstSettingsFileName(widgetName)}`;
43
43
  }
44
- function generatedFrontendDirName(widgetName) {
45
- return `${widgetName}_Angular`;
44
+ function generatedFrontendDirName(widgetName, target) {
45
+ if (target === "AngularService")
46
+ return `${widgetName}_Angular`;
47
+ if (target === "VanillaTS")
48
+ return `${widgetName}_VanillaTS`;
49
+ return `${widgetName}_VanillaJS`;
46
50
  }
47
51
  function generatedNodeExpressDirName(widgetName) {
48
52
  return `${widgetName}_anQst`;
@@ -56,7 +60,9 @@ function resolveGeneratedLayoutPaths(cwd, widgetName) {
56
60
  const designerPluginRoot = node_path_1.default.join(cppQtWidgetRoot, "designerPlugin");
57
61
  return {
58
62
  generatedRoot,
59
- frontendRoot: node_path_1.default.join(generatedRoot, "frontend", generatedFrontendDirName(widgetName)),
63
+ angularFrontendRoot: node_path_1.default.join(generatedRoot, "frontend", generatedFrontendDirName(widgetName, "AngularService")),
64
+ vanillaTsFrontendRoot: node_path_1.default.join(generatedRoot, "frontend", generatedFrontendDirName(widgetName, "VanillaTS")),
65
+ vanillaJsFrontendRoot: node_path_1.default.join(generatedRoot, "frontend", generatedFrontendDirName(widgetName, "VanillaJS")),
60
66
  nodeExpressRoot: node_path_1.default.join(generatedRoot, "backend", "node", "express", generatedNodeExpressDirName(widgetName)),
61
67
  cppCmakeRoot: node_path_1.default.join(generatedRoot, "backend", "cpp", "cmake"),
62
68
  cppQtWidgetRoot,
@@ -24,6 +24,22 @@ function qNameToText(name) {
24
24
  return name.text;
25
25
  return `${qNameToText(name.left)}.${name.right.text}`;
26
26
  }
27
+ function textToEntityName(text) {
28
+ const parts = text.split(".");
29
+ let current = typescript_1.default.factory.createIdentifier(parts[0] ?? text);
30
+ for (const part of parts.slice(1)) {
31
+ current = typescript_1.default.factory.createQualifiedName(current, typescript_1.default.factory.createIdentifier(part));
32
+ }
33
+ return current;
34
+ }
35
+ function textToExpressionName(text) {
36
+ const parts = text.split(".");
37
+ let current = typescript_1.default.factory.createIdentifier(parts[0] ?? text);
38
+ for (const part of parts.slice(1)) {
39
+ current = typescript_1.default.factory.createPropertyAccessExpression(current, typescript_1.default.factory.createIdentifier(part));
40
+ }
41
+ return current;
42
+ }
27
43
  function collectReferencedTypeNames(node) {
28
44
  const refs = new Set();
29
45
  const visit = (n) => {
@@ -33,6 +49,9 @@ function collectReferencedTypeNames(node) {
33
49
  else if (typescript_1.default.isExpressionWithTypeArguments(n) && typescript_1.default.isIdentifier(n.expression)) {
34
50
  refs.add(n.expression.text);
35
51
  }
52
+ else if (typescript_1.default.isTypeQueryNode(n)) {
53
+ refs.add(qNameToText(n.exprName));
54
+ }
36
55
  typescript_1.default.forEachChild(n, visit);
37
56
  };
38
57
  visit(node);
@@ -294,10 +313,118 @@ function requiresLocalImportResolution(moduleName) {
294
313
  return false;
295
314
  return moduleName.includes("/");
296
315
  }
297
- function parseImportedTypeDecls(specFilePath, source) {
316
+ function collectTopLevelTypeDecls(source) {
317
+ const out = new Map();
318
+ for (const stmt of source.statements) {
319
+ if ((typescript_1.default.isInterfaceDeclaration(stmt) || typescript_1.default.isTypeAliasDeclaration(stmt)) && stmt.name) {
320
+ out.set(stmt.name.text, stmt);
321
+ }
322
+ }
323
+ return out;
324
+ }
325
+ function collectReachableImportedTypeNames(topLevelDecls, rootNames) {
326
+ const queue = [...rootNames];
327
+ const seen = new Set();
328
+ const ordered = [];
329
+ while (queue.length > 0) {
330
+ const current = queue.shift();
331
+ if (seen.has(current))
332
+ continue;
333
+ seen.add(current);
334
+ const node = topLevelDecls.get(current);
335
+ if (!node)
336
+ continue;
337
+ ordered.push(current);
338
+ const decl = {
339
+ referencedTypeNames: collectReferencedTypeNames(node)
340
+ };
341
+ for (const ref of decl.referencedTypeNames) {
342
+ if (topLevelDecls.has(ref) && !seen.has(ref)) {
343
+ queue.push(ref);
344
+ }
345
+ }
346
+ }
347
+ return ordered;
348
+ }
349
+ function allocateSyntheticImportedTypeName(sourceName, usedNames) {
350
+ const cleaned = sourceName
351
+ .replace(/[^A-Za-z0-9_]/g, "_")
352
+ .replace(/_+/g, "_")
353
+ .replace(/^_+|_+$/g, "");
354
+ const base = `AnQstImported_${cleaned || "Type"}`;
355
+ let candidate = base;
356
+ let i = 2;
357
+ while (usedNames.has(candidate)) {
358
+ candidate = `${base}_${i}`;
359
+ i += 1;
360
+ }
361
+ usedNames.add(candidate);
362
+ return candidate;
363
+ }
364
+ function rewriteImportedTypeDecl(importedSource, node, finalName, nameMap) {
365
+ const renamed = typescript_1.default.isInterfaceDeclaration(node)
366
+ ? typescript_1.default.factory.updateInterfaceDeclaration(node, node.modifiers, typescript_1.default.factory.createIdentifier(finalName), node.typeParameters, node.heritageClauses, node.members)
367
+ : typescript_1.default.factory.updateTypeAliasDeclaration(node, node.modifiers, typescript_1.default.factory.createIdentifier(finalName), node.typeParameters, node.type);
368
+ const transformed = typescript_1.default.transform(renamed, [(context) => {
369
+ const visitor = (child) => {
370
+ if (typescript_1.default.isTypeReferenceNode(child)) {
371
+ const mapped = nameMap.get(qNameToText(child.typeName));
372
+ if (mapped) {
373
+ return typescript_1.default.factory.updateTypeReferenceNode(child, textToEntityName(mapped), child.typeArguments);
374
+ }
375
+ }
376
+ else if (typescript_1.default.isExpressionWithTypeArguments(child)) {
377
+ const exprText = typescript_1.default.isIdentifier(child.expression) || typescript_1.default.isPropertyAccessExpression(child.expression)
378
+ ? child.expression.getText(importedSource)
379
+ : null;
380
+ const mapped = exprText ? nameMap.get(exprText) : null;
381
+ if (mapped) {
382
+ return typescript_1.default.factory.updateExpressionWithTypeArguments(child, textToExpressionName(mapped), child.typeArguments);
383
+ }
384
+ }
385
+ else if (typescript_1.default.isTypeQueryNode(child)) {
386
+ const mapped = nameMap.get(qNameToText(child.exprName));
387
+ if (mapped) {
388
+ return typescript_1.default.factory.updateTypeQueryNode(child, textToEntityName(mapped), child.typeArguments);
389
+ }
390
+ }
391
+ return typescript_1.default.visitEachChild(child, visitor, context);
392
+ };
393
+ return (root) => typescript_1.default.visitNode(root, visitor);
394
+ }]);
395
+ const rewritten = transformed.transformed[0];
396
+ transformed.dispose();
397
+ const printer = typescript_1.default.createPrinter({ newLine: typescript_1.default.NewLineKind.LineFeed });
398
+ const rewrittenSource = typescript_1.default.createSourceFile("__anqst_imported_decl.ts", "", typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
399
+ const nodeText = printer.printNode(typescript_1.default.EmitHint.Unspecified, rewritten, rewrittenSource);
400
+ return {
401
+ name: finalName,
402
+ kind: typescript_1.default.isInterfaceDeclaration(rewritten) ? "interface" : "type",
403
+ nodeText,
404
+ referencedTypeNames: collectReferencedTypeNames(rewritten),
405
+ loc: locFromNode(importedSource, node)
406
+ };
407
+ }
408
+ function createImportedAliasDecl(aliasName, targetName, loc) {
409
+ const nodeText = `type ${aliasName} = ${targetName};`;
410
+ const source = typescript_1.default.createSourceFile("__anqst_import_alias.ts", nodeText, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
411
+ const stmt = source.statements.find(typescript_1.default.isTypeAliasDeclaration);
412
+ if (!stmt) {
413
+ throw new Error(`Unable to synthesize imported alias declaration for ${aliasName}.`);
414
+ }
415
+ return {
416
+ name: aliasName,
417
+ kind: "type",
418
+ nodeText,
419
+ referencedTypeNames: collectReferencedTypeNames(stmt),
420
+ loc
421
+ };
422
+ }
423
+ function parseImportedTypeDecls(specFilePath, source, reservedTypeNames = new Set()) {
298
424
  const importedTypeDecls = new Map();
299
425
  const importedTypeSymbols = new Set();
300
426
  const specImports = [];
427
+ const usedImportedNames = new Set(reservedTypeNames);
301
428
  for (const stmt of source.statements) {
302
429
  if (!typescript_1.default.isImportDeclaration(stmt) || !stmt.importClause || !typescript_1.default.isStringLiteral(stmt.moduleSpecifier))
303
430
  continue;
@@ -335,10 +462,38 @@ function parseImportedTypeDecls(specFilePath, source) {
335
462
  }
336
463
  const text = node_fs_1.default.readFileSync(resolved, "utf8");
337
464
  const importedSource = typescript_1.default.createSourceFile(resolved, text, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
338
- for (const importedStmt of importedSource.statements) {
339
- if (typescript_1.default.isInterfaceDeclaration(importedStmt) || typescript_1.default.isTypeAliasDeclaration(importedStmt)) {
340
- const decl = parseTypeDecl(importedSource, importedStmt);
341
- importedTypeDecls.set(decl.name, decl);
465
+ const topLevelDecls = collectTopLevelTypeDecls(importedSource);
466
+ const directAliasesBySourceName = new Map();
467
+ for (const namedImport of importModel.namedImports) {
468
+ if (!topLevelDecls.has(namedImport.importedName))
469
+ continue;
470
+ const aliases = directAliasesBySourceName.get(namedImport.importedName) ?? [];
471
+ aliases.push(namedImport.localName);
472
+ directAliasesBySourceName.set(namedImport.importedName, aliases);
473
+ }
474
+ const reachableSourceNames = collectReachableImportedTypeNames(topLevelDecls, directAliasesBySourceName.keys());
475
+ if (reachableSourceNames.length === 0)
476
+ continue;
477
+ const canonicalNameBySourceName = new Map();
478
+ for (const sourceName of reachableSourceNames) {
479
+ const directAliases = directAliasesBySourceName.get(sourceName);
480
+ if (directAliases && directAliases.length > 0) {
481
+ canonicalNameBySourceName.set(sourceName, directAliases[0]);
482
+ usedImportedNames.add(directAliases[0]);
483
+ }
484
+ else {
485
+ canonicalNameBySourceName.set(sourceName, allocateSyntheticImportedTypeName(sourceName, usedImportedNames));
486
+ }
487
+ }
488
+ for (const sourceName of reachableSourceNames) {
489
+ const node = topLevelDecls.get(sourceName);
490
+ const finalName = canonicalNameBySourceName.get(sourceName);
491
+ if (!node || !finalName)
492
+ continue;
493
+ importedTypeDecls.set(finalName, rewriteImportedTypeDecl(importedSource, node, finalName, canonicalNameBySourceName));
494
+ const directAliases = directAliasesBySourceName.get(sourceName) ?? [];
495
+ for (const alias of directAliases.slice(1)) {
496
+ importedTypeDecls.set(alias, createImportedAliasDecl(alias, finalName, locFromNode(importedSource, node)));
342
497
  }
343
498
  }
344
499
  }
@@ -398,7 +553,7 @@ function parseSpecFileAst(specFilePath) {
398
553
  namespaceTypeDecls.push(parseTypeDecl(source, stmt));
399
554
  }
400
555
  }
401
- const importInfo = parseImportedTypeDecls(specFilePath, source);
556
+ const importInfo = parseImportedTypeDecls(specFilePath, source, new Set(namespaceTypeDecls.map((decl) => decl.name)));
402
557
  return {
403
558
  filePath: specFilePath,
404
559
  widgetName: ns.name.text,
@@ -16,7 +16,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
17
  const errors_1 = require("./errors");
18
18
  const layout_1 = require("./layout");
19
- exports.DEFAULT_ANQST_GENERATE_TARGETS = ["QWidget", "AngularService", "node_express_ws"];
19
+ exports.DEFAULT_ANQST_GENERATE_TARGETS = ["QWidget", "AngularService", "VanillaTS", "VanillaJS", "node_express_ws"];
20
20
  const ANQST_DSL_IMPORT_LINE = 'import type { AnQst } from "@dusted/anqst";';
21
21
  const ANQST_BUILD_HOOK = "npx anqst build";
22
22
  function readJsonFile(filePath) {
@@ -153,7 +153,7 @@ function updateTsConfig(cwd, widgetName) {
153
153
  ? {}
154
154
  : ensureObject(compilerOptions.paths, "Invalid tsconfig.json: expected object at 'compilerOptions.paths'.");
155
155
  compilerOptions.paths = pathsObject;
156
- const generatedAliasPath = `AnQst/generated/frontend/${(0, layout_1.generatedFrontendDirName)(widgetName)}/*`;
156
+ const generatedAliasPath = `AnQst/generated/frontend/${(0, layout_1.generatedFrontendDirName)(widgetName, "AngularService")}/*`;
157
157
  const existingAlias = pathsObject["anqst-generated/*"];
158
158
  const aliasList = Array.isArray(existingAlias)
159
159
  ? existingAlias.filter((entry) => typeof entry === "string")
@@ -164,7 +164,7 @@ function updateTsConfig(cwd, widgetName) {
164
164
  pathsObject["anqst-generated/*"] = [...new Set(aliasList)];
165
165
  if (Array.isArray(tsConfig.include)) {
166
166
  const includeList = tsConfig.include.filter((entry) => typeof entry === "string");
167
- const generatedTypesPattern = `AnQst/generated/frontend/${(0, layout_1.generatedFrontendDirName)(widgetName)}/**/*.d.ts`;
167
+ const generatedTypesPattern = `AnQst/generated/frontend/${(0, layout_1.generatedFrontendDirName)(widgetName, "AngularService")}/**/*.d.ts`;
168
168
  if (!includeList.includes(generatedTypesPattern)) {
169
169
  includeList.push(generatedTypesPattern);
170
170
  }
@@ -135,8 +135,10 @@ function collectReachableTypeNames(spec) {
135
135
  const allDecls = new Map();
136
136
  for (const d of spec.namespaceTypeDecls)
137
137
  allDecls.set(d.name, d);
138
- for (const [name, d] of spec.importedTypeDecls)
139
- allDecls.set(name, d);
138
+ for (const [name, d] of spec.importedTypeDecls) {
139
+ if (!allDecls.has(name))
140
+ allDecls.set(name, d);
141
+ }
140
142
  const queue = [];
141
143
  const seen = new Set();
142
144
  for (const d of spec.namespaceTypeDecls)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dusted/anqst",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Opinionated backend generator for webapps.",
5
5
  "keywords": [
6
6
  "nodejs",
@@ -31,13 +31,15 @@ export namespace AnQst {
31
31
  interface Service { }
32
32
 
33
33
  /**
34
- * Declare service `InterfaceName` as development-mode capable transport service.
34
+ * Declare service `InterfaceName` as development-mode capable browser transport service.
35
35
  *
36
36
  * @remarks
37
37
  * - Extends the same method/property semantics as `AnQst.Service`.
38
- * - Signals to the generator/runtime that this widget should emit dual-transport bridge support
38
+ * - Signals to the generator/runtime that this widget should emit dual-transport browser bridge support
39
39
  * (Qt WebChannel + HTTP/WebSocket development bridge).
40
40
  * - Existing generated service APIs remain unchanged.
41
+ * - The name is historical. The capability applies to browser frontend profiles generally,
42
+ * not only Angular-specific generation.
41
43
  *
42
44
  * @example
43
45
  * export interface UserService extends AnQst.AngularHTTPBaseServerClass { }
@@ -63,7 +65,7 @@ export namespace AnQst {
63
65
  * @example
64
66
  * // AnQst spec:
65
67
  * getUserById(userId: string): AnQst.Call<User>
66
- * //Angular app:
68
+ * //Browser app:
67
69
  * const user: User = await this.userService.getUserById("abc");
68
70
  */
69
71
  interface Call<T, Config extends CallConfig = {}> { dummy: T; config?: Config }
@@ -89,7 +91,7 @@ export namespace AnQst {
89
91
  * @example
90
92
  * // AnQst spec:
91
93
  * getUsernameSubstring(from: number, to:number ): AnQst.Slot<string>
92
- * //Angular app:
94
+ * //Browser app:
93
95
  * this.userService.onSlot.getUsername( provider );
94
96
  * //Parent:
95
97
  * auto currentFormUsername = userMgmt.getUsername();
@@ -111,7 +113,7 @@ export namespace AnQst {
111
113
  * @example
112
114
  * // AnQst spec:
113
115
  * complain(whine: string): AnQst.Emitter;
114
- * //Angular app:
116
+ * //Browser app:
115
117
  * this.userService.complain("Why won't you LISTEN!");
116
118
  */
117
119
  interface Emitter { }
@@ -121,14 +123,14 @@ export namespace AnQst {
121
123
  * Declare reactive `PropertyName`:`OutputType` and set.`PropertyName`(arg: `OutputType`)
122
124
  * @remarks
123
125
  * - **Parent** -> Widget
124
- * True Angular signal semantics (Property updates, signal emits, no return path, no registration requirement)
126
+ * True frontend reactive semantics (Property updates, accessor reflects latest value, no return path, no registration requirement)
125
127
  * - Flow:
126
128
  * - Parent: Sets generated widget property `PropertyName`.
127
129
  * - Widget: Service updates readonly property `PropertyName` and emits signal.
128
130
  * @example
129
131
  * // AnQst spec:
130
132
  * activeUsers: AnQst.Output<number>;
131
- * //Angular app template:
133
+ * //Browser app:
132
134
  * <p>{{ userService.activeUsers() }}</p>
133
135
  * //Parent:
134
136
  * int users = userMgmt.activeUsers;
@@ -148,7 +150,7 @@ export namespace AnQst {
148
150
  * @example
149
151
  * // AnQst spec:
150
152
  * currentUsername: AnQst.Input<string>;
151
- * //Angular app template:
153
+ * //Browser app:
152
154
  * <input type="text" placeholder="Your Name Here" (input)="userService.set.currentUsername(($event.target as HTMLInputElement).value)" />
153
155
  * //Parent:
154
156
  * QString userName = userMgmt.currentUsername;
@@ -160,15 +162,15 @@ export namespace AnQst {
160
162
  * Declare drop-target `PropertyName`:`PayloadType`
161
163
  * @remarks
162
164
  * - **Parent** -> Widget (framework-mediated)
163
- * - True Angular signal semantics.
165
+ * - True frontend reactive semantics.
164
166
  * - Flow:
165
167
  * - External: A Qt widget initiates a QDrag carrying QMimeData.
166
168
  * - Parent: AnQstWebHostBase intercepts the drop via event filter on the
167
169
  * embedded QWebEngineView's rendering surface.
168
170
  * - Parent: QMimeData for the accepted format is deserialized from a JSON
169
171
  * array of normalized AnQst wire items and forwarded through the bridge.
170
- * - Widget: Service updates signal `PropertyName` with the deserialized
171
- * payload and drop coordinates. Angular components react via effect() / template binding.
172
+ * - Widget: Service updates the generated accessor for `PropertyName` with the deserialized
173
+ * payload and drop coordinates. Browser application code reacts through the generated frontend API.
172
174
  * - MIME type is convention-derived: `application/anqst-dragdropevent_<ServiceName>-<TypeName>`.
173
175
  * - The source QWidget must serialize the generated AnQst wire payload as a
174
176
  * JSON array under the same MIME type. Generated Qt widgets expose helper
@@ -178,11 +180,9 @@ export namespace AnQst {
178
180
  * @example
179
181
  * // AnQst spec:
180
182
  * trackDropped: AnQst.DropTarget<Track>;
181
- * // Angular app:
182
- * effect(() => {
183
- * const drop = this.service.trackDropped();
184
- * if (drop !== null) { console.log(drop.payload, drop.x, drop.y); }
185
- * });
183
+ * // Browser app:
184
+ * const drop = frontend.services.DragService.trackDropped();
185
+ * if (drop !== null) { console.log(drop.payload, drop.x, drop.y); }
186
186
  */
187
187
  interface DropTarget<T> { dummy: T }
188
188
 
@@ -191,7 +191,7 @@ export namespace AnQst {
191
191
  * Declare hover-target `PropertyName`:`PayloadType`
192
192
  * @remarks
193
193
  * - **Parent** -> Widget (framework-mediated)
194
- * - True Angular signal semantics.
194
+ * - True frontend reactive semantics.
195
195
  * - Flow:
196
196
  * - External: A Qt widget initiates a QDrag carrying QMimeData.
197
197
  * - Parent: AnQstWebHostBase intercepts drag-move events via event filter on the
@@ -199,8 +199,8 @@ export namespace AnQst {
199
199
  * - Parent: Payload is deserialized once on DragEnter from a JSON array of
200
200
  * normalized AnQst wire items; subsequent DragMove events forward only
201
201
  * the updated position.
202
- * - Widget: Service updates signal `PropertyName` with the payload and current
203
- * coordinates. Signal becomes null on DragLeave.
202
+ * - Widget: Service updates the generated accessor for `PropertyName` with the payload and current
203
+ * coordinates. The accessor becomes null on DragLeave.
204
204
  * - Shares the same MIME type convention as DropTarget: `application/anqst-dragdropevent_<ServiceName>-<TypeName>`.
205
205
  * - A HoverTarget without a corresponding DropTarget means "show previews but reject the drop".
206
206
  * - Optional config supports throttle tuning:
@@ -217,11 +217,9 @@ export namespace AnQst {
217
217
  * trackHovering: AnQst.HoverTarget<Track, { maxRateHz: 10 }>;
218
218
  * // AnQst spec (no throttling):
219
219
  * trackHovering: AnQst.HoverTarget<Track, { maxRateHz: 0 }>;
220
- * // Angular app:
221
- * effect(() => {
222
- * const hover = this.service.trackHovering();
223
- * if (hover !== null) { highlight(document.elementFromPoint(hover.x, hover.y)); }
224
- * });
220
+ * // Browser app:
221
+ * const hover = frontend.services.DragService.trackHovering();
222
+ * if (hover !== null) { highlight(document.elementFromPoint(hover.x, hover.y)); }
225
223
  */
226
224
 
227
225
  /**