@dusted/anqst 1.6.0 → 1.7.1

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/app.js CHANGED
@@ -25,6 +25,14 @@ class CliUsageError extends Error {
25
25
  this.name = "CliUsageError";
26
26
  }
27
27
  }
28
+ function firstBrowserFrontendTarget(targets) {
29
+ for (const target of targets) {
30
+ if (target === "AngularService" || target === "VanillaTS" || target === "VanillaJS") {
31
+ return target;
32
+ }
33
+ }
34
+ return null;
35
+ }
28
36
  const ANQSTGEN_ACTIVE_STAMP_FILE = ".anqstgen-version-active.json";
29
37
  function renderHelp() {
30
38
  const version = readActiveBuildStamp();
@@ -219,7 +227,9 @@ function runBuild(cwd, designerPlugin = false) {
219
227
  try {
220
228
  const specPath = (0, project_1.resolveAnQstSpecPath)(cwd);
221
229
  const configuredWidgetName = (0, project_1.resolveAnQstWidgetName)(cwd);
230
+ const configuredTargets = (0, project_1.resolveAnQstGenerateTargets)(cwd);
222
231
  const generationTargets = resolveGenerationTargetsFromCwd(cwd, true);
232
+ const preferredFrontendTarget = firstBrowserFrontendTarget(configuredTargets);
223
233
  const parsed = (0, parser_1.parseSpecFile)(specPath);
224
234
  (0, verify_1.verifySpec)(parsed);
225
235
  if (parsed.widgetName !== configuredWidgetName) {
@@ -231,7 +241,9 @@ function runBuild(cwd, designerPlugin = false) {
231
241
  if (generationTargets.emitQWidget) {
232
242
  (0, emit_1.installQtIntegrationCMake)(cwd, parsed.widgetName);
233
243
  }
234
- const shouldRunAngularBuild = generationTargets.emitQWidget && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "angular.json"));
244
+ const shouldRunAngularBuild = generationTargets.emitQWidget
245
+ && preferredFrontendTarget === "AngularService"
246
+ && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "angular.json"));
235
247
  if (shouldRunAngularBuild) {
236
248
  const angularBuild = (0, node_child_process_1.spawnSync)("npx", ["ng", "build", "--configuration", "production"], {
237
249
  cwd,
@@ -242,8 +254,25 @@ function runBuild(cwd, designerPlugin = false) {
242
254
  throw new errors_1.VerifyError("Angular build failed while preparing embedded widget assets.");
243
255
  }
244
256
  }
257
+ let embeddedAssetsRefreshed = false;
245
258
  if (generationTargets.emitQWidget) {
259
+ if (preferredFrontendTarget === "VanillaJS") {
260
+ try {
261
+ (0, emit_1.buildVanillaJsBrowserBundle)(cwd, parsed.widgetName);
262
+ }
263
+ catch (error) {
264
+ const message = error instanceof Error ? error.message : String(error);
265
+ throw new errors_1.VerifyError(`VanillaJS browser packaging failed: ${message}`);
266
+ }
267
+ }
246
268
  const embedded = (0, emit_1.installEmbeddedWebBundle)(cwd, parsed.widgetName);
269
+ embeddedAssetsRefreshed = embedded;
270
+ if (preferredFrontendTarget === "VanillaJS" && !embedded) {
271
+ throw new errors_1.VerifyError("Unable to embed VanillaJS browser output. Ensure src/index.html and src/main.js produced a browser bundle.");
272
+ }
273
+ if (preferredFrontendTarget === "VanillaTS" && !embedded) {
274
+ throw new errors_1.VerifyError("Unable to embed VanillaTS browser output. Ensure the widget frontend build produced dist/browser with index.html.");
275
+ }
247
276
  if (shouldRunAngularBuild && !embedded) {
248
277
  throw new errors_1.VerifyError("Unable to embed browser output. Ensure the browser build produced a dist bundle with index.html.");
249
278
  }
@@ -284,19 +313,21 @@ function runBuild(cwd, designerPlugin = false) {
284
313
  if (generationTargets.emitVanillaTS) {
285
314
  detailLines.push(" Target VanillaTS:");
286
315
  detailLines.push(` - Browser bundle root: ${(0, layout_1.toProjectRelative)(cwd, layout.vanillaTsFrontendRoot)}`);
287
- detailLines.push(` - Browser global: window.AnQstGenerated.widgets.${parsed.widgetName}`);
316
+ detailLines.push(` - Browser global: window.AnQstGenerated.${parsed.widgetName}`);
288
317
  }
289
318
  if (generationTargets.emitVanillaJS) {
290
319
  detailLines.push(" Target VanillaJS:");
291
320
  detailLines.push(` - Browser bundle root: ${(0, layout_1.toProjectRelative)(cwd, layout.vanillaJsFrontendRoot)}`);
292
- detailLines.push(` - Browser global: window.AnQstGenerated.widgets.${parsed.widgetName}`);
321
+ detailLines.push(` - Browser global: window.AnQstGenerated.${parsed.widgetName}`);
293
322
  }
294
323
  if (generationTargets.emitQWidget) {
295
324
  detailLines.push(" Target QWidget:");
296
325
  detailLines.push(` - Qt integration CMake: ${(0, layout_1.toProjectRelative)(cwd, node_path_1.default.join(layout.cppCmakeRoot, "CMakeLists.txt"))}`);
297
326
  detailLines.push(` - Widget output root: ${(0, layout_1.toProjectRelative)(cwd, layout.cppQtWidgetRoot)}`);
298
327
  detailLines.push(" - C++ handoff: downstream CMake consumes this generated tree directly");
299
- detailLines.push(" - Embedded web assets refreshed from detected browser dist output");
328
+ if (embeddedAssetsRefreshed) {
329
+ detailLines.push(" - Embedded web assets refreshed from detected browser dist output");
330
+ }
300
331
  }
301
332
  if (generationTargets.emitNodeExpressWs) {
302
333
  detailLines.push(" Target node_express_ws:");
@@ -128,8 +128,10 @@ class BoundaryTransportAnalyzer {
128
128
  const out = new Map();
129
129
  for (const decl of this.spec.namespaceTypeDecls)
130
130
  out.set(decl.name, decl);
131
- for (const decl of this.spec.importedTypeDecls.values())
132
- out.set(decl.name, decl);
131
+ for (const decl of this.spec.importedTypeDecls.values()) {
132
+ if (!out.has(decl.name))
133
+ out.set(decl.name, decl);
134
+ }
133
135
  return [...out.values()];
134
136
  }
135
137
  nodeMeta(typeText, path, identityParts) {
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;
@@ -461,8 +462,10 @@ function collectStructDecls(spec) {
461
462
  const out = new Map();
462
463
  for (const d of spec.namespaceTypeDecls)
463
464
  out.set(d.name, d);
464
- for (const d of spec.importedTypeDecls.values())
465
- 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
+ }
466
469
  return [...out.values()];
467
470
  }
468
471
  function mapTypeTextToTs(typeText) {
@@ -574,14 +577,18 @@ function normalizeImportPathForGenerated(specFilePath, generatedFileRelPath, mod
574
577
  return normalized;
575
578
  return `./${normalized}`;
576
579
  }
577
- function renderRequiredTypeImports(spec, generatedFileRelPath) {
580
+ function renderRequiredTypeImports(spec, generatedFileRelPath, omitSymbols = new Set()) {
578
581
  const requiredSymbols = collectRequiredImportedSymbols(spec);
579
582
  if (requiredSymbols.size === 0)
580
583
  return "";
581
584
  const importLines = [];
582
585
  for (const imp of spec.specImports) {
583
- const defaultImport = imp.defaultImport && requiredSymbols.has(imp.defaultImport) ? imp.defaultImport : null;
584
- 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));
585
592
  if (!defaultImport && named.length === 0)
586
593
  continue;
587
594
  const moduleSpecifier = normalizeImportPathForGenerated(spec.filePath, generatedFileRelPath, imp.moduleSpecifier);
@@ -1280,6 +1287,7 @@ function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
1280
1287
  }
1281
1288
  else {
1282
1289
  publicSlots.push(`void ${member.name}Slot(const ${cppType}& value);`);
1290
+ fields.push(`bool m_${member.name}Published{false};`);
1283
1291
  }
1284
1292
  }
1285
1293
  else if (member.kind === "DropTarget" && member.payloadTypeText) {
@@ -1878,7 +1886,12 @@ function renderCppStub(spec, cppTypes, cppCodecCatalog) {
1878
1886
  lines.push("}");
1879
1887
  lines.push("");
1880
1888
  lines.push(`void ${widgetClassName}::set${cap}(const ${cppType}& value) {`);
1881
- lines.push(` if (m_${member.name} == value) return;`);
1889
+ if (member.kind === "Output") {
1890
+ lines.push(` if (m_${member.name}Published && m_${member.name} == value) return;`);
1891
+ }
1892
+ else {
1893
+ lines.push(` if (m_${member.name} == value) return;`);
1894
+ }
1882
1895
  if (member.kind === "Output") {
1883
1896
  lines.push(` QVariant encodedValue;`);
1884
1897
  lines.push(` try {`);
@@ -1912,6 +1925,7 @@ function renderCppStub(spec, cppTypes, cppCodecCatalog) {
1912
1925
  }
1913
1926
  lines.push(` m_${member.name} = value;`);
1914
1927
  if (member.kind === "Output") {
1928
+ lines.push(` m_${member.name}Published = true;`);
1915
1929
  lines.push(` setOutputValue(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), encodedValue);`);
1916
1930
  }
1917
1931
  lines.push(` emit ${member.name}Changed(value);`);
@@ -3370,19 +3384,132 @@ ${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMem
3370
3384
  }
3371
3385
  `;
3372
3386
  }
3373
- function renderVanillaBrowserTs(spec, codecCatalog) {
3387
+ function collectVanillaValueClasses(spec) {
3388
+ const classes = [];
3389
+ const requiredImportedSymbols = collectRequiredImportedSymbols(spec);
3390
+ const candidateDecls = [
3391
+ ...spec.namespaceTypeDecls,
3392
+ ...[...spec.importedTypeDecls.values()].filter((decl) => requiredImportedSymbols.has(decl.name))
3393
+ ];
3394
+ for (const decl of candidateDecls) {
3395
+ if (decl.kind !== "interface")
3396
+ continue;
3397
+ const node = parseTypeDeclNode(decl.nodeText);
3398
+ if (!node || !typescript_1.default.isInterfaceDeclaration(node))
3399
+ continue;
3400
+ const fields = [];
3401
+ let supported = true;
3402
+ for (const member of node.members) {
3403
+ if (!typescript_1.default.isPropertySignature(member) || !member.type || !member.name || !typescript_1.default.isIdentifier(member.name)) {
3404
+ supported = false;
3405
+ break;
3406
+ }
3407
+ fields.push({
3408
+ name: member.name.text,
3409
+ typeText: member.type.getText(),
3410
+ optional: !!member.questionToken
3411
+ });
3412
+ }
3413
+ if (supported) {
3414
+ classes.push({
3415
+ name: decl.name,
3416
+ fields
3417
+ });
3418
+ }
3419
+ }
3420
+ return classes;
3421
+ }
3422
+ function renderVanillaValueClassConstructorArgs(fields) {
3423
+ let lastRequiredIndex = -1;
3424
+ for (let i = 0; i < fields.length; i += 1) {
3425
+ if (!fields[i].optional) {
3426
+ lastRequiredIndex = i;
3427
+ }
3428
+ }
3429
+ return fields.map((field, index) => {
3430
+ const tsType = mapTypeTextToTs(field.typeText);
3431
+ if (!field.optional) {
3432
+ return `${field.name}: ${tsType}`;
3433
+ }
3434
+ if (index > lastRequiredIndex) {
3435
+ return `${field.name}?: ${tsType}`;
3436
+ }
3437
+ return `${field.name}: ${tsType} | undefined`;
3438
+ }).join(", ");
3439
+ }
3440
+ function renderVanillaValueClassTs(model) {
3441
+ const fieldLines = model.fields.map((field) => {
3442
+ const tsType = mapTypeTextToTs(field.typeText);
3443
+ return field.optional
3444
+ ? ` ${field.name}?: ${tsType};`
3445
+ : ` ${field.name}: ${tsType};`;
3446
+ });
3447
+ const constructorArgs = renderVanillaValueClassConstructorArgs(model.fields);
3448
+ const constructorBody = model.fields.map((field) => ` this.${field.name} = ${field.name};`);
3449
+ const constructorLines = [
3450
+ ` constructor(${constructorArgs}) {`,
3451
+ ...constructorBody,
3452
+ " }"
3453
+ ];
3454
+ return `class ${model.name} {
3455
+ ${fieldLines.join("\n")}
3456
+
3457
+ ${constructorLines.join("\n")}
3458
+ }`;
3459
+ }
3460
+ function renderVanillaValueClassDts(model) {
3461
+ const fieldLines = model.fields.map((field) => {
3462
+ const tsType = mapTypeTextToTs(field.typeText);
3463
+ return field.optional
3464
+ ? ` ${field.name}?: ${tsType};`
3465
+ : ` ${field.name}: ${tsType};`;
3466
+ });
3467
+ const constructorArgs = renderVanillaValueClassConstructorArgs(model.fields);
3468
+ return `interface ${model.name} {
3469
+ ${fieldLines.join("\n")}
3470
+ }
3471
+
3472
+ declare const ${model.name}: {
3473
+ new (${constructorArgs}): ${model.name};
3474
+ prototype: ${model.name};
3475
+ };`;
3476
+ }
3477
+ function renderVanillaBrowserTs(spec, codecCatalog, emitExports = false) {
3374
3478
  const localTypeDecls = renderTypeDeclarations(spec).trim();
3375
3479
  const localTypesBlock = localTypeDecls.length > 0 ? `${localTypeDecls}\n\n` : "";
3480
+ const valueClasses = collectVanillaValueClasses(spec);
3481
+ const valueClassDecls = valueClasses.map((model) => renderVanillaValueClassTs(model)).join("\n\n");
3482
+ const valueClassBlock = valueClassDecls.length > 0 ? `${valueClassDecls}\n\n` : "";
3376
3483
  const dragDropHelperBlock = renderTsDragDropPayloadHelpers(spec, codecCatalog).trim();
3377
3484
  const dragDropHelpers = dragDropHelperBlock.length > 0 ? `\n// Drag/drop payload helpers\n${dragDropHelperBlock}\n` : "";
3378
3485
  const serviceClasses = spec.services.map((s) => renderVanillaServiceTs(spec, s.name, codecCatalog)).join("\n");
3379
- const frontendServices = spec.services.length > 0
3380
- ? spec.services.map((s) => ` ${s.name}: ${s.name};`).join("\n")
3381
- : "";
3382
- const frontendServiceFactories = spec.services.length > 0
3383
- ? spec.services.map((s) => ` ${s.name}: new ${s.name}(bridge)`).join(",\n")
3486
+ const frontendShapeLines = [
3487
+ " diagnostics: AnQstBridgeDiagnostics;",
3488
+ ...spec.services.map((s) => ` ${s.name}: ${s.name};`),
3489
+ ...valueClasses.map((model) => ` ${model.name}: typeof ${model.name};`)
3490
+ ];
3491
+ const frontendFactoryLines = [
3492
+ " diagnostics: new AnQstBridgeDiagnostics(bridge)",
3493
+ ...spec.services.map((s) => ` ${s.name}: new ${s.name}(bridge)`),
3494
+ ...valueClasses.map((model) => ` ${model.name}`)
3495
+ ];
3496
+ const exportedRuntimeSymbols = [
3497
+ "AnQstBridgeDiagnostics",
3498
+ ...spec.services.map((s) => s.name),
3499
+ ...valueClasses.map((model) => model.name),
3500
+ "createFrontend"
3501
+ ];
3502
+ const exportedTypeSymbols = [
3503
+ "AnQstBridgeDiagnostic",
3504
+ "AnQstBridgeState",
3505
+ `${spec.widgetName}Frontend`,
3506
+ `${spec.widgetName}Global`,
3507
+ "AnQstGeneratedRoot"
3508
+ ];
3509
+ const exportsBlock = emitExports
3510
+ ? `\nexport { ${exportedRuntimeSymbols.join(", ")} };\nexport type { ${exportedTypeSymbols.join(", ")} };\n`
3384
3511
  : "";
3385
- return `${localTypesBlock}// Boundary codec plan helpers
3512
+ return `${localTypesBlock}${valueClassBlock}// Boundary codec plan helpers
3386
3513
  ${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
3387
3514
  ${dragDropHelpers}
3388
3515
 
@@ -4200,34 +4327,25 @@ class AnQstBridgeDiagnostics {
4200
4327
  }
4201
4328
 
4202
4329
  ${serviceClasses}
4203
- interface ${spec.widgetName}FrontendServices {
4204
- ${frontendServices}
4205
- }
4206
-
4207
4330
  interface ${spec.widgetName}Frontend {
4208
- diagnostics: AnQstBridgeDiagnostics;
4209
- services: ${spec.widgetName}FrontendServices;
4331
+ ${frontendShapeLines.join("\n")}
4210
4332
  }
4211
4333
 
4212
4334
  async function createFrontend(): Promise<${spec.widgetName}Frontend> {
4213
4335
  const bridge = new AnQstBridgeRuntime();
4214
4336
  await bridge.ready();
4215
4337
  return {
4216
- diagnostics: new AnQstBridgeDiagnostics(bridge),
4217
- services: {
4218
- ${frontendServiceFactories}
4219
- }
4338
+ ${frontendFactoryLines.join(",\n")}
4220
4339
  };
4221
4340
  }
4222
4341
 
4223
- (function bootstrapAnQstGenerated(global: typeof globalThis & { AnQstGenerated?: { widgets?: Record<string, unknown> } }) {
4342
+ (function bootstrapAnQstGenerated(global: typeof globalThis & { AnQstGenerated?: Record<string, unknown> }) {
4224
4343
  const root = global.AnQstGenerated ?? (global.AnQstGenerated = {});
4225
- const widgets = root.widgets ?? (root.widgets = {});
4226
- widgets["${spec.widgetName}"] = {
4344
+ root["${spec.widgetName}"] = {
4227
4345
  createFrontend
4228
4346
  };
4229
- })(window as typeof globalThis & { AnQstGenerated?: { widgets?: Record<string, unknown> } });
4230
- `;
4347
+ })(window as typeof globalThis & { AnQstGenerated?: Record<string, unknown> });
4348
+ ${exportsBlock}`;
4231
4349
  }
4232
4350
  function renderVanillaServiceDts(spec, serviceName) {
4233
4351
  const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
@@ -4289,16 +4407,34 @@ ${declareBodyLines.join("\n")}
4289
4407
  }`;
4290
4408
  }
4291
4409
  function renderVanillaIndexDts(spec) {
4292
- const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}/index.d.ts`).trim();
4410
+ const valueClasses = collectVanillaValueClasses(spec);
4411
+ const valueClassNames = new Set(valueClasses.map((model) => model.name));
4412
+ const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}/index.d.ts`, valueClassNames).trim();
4293
4413
  // Export widget namespace types so other packages can `import type { ... }` from this declaration file
4294
4414
  // (e.g. a second widget spec that reuses structs generated for the first widget).
4295
4415
  const localTypeDecls = renderTypeDeclarations(spec, true).trim();
4416
+ const valueClassDecls = valueClasses.map((model) => renderVanillaValueClassDts(model)).join("\n\n");
4296
4417
  const serviceDecls = spec.services.map((s) => renderVanillaServiceDts(spec, s.name)).join("\n\n");
4297
- const servicesShape = spec.services.length > 0
4298
- ? spec.services.map((s) => ` ${s.name}: ${s.name};`).join("\n")
4299
- : "";
4300
- const sections = [externalTypeImports, localTypeDecls].filter((s) => s.length > 0);
4418
+ const frontendShapeLines = [
4419
+ " diagnostics: AnQstBridgeDiagnostics;",
4420
+ ...spec.services.map((s) => ` ${s.name}: ${s.name};`),
4421
+ ...valueClasses.map((model) => ` ${model.name}: typeof ${model.name};`)
4422
+ ];
4423
+ const sections = [externalTypeImports, localTypeDecls, valueClassDecls].filter((s) => s.length > 0);
4301
4424
  const prelude = sections.length > 0 ? `${sections.join("\n\n")}\n\n` : "";
4425
+ const exportedRuntimeSymbols = [
4426
+ "AnQstBridgeDiagnostics",
4427
+ ...spec.services.map((s) => s.name),
4428
+ ...valueClasses.map((model) => model.name),
4429
+ "createFrontend"
4430
+ ];
4431
+ const exportedTypeSymbols = [
4432
+ "AnQstBridgeDiagnostic",
4433
+ "AnQstBridgeState",
4434
+ `${spec.widgetName}Frontend`,
4435
+ `${spec.widgetName}Global`,
4436
+ "AnQstGeneratedRoot"
4437
+ ];
4302
4438
  return `export {};
4303
4439
  ${prelude}type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
4304
4440
 
@@ -4331,25 +4467,18 @@ declare class AnQstBridgeDiagnostics {
4331
4467
 
4332
4468
  ${serviceDecls}
4333
4469
 
4334
- interface ${spec.widgetName}FrontendServices {
4335
- ${servicesShape}
4336
- }
4337
-
4338
4470
  interface ${spec.widgetName}Frontend {
4339
- diagnostics: AnQstBridgeDiagnostics;
4340
- services: ${spec.widgetName}FrontendServices;
4471
+ ${frontendShapeLines.join("\n")}
4341
4472
  }
4342
4473
 
4474
+ declare function createFrontend(): Promise<${spec.widgetName}Frontend>;
4475
+
4343
4476
  interface ${spec.widgetName}Global {
4344
4477
  createFrontend(): Promise<${spec.widgetName}Frontend>;
4345
4478
  }
4346
4479
 
4347
- interface AnQstGeneratedWidgets {
4348
- ${spec.widgetName}: ${spec.widgetName}Global;
4349
- }
4350
-
4351
4480
  interface AnQstGeneratedRoot {
4352
- widgets: AnQstGeneratedWidgets;
4481
+ ${spec.widgetName}: ${spec.widgetName}Global;
4353
4482
  }
4354
4483
 
4355
4484
  declare global {
@@ -4359,6 +4488,9 @@ declare global {
4359
4488
 
4360
4489
  var AnQstGenerated: AnQstGeneratedRoot;
4361
4490
  }
4491
+
4492
+ export { ${exportedRuntimeSymbols.join(", ")} };
4493
+ export type { ${exportedTypeSymbols.join(", ")} };
4362
4494
  `;
4363
4495
  }
4364
4496
  function transpileBrowserTsToJs(source) {
@@ -5205,10 +5337,12 @@ function generateOutputs(spec, options = {}) {
5205
5337
  outputs[`${angularFrontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
5206
5338
  }
5207
5339
  if (normalizedOptions.emitVanillaTS || normalizedOptions.emitVanillaJS) {
5208
- const vanillaBrowserTs = renderVanillaBrowserTs(spec, codecCatalog);
5209
- const vanillaBrowserJs = transpileBrowserTsToJs(vanillaBrowserTs);
5340
+ const vanillaBrowserTs = renderVanillaBrowserTs(spec, codecCatalog, true);
5341
+ const vanillaBrowserScriptTs = renderVanillaBrowserTs(spec, codecCatalog, false);
5342
+ const vanillaBrowserJs = transpileBrowserTsToJs(vanillaBrowserScriptTs);
5210
5343
  if (normalizedOptions.emitVanillaTS) {
5211
5344
  outputs[`${vanillaTsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaTS");
5345
+ outputs[`${vanillaTsFrontendDir}/index.ts`] = vanillaBrowserTs;
5212
5346
  outputs[`${vanillaTsFrontendDir}/index.js`] = vanillaBrowserJs;
5213
5347
  outputs[`${vanillaTsFrontendDir}/index.d.ts`] = renderVanillaIndexDts(spec);
5214
5348
  }
@@ -5241,6 +5375,52 @@ function writeGeneratedOutputs(cwd, outputs) {
5241
5375
  node_fs_1.default.writeFileSync(filePath, withBuildStamp(relPath, content), "utf8");
5242
5376
  }
5243
5377
  }
5378
+ function normalizeVanillaJsIndexHtml(html) {
5379
+ let normalized = html.replace(/<script\b[^>]*src=["'](?:\.\/)?main\.js["'][^>]*>\s*<\/script>\s*/gi, "");
5380
+ const bundleScriptTag = ' <script defer src="./main.js"></script>\n';
5381
+ if (/<\/head>/i.test(normalized)) {
5382
+ normalized = normalized.replace(/<\/head>/i, `${bundleScriptTag}</head>`);
5383
+ }
5384
+ else if (/<\/body>/i.test(normalized)) {
5385
+ normalized = normalized.replace(/<\/body>/i, `${bundleScriptTag}</body>`);
5386
+ }
5387
+ else {
5388
+ normalized = `${normalized}\n${bundleScriptTag}`;
5389
+ }
5390
+ return normalized;
5391
+ }
5392
+ function buildVanillaJsBrowserBundle(cwd, widgetName) {
5393
+ const srcRoot = node_path_1.default.join(cwd, "src");
5394
+ const srcIndexPath = node_path_1.default.join(srcRoot, "index.html");
5395
+ const srcMainPath = node_path_1.default.join(srcRoot, "main.js");
5396
+ const generatedRuntimePath = node_path_1.default.join((0, layout_1.resolveGeneratedLayoutPaths)(cwd, widgetName).vanillaJsFrontendRoot, "index.js");
5397
+ if (!node_fs_1.default.existsSync(srcIndexPath)) {
5398
+ throw new Error("VanillaJS build requires src/index.html.");
5399
+ }
5400
+ if (!node_fs_1.default.existsSync(srcMainPath)) {
5401
+ throw new Error("VanillaJS build requires src/main.js.");
5402
+ }
5403
+ if (!node_fs_1.default.existsSync(generatedRuntimePath)) {
5404
+ throw new Error("VanillaJS generated runtime is missing. Run generation before packaging the browser bundle.");
5405
+ }
5406
+ const distWebRoot = node_path_1.default.join(cwd, "dist", "browser");
5407
+ node_fs_1.default.rmSync(distWebRoot, { recursive: true, force: true });
5408
+ node_fs_1.default.mkdirSync(distWebRoot, { recursive: true });
5409
+ copyDirectoryRecursive(srcRoot, distWebRoot);
5410
+ const generatedRuntime = node_fs_1.default.readFileSync(generatedRuntimePath, "utf8").trimEnd();
5411
+ const appMain = node_fs_1.default.readFileSync(srcMainPath, "utf8").trim();
5412
+ const bundleSource = `${generatedRuntime}
5413
+
5414
+ ${appMain}
5415
+
5416
+ void main(window, document, window.AnQstGenerated);
5417
+ `;
5418
+ node_fs_1.default.writeFileSync(node_path_1.default.join(distWebRoot, "main.js"), withBuildStamp("dist/browser/main.js", bundleSource), "utf8");
5419
+ const sourceIndexHtml = node_fs_1.default.readFileSync(srcIndexPath, "utf8");
5420
+ const normalizedIndexHtml = normalizeVanillaJsIndexHtml(sourceIndexHtml);
5421
+ node_fs_1.default.writeFileSync(node_path_1.default.join(distWebRoot, "index.html"), withBuildStamp("dist/browser/index.html", normalizedIndexHtml), "utf8");
5422
+ return distWebRoot;
5423
+ }
5244
5424
  function listFilesRecursively(rootDir) {
5245
5425
  const output = [];
5246
5426
  const queue = [rootDir];
@@ -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,
@@ -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.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Opinionated backend generator for webapps.",
5
5
  "keywords": [
6
6
  "nodejs",