@ttmg/cli 0.3.8 → 0.3.9-beta.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.
Files changed (114) hide show
  1. package/dist/index.js +2155 -1293
  2. package/dist/index.js.map +1 -1
  3. package/dist/package.json +7 -2
  4. package/dist/public/assets/Card-CTv745_w.js +1 -0
  5. package/dist/public/assets/Detail-DgPwDVds.js +1 -0
  6. package/dist/public/assets/Detail-DgPwDVds.js.br +0 -0
  7. package/dist/public/assets/MonetizationMode-CqG7VmZX.js +1 -0
  8. package/dist/public/assets/MonetizationModeSummary-BSmi2Vjs.js +1 -0
  9. package/dist/public/assets/SectionHeader-BFCjOsAZ.js +1 -0
  10. package/dist/public/assets/Tag-Cwc1Bal2.js +1 -0
  11. package/dist/public/assets/arrow-left-DrT44p81.js +1 -0
  12. package/dist/public/assets/baseForm-DY0XNYDE.js +10 -0
  13. package/dist/public/assets/baseForm-DY0XNYDE.js.br +0 -0
  14. package/dist/public/assets/baseForm-Dl4zA6hU.css +1 -0
  15. package/dist/public/assets/baseForm-Dl4zA6hU.css.br +0 -0
  16. package/dist/public/assets/chevron-right-DzleLIdl.js +1 -0
  17. package/dist/public/assets/compass-BW4vGO0x.js +1 -0
  18. package/dist/public/assets/index-4NS2mMuQ.css +1 -0
  19. package/dist/public/assets/index-B-AeuNlL.css +1 -0
  20. package/dist/public/assets/index-B-AeuNlL.css.br +0 -0
  21. package/dist/public/assets/index-B3e59WZW.js +1 -0
  22. package/dist/public/assets/{index-tOg_vZEc.js → index-B6NHbQwP.js} +1 -1
  23. package/dist/public/assets/index-B6RZ7AdI.js +1 -0
  24. package/dist/public/assets/index-B6RZ7AdI.js.br +0 -0
  25. package/dist/public/assets/index-BAud2cRu.css +1 -0
  26. package/dist/public/assets/index-BBuCNszs.js +1 -0
  27. package/dist/public/assets/index-BJ1WI0xW.js +14 -0
  28. package/dist/public/assets/index-BJ1WI0xW.js.br +0 -0
  29. package/dist/public/assets/index-BOl1-Siv.css +1 -0
  30. package/dist/public/assets/index-BOl1-Siv.css.br +0 -0
  31. package/dist/public/assets/index-BQiwR45B.js +1 -0
  32. package/dist/public/assets/index-BR2iFF5J.js +1 -0
  33. package/dist/public/assets/index-BR2iFF5J.js.br +0 -0
  34. package/dist/public/assets/index-BbSY5AoV.js +1 -0
  35. package/dist/public/assets/index-BmP2LC8F.js +1 -0
  36. package/dist/public/assets/index-Bmw61rl1.css +1 -0
  37. package/dist/public/assets/index-Bmw61rl1.css.br +0 -0
  38. package/dist/public/assets/index-BqUsIqrD.js +1 -0
  39. package/dist/public/assets/index-BqUsIqrD.js.br +0 -0
  40. package/dist/public/assets/index-BrC0aWmM.js +1 -0
  41. package/dist/public/assets/index-BsEJ06u7.js +1 -0
  42. package/dist/public/assets/index-C-tTmNa4.css +1 -0
  43. package/dist/public/assets/index-C0slAX90.js +1 -0
  44. package/dist/public/assets/index-C0slAX90.js.br +0 -0
  45. package/dist/public/assets/index-CBlaFQAe.js +1 -0
  46. package/dist/public/assets/index-CBlaFQAe.js.br +0 -0
  47. package/dist/public/assets/index-CCIR5x04.js +1 -0
  48. package/dist/public/assets/index-CVrG-aCP.js +1 -0
  49. package/dist/public/assets/index-CYONSh3E.js +1 -0
  50. package/dist/public/assets/index-Cc1ilXmc.css +1 -0
  51. package/dist/public/assets/index-CmVUh50W.css +1 -0
  52. package/dist/public/assets/index-Crx61Qjc.css +1 -0
  53. package/dist/public/assets/index-D-GbEkoB.css +1 -0
  54. package/dist/public/assets/index-DYNaiQIB.js +1 -0
  55. package/dist/public/assets/index-Dvg_oNs7.css +1 -0
  56. package/dist/public/assets/{index-D2LsTDVa.css → index-ROKxx4f7.css} +1 -1
  57. package/dist/public/assets/index-ROKxx4f7.css.br +0 -0
  58. package/dist/public/assets/index-RYY-l6Oq.css +1 -0
  59. package/dist/public/assets/index-TElbHITb.js +1 -0
  60. package/dist/public/assets/index-Yv0czelv.js +1 -0
  61. package/dist/public/assets/index-ZYC8SS17.js +1 -0
  62. package/dist/public/assets/index-faRHENEQ.css +1 -0
  63. package/dist/public/assets/sparkles-KMi7bfJ3.js +1 -0
  64. package/dist/public/assets/zap-BlvSLoGD.js +1 -0
  65. package/dist/public/index.html +13 -3
  66. package/dist/scripts/dev-debug.js +191 -0
  67. package/package.json +7 -2
  68. package/CHANGELOG.md +0 -228
  69. package/dist/public/assets/Detail-DuoflIDB.js +0 -1
  70. package/dist/public/assets/Detail-DuoflIDB.js.br +0 -0
  71. package/dist/public/assets/MonetizationMode-C6-xgErM.js +0 -1
  72. package/dist/public/assets/MonetizationModeSummary-v1a9_YaN.js +0 -1
  73. package/dist/public/assets/baseForm-CB6KCNqW.css +0 -1
  74. package/dist/public/assets/baseForm-CB6KCNqW.css.br +0 -0
  75. package/dist/public/assets/baseForm-DqyLTNAx.js +0 -10
  76. package/dist/public/assets/baseForm-DqyLTNAx.js.br +0 -0
  77. package/dist/public/assets/index-88dZ53Te.css +0 -1
  78. package/dist/public/assets/index-88dZ53Te.css.br +0 -0
  79. package/dist/public/assets/index-Bf6aJOeV.css +0 -1
  80. package/dist/public/assets/index-Bf85t01Q.css +0 -1
  81. package/dist/public/assets/index-Bf85t01Q.css.br +0 -0
  82. package/dist/public/assets/index-BmcEpXdh.js +0 -1
  83. package/dist/public/assets/index-BmcEpXdh.js.br +0 -0
  84. package/dist/public/assets/index-BnU4EHWL.css +0 -1
  85. package/dist/public/assets/index-BnU4EHWL.css.br +0 -0
  86. package/dist/public/assets/index-Bpba_DWs.js +0 -1
  87. package/dist/public/assets/index-C06KDNuj.css +0 -1
  88. package/dist/public/assets/index-CH7igbHY.css +0 -1
  89. package/dist/public/assets/index-CLgcHgzd.css +0 -1
  90. package/dist/public/assets/index-CPJp_0l2.js +0 -1
  91. package/dist/public/assets/index-ClpFHzCy.js +0 -1
  92. package/dist/public/assets/index-Crf11eTs.js +0 -1
  93. package/dist/public/assets/index-Crf11eTs.js.br +0 -0
  94. package/dist/public/assets/index-CwihbwhD.js +0 -1
  95. package/dist/public/assets/index-D2LsTDVa.css.br +0 -0
  96. package/dist/public/assets/index-DJTcocTh.js +0 -1
  97. package/dist/public/assets/index-DNsJSSmy.css +0 -1
  98. package/dist/public/assets/index-DO22Abni.js +0 -1
  99. package/dist/public/assets/index-DPSts5Re.css +0 -1
  100. package/dist/public/assets/index-DPSts5Re.css.br +0 -0
  101. package/dist/public/assets/index-DWw-gYwj.js +0 -1
  102. package/dist/public/assets/index-DWw-gYwj.js.br +0 -0
  103. package/dist/public/assets/index-Dc800O6M.js +0 -1
  104. package/dist/public/assets/index-De7XyxJU.js +0 -1
  105. package/dist/public/assets/index-Pfoc2Cwl.js +0 -14
  106. package/dist/public/assets/index-Pfoc2Cwl.js.br +0 -0
  107. package/dist/public/assets/index-SOEihm3l.js +0 -1
  108. package/dist/public/assets/index-dSgab9d_.js +0 -1
  109. package/dist/public/assets/index-dSgab9d_.js.br +0 -0
  110. package/dist/public/assets/index-iUN2bzlN.js +0 -1
  111. package/dist/public/assets/index-yXoFuCJJ.js +0 -1
  112. package/dist/public/assets/index-yXoFuCJJ.js.br +0 -0
  113. package/dist/public/assets/times-BsA3kTAU.js +0 -1
  114. package/dist/public/assets/times-C7C5ulLg.css +0 -1
package/dist/index.js CHANGED
@@ -38,9 +38,12 @@ var FormData$1 = require('form-data');
38
38
  var ttmgPack = require('ttmg-pack');
39
39
  var expressStaticGzip = require('express-static-gzip');
40
40
  var fileUpload = require('express-fileupload');
41
+ var ttmgWasmtool = require('@anrans001/ttmg-wasmtool');
42
+ var crypto$1 = require('node:crypto');
41
43
  var fs$1 = require('node:fs');
42
44
  var path$1 = require('node:path');
43
45
  var zlib = require('zlib');
46
+ var crypto = require('crypto');
44
47
  var promises = require('fs/promises');
45
48
  var qs = require('qs');
46
49
 
@@ -69,6 +72,8 @@ var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
69
72
  var dns__namespace = /*#__PURE__*/_interopNamespaceDefault(dns);
70
73
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
71
74
  var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
75
+ var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
76
+ var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
72
77
 
73
78
  async function openUrl(url) {
74
79
  const { launch } = await import('chrome-launcher');
@@ -584,17 +589,17 @@ var hasRequiredBrowser;
584
589
  function requireBrowser () {
585
590
  if (hasRequiredBrowser) return browser.exports;
586
591
  hasRequiredBrowser = 1;
587
- (function (module, exports) {
592
+ (function (module, exports$1) {
588
593
  /**
589
594
  * This is the web browser implementation of `debug()`.
590
595
  */
591
596
 
592
- exports.formatArgs = formatArgs;
593
- exports.save = save;
594
- exports.load = load;
595
- exports.useColors = useColors;
596
- exports.storage = localstorage();
597
- exports.destroy = (() => {
597
+ exports$1.formatArgs = formatArgs;
598
+ exports$1.save = save;
599
+ exports$1.load = load;
600
+ exports$1.useColors = useColors;
601
+ exports$1.storage = localstorage();
602
+ exports$1.destroy = (() => {
598
603
  let warned = false;
599
604
 
600
605
  return () => {
@@ -609,7 +614,7 @@ function requireBrowser () {
609
614
  * Colors.
610
615
  */
611
616
 
612
- exports.colors = [
617
+ exports$1.colors = [
613
618
  '#0000CC',
614
619
  '#0000FF',
615
620
  '#0033CC',
@@ -774,7 +779,7 @@ function requireBrowser () {
774
779
  *
775
780
  * @api public
776
781
  */
777
- exports.log = console.debug || console.log || (() => {});
782
+ exports$1.log = console.debug || console.log || (() => {});
778
783
 
779
784
  /**
780
785
  * Save `namespaces`.
@@ -785,9 +790,9 @@ function requireBrowser () {
785
790
  function save(namespaces) {
786
791
  try {
787
792
  if (namespaces) {
788
- exports.storage.setItem('debug', namespaces);
793
+ exports$1.storage.setItem('debug', namespaces);
789
794
  } else {
790
- exports.storage.removeItem('debug');
795
+ exports$1.storage.removeItem('debug');
791
796
  }
792
797
  } catch (error) {
793
798
  // Swallow
@@ -804,7 +809,7 @@ function requireBrowser () {
804
809
  function load() {
805
810
  let r;
806
811
  try {
807
- r = exports.storage.getItem('debug') || exports.storage.getItem('DEBUG') ;
812
+ r = exports$1.storage.getItem('debug') || exports$1.storage.getItem('DEBUG') ;
808
813
  } catch (error) {
809
814
  // Swallow
810
815
  // XXX (@Qix-) should we be logging these?
@@ -840,7 +845,7 @@ function requireBrowser () {
840
845
  }
841
846
  }
842
847
 
843
- module.exports = requireCommon$1()(exports);
848
+ module.exports = requireCommon$1()(exports$1);
844
849
 
845
850
  const {formatters} = module.exports;
846
851
 
@@ -1029,7 +1034,7 @@ var hasRequiredNode;
1029
1034
  function requireNode () {
1030
1035
  if (hasRequiredNode) return node.exports;
1031
1036
  hasRequiredNode = 1;
1032
- (function (module, exports) {
1037
+ (function (module, exports$1) {
1033
1038
  const tty = require$$1;
1034
1039
  const util = require$$1$1;
1035
1040
 
@@ -1037,13 +1042,13 @@ function requireNode () {
1037
1042
  * This is the Node.js implementation of `debug()`.
1038
1043
  */
1039
1044
 
1040
- exports.init = init;
1041
- exports.log = log;
1042
- exports.formatArgs = formatArgs;
1043
- exports.save = save;
1044
- exports.load = load;
1045
- exports.useColors = useColors;
1046
- exports.destroy = util.deprecate(
1045
+ exports$1.init = init;
1046
+ exports$1.log = log;
1047
+ exports$1.formatArgs = formatArgs;
1048
+ exports$1.save = save;
1049
+ exports$1.load = load;
1050
+ exports$1.useColors = useColors;
1051
+ exports$1.destroy = util.deprecate(
1047
1052
  () => {},
1048
1053
  'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'
1049
1054
  );
@@ -1052,7 +1057,7 @@ function requireNode () {
1052
1057
  * Colors.
1053
1058
  */
1054
1059
 
1055
- exports.colors = [6, 2, 3, 4, 5, 1];
1060
+ exports$1.colors = [6, 2, 3, 4, 5, 1];
1056
1061
 
1057
1062
  try {
1058
1063
  // Optional dependency (as in, doesn't need to be installed, NOT like optionalDependencies in package.json)
@@ -1060,7 +1065,7 @@ function requireNode () {
1060
1065
  const supportsColor = requireSupportsColor();
1061
1066
 
1062
1067
  if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
1063
- exports.colors = [
1068
+ exports$1.colors = [
1064
1069
  20,
1065
1070
  21,
1066
1071
  26,
@@ -1149,7 +1154,7 @@ function requireNode () {
1149
1154
  * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
1150
1155
  */
1151
1156
 
1152
- exports.inspectOpts = Object.keys(process.env).filter(key => {
1157
+ exports$1.inspectOpts = Object.keys(process.env).filter(key => {
1153
1158
  return /^debug_/i.test(key);
1154
1159
  }).reduce((obj, key) => {
1155
1160
  // Camel-case
@@ -1181,8 +1186,8 @@ function requireNode () {
1181
1186
  */
1182
1187
 
1183
1188
  function useColors() {
1184
- return 'colors' in exports.inspectOpts ?
1185
- Boolean(exports.inspectOpts.colors) :
1189
+ return 'colors' in exports$1.inspectOpts ?
1190
+ Boolean(exports$1.inspectOpts.colors) :
1186
1191
  tty.isatty(process.stderr.fd);
1187
1192
  }
1188
1193
 
@@ -1208,7 +1213,7 @@ function requireNode () {
1208
1213
  }
1209
1214
 
1210
1215
  function getDate() {
1211
- if (exports.inspectOpts.hideDate) {
1216
+ if (exports$1.inspectOpts.hideDate) {
1212
1217
  return '';
1213
1218
  }
1214
1219
  return new Date().toISOString() + ' ';
@@ -1219,7 +1224,7 @@ function requireNode () {
1219
1224
  */
1220
1225
 
1221
1226
  function log(...args) {
1222
- return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + '\n');
1227
+ return process.stderr.write(util.formatWithOptions(exports$1.inspectOpts, ...args) + '\n');
1223
1228
  }
1224
1229
 
1225
1230
  /**
@@ -1259,13 +1264,13 @@ function requireNode () {
1259
1264
  function init(debug) {
1260
1265
  debug.inspectOpts = {};
1261
1266
 
1262
- const keys = Object.keys(exports.inspectOpts);
1267
+ const keys = Object.keys(exports$1.inspectOpts);
1263
1268
  for (let i = 0; i < keys.length; i++) {
1264
- debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
1269
+ debug.inspectOpts[keys[i]] = exports$1.inspectOpts[keys[i]];
1265
1270
  }
1266
1271
  }
1267
1272
 
1268
- module.exports = requireCommon$1()(exports);
1273
+ module.exports = requireCommon$1()(exports$1);
1269
1274
 
1270
1275
  const {formatters} = module.exports;
1271
1276
 
@@ -3354,27 +3359,6 @@ var ipv4 = {};
3354
3359
 
3355
3360
  var common = {};
3356
3361
 
3357
- var addressError = {};
3358
-
3359
- var hasRequiredAddressError;
3360
-
3361
- function requireAddressError () {
3362
- if (hasRequiredAddressError) return addressError;
3363
- hasRequiredAddressError = 1;
3364
- Object.defineProperty(addressError, "__esModule", { value: true });
3365
- addressError.AddressError = void 0;
3366
- class AddressError extends Error {
3367
- constructor(message, parseMessage) {
3368
- super(message);
3369
- this.name = 'AddressError';
3370
- this.parseMessage = parseMessage;
3371
- }
3372
- }
3373
- addressError.AddressError = AddressError;
3374
-
3375
- return addressError;
3376
- }
3377
-
3378
3362
  var hasRequiredCommon;
3379
3363
 
3380
3364
  function requireCommon () {
@@ -3383,11 +3367,9 @@ function requireCommon () {
3383
3367
  Object.defineProperty(common, "__esModule", { value: true });
3384
3368
  common.isInSubnet = isInSubnet;
3385
3369
  common.isCorrect = isCorrect;
3386
- common.prefixLengthFromMask = prefixLengthFromMask;
3387
3370
  common.numberToPaddedHex = numberToPaddedHex;
3388
3371
  common.stringToPaddedHex = stringToPaddedHex;
3389
3372
  common.testBit = testBit;
3390
- const address_error_1 = /*@__PURE__*/ requireAddressError();
3391
3373
  function isInSubnet(address) {
3392
3374
  if (this.subnetMask < address.subnetMask) {
3393
3375
  return false;
@@ -3408,25 +3390,6 @@ function requireCommon () {
3408
3390
  return this.parsedSubnet === String(this.subnetMask);
3409
3391
  };
3410
3392
  }
3411
- /**
3412
- * Returns the prefix length (number of leading 1 bits) of a contiguous
3413
- * subnet mask. Throws `AddressError` if the mask is non-contiguous (e.g.
3414
- * `255.0.255.0`).
3415
- */
3416
- function prefixLengthFromMask(value, totalBits) {
3417
- const binary = value.toString(2).padStart(totalBits, '0');
3418
- if (binary.length > totalBits) {
3419
- throw new address_error_1.AddressError('Invalid subnet mask.');
3420
- }
3421
- const firstZero = binary.indexOf('0');
3422
- if (firstZero === -1) {
3423
- return totalBits;
3424
- }
3425
- if (binary.slice(firstZero).includes('1')) {
3426
- throw new address_error_1.AddressError('Invalid subnet mask.');
3427
- }
3428
- return firstZero;
3429
- }
3430
3393
  function numberToPaddedHex(number) {
3431
3394
  return number.toString(16).padStart(2, '0');
3432
3395
  }
@@ -3466,6 +3429,27 @@ function requireConstants$1 () {
3466
3429
  return constants$1;
3467
3430
  }
3468
3431
 
3432
+ var addressError = {};
3433
+
3434
+ var hasRequiredAddressError;
3435
+
3436
+ function requireAddressError () {
3437
+ if (hasRequiredAddressError) return addressError;
3438
+ hasRequiredAddressError = 1;
3439
+ Object.defineProperty(addressError, "__esModule", { value: true });
3440
+ addressError.AddressError = void 0;
3441
+ class AddressError extends Error {
3442
+ constructor(message, parseMessage) {
3443
+ super(message);
3444
+ this.name = 'AddressError';
3445
+ this.parseMessage = parseMessage;
3446
+ }
3447
+ }
3448
+ addressError.AddressError = AddressError;
3449
+
3450
+ return addressError;
3451
+ }
3452
+
3469
3453
  var hasRequiredIpv4;
3470
3454
 
3471
3455
  function requireIpv4 () {
@@ -3497,12 +3481,12 @@ function requireIpv4 () {
3497
3481
  };
3498
3482
  Object.defineProperty(ipv4, "__esModule", { value: true });
3499
3483
  ipv4.Address4 = void 0;
3500
- const common = __importStar(/*@__PURE__*/ requireCommon());
3501
- const constants = __importStar(/*@__PURE__*/ requireConstants$1());
3502
- const address_error_1 = /*@__PURE__*/ requireAddressError();
3503
- const isCorrect4 = common.isCorrect(constants.BITS);
3484
+ const common = __importStar(requireCommon());
3485
+ const constants = __importStar(requireConstants$1());
3486
+ const address_error_1 = requireAddressError();
3504
3487
  /**
3505
3488
  * Represents an IPv4 address
3489
+ * @class Address4
3506
3490
  * @param {string} address - An IPv4 address string
3507
3491
  */
3508
3492
  class Address4 {
@@ -3515,11 +3499,15 @@ function requireIpv4 () {
3515
3499
  this.v4 = true;
3516
3500
  /**
3517
3501
  * Returns true if the address is correct, false otherwise
3502
+ * @memberof Address4
3503
+ * @instance
3518
3504
  * @returns {Boolean}
3519
3505
  */
3520
- this.isCorrect = isCorrect4;
3506
+ this.isCorrect = common.isCorrect(constants.BITS);
3521
3507
  /**
3522
3508
  * Returns true if the given address is in the subnet of the current address
3509
+ * @memberof Address4
3510
+ * @instance
3523
3511
  * @returns {boolean}
3524
3512
  */
3525
3513
  this.isInSubnet = common.isInSubnet;
@@ -3537,13 +3525,6 @@ function requireIpv4 () {
3537
3525
  this.addressMinusSuffix = address;
3538
3526
  this.parsedAddress = this.parse(address);
3539
3527
  }
3540
- /**
3541
- * Returns true if the given string is a valid IPv4 address (with optional
3542
- * CIDR subnet), false otherwise. Host bits in the subnet portion are
3543
- * allowed (e.g. `192.168.1.5/24` is valid); for strict network-address
3544
- * validation compare `correctForm()` to `startAddress().correctForm()`,
3545
- * or use `networkForm()`.
3546
- */
3547
3528
  static isValid(address) {
3548
3529
  try {
3549
3530
  // eslint-disable-next-line no-new
@@ -3554,11 +3535,8 @@ function requireIpv4 () {
3554
3535
  return false;
3555
3536
  }
3556
3537
  }
3557
- /**
3558
- * Parses an IPv4 address string into its four octet groups and stores the
3559
- * result on `this.parsedAddress`. Called automatically by the constructor;
3560
- * you typically don't need to call it directly. Throws `AddressError` if
3561
- * the input is not a valid IPv4 address.
3538
+ /*
3539
+ * Parses a v4 address
3562
3540
  */
3563
3541
  parse(address) {
3564
3542
  const groups = address.split('.');
@@ -3568,110 +3546,45 @@ function requireIpv4 () {
3568
3546
  return groups;
3569
3547
  }
3570
3548
  /**
3571
- * Returns the address in correct form: octets joined with `.` and any
3572
- * leading zeros stripped (e.g. `192.168.1.1`). For IPv4 this matches the
3573
- * canonical dotted-decimal representation.
3549
+ * Returns the correct form of an address
3550
+ * @memberof Address4
3551
+ * @instance
3552
+ * @returns {String}
3574
3553
  */
3575
3554
  correctForm() {
3576
3555
  return this.parsedAddress.map((part) => parseInt(part, 10)).join('.');
3577
3556
  }
3578
3557
  /**
3579
- * Construct an `Address4` from an address and a dotted-decimal subnet
3580
- * mask given as separate strings (e.g. as returned by Node's
3581
- * `os.networkInterfaces()`). Throws `AddressError` if the mask is
3582
- * non-contiguous (e.g. `255.0.255.0`).
3583
- * @example
3584
- * var address = Address4.fromAddressAndMask('192.168.1.1', '255.255.255.0');
3585
- * address.subnetMask; // 24
3586
- */
3587
- static fromAddressAndMask(address, mask) {
3588
- const bits = common.prefixLengthFromMask(new Address4(mask).bigInt(), constants.BITS);
3589
- return new Address4(`${address}/${bits}`);
3590
- }
3591
- /**
3592
- * Construct an `Address4` from an address and a Cisco-style wildcard mask
3593
- * given as separate strings (e.g. `0.0.0.255` for a `/24`). The wildcard
3594
- * mask is the bitwise inverse of the subnet mask. Throws `AddressError`
3595
- * if the mask is non-contiguous (e.g. `0.255.0.255`).
3596
- * @example
3597
- * var address = Address4.fromAddressAndWildcardMask('10.0.0.1', '0.0.0.255');
3598
- * address.subnetMask; // 24
3599
- */
3600
- static fromAddressAndWildcardMask(address, wildcardMask) {
3601
- const wildcard = new Address4(wildcardMask).bigInt();
3602
- const allOnes = (BigInt(1) << BigInt(constants.BITS)) - BigInt(1);
3603
- // eslint-disable-next-line no-bitwise
3604
- const mask = wildcard ^ allOnes;
3605
- const bits = common.prefixLengthFromMask(mask, constants.BITS);
3606
- return new Address4(`${address}/${bits}`);
3607
- }
3608
- /**
3609
- * Construct an `Address4` from a wildcard pattern with trailing `*`
3610
- * octets. The number of trailing wildcards determines the prefix
3611
- * length: each `*` represents 8 bits.
3612
- *
3613
- * Only trailing whole-octet wildcards are supported. Partial-octet
3614
- * wildcards (e.g. `192.168.0.1*`) and interior wildcards (e.g.
3615
- * `192.*.0.1`) throw `AddressError`.
3616
- * @example
3617
- * Address4.fromWildcard('192.168.0.*').subnet; // '/24'
3618
- * Address4.fromWildcard('192.168.*.*').subnet; // '/16'
3619
- * Address4.fromWildcard('*.*.*.*').subnet; // '/0'
3620
- */
3621
- static fromWildcard(input) {
3622
- const groups = input.split('.');
3623
- if (groups.length !== constants.GROUPS) {
3624
- throw new address_error_1.AddressError('Wildcard pattern must have 4 octets');
3625
- }
3626
- let firstWildcard = -1;
3627
- for (let i = 0; i < groups.length; i++) {
3628
- if (groups[i] === '*') {
3629
- if (firstWildcard === -1) {
3630
- firstWildcard = i;
3631
- }
3632
- }
3633
- else if (firstWildcard !== -1) {
3634
- throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing octets (e.g. `192.168.0.*`)');
3635
- }
3636
- }
3637
- const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard;
3638
- const replaced = groups.map((g) => (g === '*' ? '0' : g));
3639
- const subnetBits = constants.BITS - trailing * 8;
3640
- return new Address4(`${replaced.join('.')}/${subnetBits}`);
3641
- }
3642
- /**
3643
- * Converts a hex string to an IPv4 address object. Accepts 8 hex digits
3644
- * with optional `:` separators (e.g. `'7f000001'` or `'7f:00:00:01'`).
3645
- * Throws `AddressError` for any other length or for non-hex characters.
3558
+ * Converts a hex string to an IPv4 address object
3559
+ * @memberof Address4
3560
+ * @static
3646
3561
  * @param {string} hex - a hex string to convert
3647
3562
  * @returns {Address4}
3648
3563
  */
3649
3564
  static fromHex(hex) {
3650
- const stripped = hex.replace(/:/g, '');
3651
- if (!/^[0-9a-fA-F]{8}$/.test(stripped)) {
3652
- throw new address_error_1.AddressError('IPv4 hex must be exactly 8 hex digits');
3653
- }
3565
+ const padded = hex.replace(/:/g, '').padStart(8, '0');
3654
3566
  const groups = [];
3655
- for (let i = 0; i < 8; i += 2) {
3656
- groups.push(parseInt(stripped.slice(i, i + 2), 16));
3567
+ let i;
3568
+ for (i = 0; i < 8; i += 2) {
3569
+ const h = padded.slice(i, i + 2);
3570
+ groups.push(parseInt(h, 16));
3657
3571
  }
3658
3572
  return new Address4(groups.join('.'));
3659
3573
  }
3660
3574
  /**
3661
- * Converts an integer into a IPv4 address object. The integer must be a
3662
- * non-negative safe integer in the range `[0, 2**32 - 1]`; otherwise
3663
- * `AddressError` is thrown.
3575
+ * Converts an integer into a IPv4 address object
3576
+ * @memberof Address4
3577
+ * @static
3664
3578
  * @param {integer} integer - a number to convert
3665
3579
  * @returns {Address4}
3666
3580
  */
3667
3581
  static fromInteger(integer) {
3668
- if (!Number.isInteger(integer) || integer < 0 || integer > 0xffffffff) {
3669
- throw new address_error_1.AddressError('IPv4 integer must be in the range 0 to 2**32 - 1');
3670
- }
3671
- return Address4.fromHex(integer.toString(16).padStart(8, '0'));
3582
+ return Address4.fromHex(integer.toString(16));
3672
3583
  }
3673
3584
  /**
3674
3585
  * Return an address from in-addr.arpa form
3586
+ * @memberof Address4
3587
+ * @static
3675
3588
  * @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address
3676
3589
  * @returns {Adress4}
3677
3590
  * @example
@@ -3686,15 +3599,17 @@ function requireIpv4 () {
3686
3599
  }
3687
3600
  /**
3688
3601
  * Converts an IPv4 address object to a hex string
3602
+ * @memberof Address4
3603
+ * @instance
3689
3604
  * @returns {String}
3690
3605
  */
3691
3606
  toHex() {
3692
3607
  return this.parsedAddress.map((part) => common.stringToPaddedHex(part)).join(':');
3693
3608
  }
3694
3609
  /**
3695
- * Converts an IPv4 address object to an array of bytes.
3696
- *
3697
- * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toArray())`.
3610
+ * Converts an IPv4 address object to an array of bytes
3611
+ * @memberof Address4
3612
+ * @instance
3698
3613
  * @returns {Array}
3699
3614
  */
3700
3615
  toArray() {
@@ -3702,6 +3617,8 @@ function requireIpv4 () {
3702
3617
  }
3703
3618
  /**
3704
3619
  * Converts an IPv4 address object to an IPv6 address group
3620
+ * @memberof Address4
3621
+ * @instance
3705
3622
  * @returns {String}
3706
3623
  */
3707
3624
  toGroup6() {
@@ -3714,6 +3631,8 @@ function requireIpv4 () {
3714
3631
  }
3715
3632
  /**
3716
3633
  * Returns the address as a `bigint`
3634
+ * @memberof Address4
3635
+ * @instance
3717
3636
  * @returns {bigint}
3718
3637
  */
3719
3638
  bigInt() {
@@ -3721,6 +3640,8 @@ function requireIpv4 () {
3721
3640
  }
3722
3641
  /**
3723
3642
  * Helper function getting start address.
3643
+ * @memberof Address4
3644
+ * @instance
3724
3645
  * @returns {bigint}
3725
3646
  */
3726
3647
  _startAddress() {
@@ -3729,6 +3650,8 @@ function requireIpv4 () {
3729
3650
  /**
3730
3651
  * The first address in the range given by this address' subnet.
3731
3652
  * Often referred to as the Network Address.
3653
+ * @memberof Address4
3654
+ * @instance
3732
3655
  * @returns {Address4}
3733
3656
  */
3734
3657
  startAddress() {
@@ -3737,6 +3660,8 @@ function requireIpv4 () {
3737
3660
  /**
3738
3661
  * The first host address in the range given by this address's subnet ie
3739
3662
  * the first address after the Network Address
3663
+ * @memberof Address4
3664
+ * @instance
3740
3665
  * @returns {Address4}
3741
3666
  */
3742
3667
  startAddressExclusive() {
@@ -3745,6 +3670,8 @@ function requireIpv4 () {
3745
3670
  }
3746
3671
  /**
3747
3672
  * Helper function getting end address.
3673
+ * @memberof Address4
3674
+ * @instance
3748
3675
  * @returns {bigint}
3749
3676
  */
3750
3677
  _endAddress() {
@@ -3753,6 +3680,8 @@ function requireIpv4 () {
3753
3680
  /**
3754
3681
  * The last address in the range given by this address' subnet
3755
3682
  * Often referred to as the Broadcast
3683
+ * @memberof Address4
3684
+ * @instance
3756
3685
  * @returns {Address4}
3757
3686
  */
3758
3687
  endAddress() {
@@ -3761,6 +3690,8 @@ function requireIpv4 () {
3761
3690
  /**
3762
3691
  * The last host address in the range given by this address's subnet ie
3763
3692
  * the last address prior to the Broadcast Address
3693
+ * @memberof Address4
3694
+ * @instance
3764
3695
  * @returns {Address4}
3765
3696
  */
3766
3697
  endAddressExclusive() {
@@ -3768,47 +3699,19 @@ function requireIpv4 () {
3768
3699
  return Address4.fromBigInt(this._endAddress() - adjust);
3769
3700
  }
3770
3701
  /**
3771
- * The dotted-decimal form of the subnet mask, e.g. `255.255.240.0` for
3772
- * a `/20`. Returns an `Address4`; call `.correctForm()` for the string.
3773
- * @returns {Address4}
3774
- */
3775
- subnetMaskAddress() {
3776
- return Address4.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants.BITS - this.subnetMask)}`));
3777
- }
3778
- /**
3779
- * The Cisco-style wildcard mask, e.g. `0.0.0.255` for a `/24`. This is
3780
- * the bitwise inverse of `subnetMaskAddress()`. Returns an `Address4`;
3781
- * call `.correctForm()` for the string.
3782
- * @returns {Address4}
3783
- */
3784
- wildcardMask() {
3785
- return Address4.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants.BITS - this.subnetMask)}`));
3786
- }
3787
- /**
3788
- * The network address in CIDR string form, e.g. `192.168.1.0/24` for
3789
- * `192.168.1.5/24`. For an address with no explicit subnet the prefix is
3790
- * `/32`, e.g. `networkForm()` on `192.168.1.5` returns `192.168.1.5/32`.
3791
- * @returns {string}
3792
- */
3793
- networkForm() {
3794
- return `${this.startAddress().correctForm()}/${this.subnetMask}`;
3795
- }
3796
- /**
3797
- * Converts a BigInt to a v4 address object. The value must be in the
3798
- * range `[0, 2**32 - 1]`; otherwise `AddressError` is thrown.
3702
+ * Converts a BigInt to a v4 address object
3703
+ * @memberof Address4
3704
+ * @static
3799
3705
  * @param {bigint} bigInt - a BigInt to convert
3800
3706
  * @returns {Address4}
3801
3707
  */
3802
3708
  static fromBigInt(bigInt) {
3803
- if (bigInt < 0n || bigInt > 0xffffffffn) {
3804
- throw new address_error_1.AddressError('IPv4 BigInt must be in the range 0 to 2**32 - 1');
3805
- }
3806
- return Address4.fromHex(bigInt.toString(16).padStart(8, '0'));
3709
+ return Address4.fromHex(bigInt.toString(16));
3807
3710
  }
3808
3711
  /**
3809
- * Convert a byte array to an Address4 object.
3810
- *
3811
- * To convert from a Node.js `Buffer`, spread it: `Address4.fromByteArray([...buf])`.
3712
+ * Convert a byte array to an Address4 object
3713
+ * @memberof Address4
3714
+ * @static
3812
3715
  * @param {Array<number>} bytes - an array of 4 bytes (0-255)
3813
3716
  * @returns {Address4}
3814
3717
  */
@@ -3826,6 +3729,8 @@ function requireIpv4 () {
3826
3729
  }
3827
3730
  /**
3828
3731
  * Convert an unsigned byte array to an Address4 object
3732
+ * @memberof Address4
3733
+ * @static
3829
3734
  * @param {Array<number>} bytes - an array of 4 unsigned bytes (0-255)
3830
3735
  * @returns {Address4}
3831
3736
  */
@@ -3839,6 +3744,8 @@ function requireIpv4 () {
3839
3744
  /**
3840
3745
  * Returns the first n bits of the address, defaulting to the
3841
3746
  * subnet mask
3747
+ * @memberof Address4
3748
+ * @instance
3842
3749
  * @returns {String}
3843
3750
  */
3844
3751
  mask(mask) {
@@ -3849,6 +3756,8 @@ function requireIpv4 () {
3849
3756
  }
3850
3757
  /**
3851
3758
  * Returns the bits in the given range as a base-2 string
3759
+ * @memberof Address4
3760
+ * @instance
3852
3761
  * @returns {string}
3853
3762
  */
3854
3763
  getBitsBase2(start, end) {
@@ -3856,8 +3765,10 @@ function requireIpv4 () {
3856
3765
  }
3857
3766
  /**
3858
3767
  * Return the reversed ip6.arpa form of the address
3768
+ * @memberof Address4
3859
3769
  * @param {Object} options
3860
3770
  * @param {boolean} options.omitSuffix - omit the "in-addr.arpa" suffix
3771
+ * @instance
3861
3772
  * @returns {String}
3862
3773
  */
3863
3774
  reverseForm(options) {
@@ -3872,62 +3783,21 @@ function requireIpv4 () {
3872
3783
  }
3873
3784
  /**
3874
3785
  * Returns true if the given address is a multicast address
3786
+ * @memberof Address4
3787
+ * @instance
3875
3788
  * @returns {boolean}
3876
3789
  */
3877
3790
  isMulticast() {
3878
- return this.isInSubnet(MULTICAST_V4);
3879
- }
3880
- /**
3881
- * Returns true if the address is in one of the [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private address ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`).
3882
- * @returns {boolean}
3883
- */
3884
- isPrivate() {
3885
- return PRIVATE_V4.some((subnet) => this.isInSubnet(subnet));
3886
- }
3887
- /**
3888
- * Returns true if the address is in the loopback range `127.0.0.0/8` ([RFC 1122](https://datatracker.ietf.org/doc/html/rfc1122)).
3889
- * @returns {boolean}
3890
- */
3891
- isLoopback() {
3892
- return this.isInSubnet(LOOPBACK_V4);
3893
- }
3894
- /**
3895
- * Returns true if the address is in the link-local range `169.254.0.0/16` ([RFC 3927](https://datatracker.ietf.org/doc/html/rfc3927)).
3896
- * @returns {boolean}
3897
- */
3898
- isLinkLocal() {
3899
- return this.isInSubnet(LINK_LOCAL_V4);
3900
- }
3901
- /**
3902
- * Returns true if the address is the unspecified address `0.0.0.0`.
3903
- * @returns {boolean}
3904
- */
3905
- isUnspecified() {
3906
- return this.isInSubnet(UNSPECIFIED_V4);
3907
- }
3908
- /**
3909
- * Returns true if the address is the limited broadcast address `255.255.255.255` ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)).
3910
- * @returns {boolean}
3911
- */
3912
- isBroadcast() {
3913
- return this.isInSubnet(BROADCAST_V4);
3914
- }
3915
- /**
3916
- * Returns true if the address is in the carrier-grade NAT range `100.64.0.0/10` ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)).
3917
- * @returns {boolean}
3918
- */
3919
- isCGNAT() {
3920
- return this.isInSubnet(CGNAT_V4);
3791
+ return this.isInSubnet(new Address4('224.0.0.0/4'));
3921
3792
  }
3922
3793
  /**
3923
3794
  * Returns a zero-padded base-2 string representation of the address
3795
+ * @memberof Address4
3796
+ * @instance
3924
3797
  * @returns {string}
3925
3798
  */
3926
3799
  binaryZeroPad() {
3927
- if (this._binaryZeroPad === undefined) {
3928
- this._binaryZeroPad = this.bigInt().toString(2).padStart(constants.BITS, '0');
3929
- }
3930
- return this._binaryZeroPad;
3800
+ return this.bigInt().toString(2).padStart(constants.BITS, '0');
3931
3801
  }
3932
3802
  /**
3933
3803
  * Groups an IPv4 address for inclusion at the end of an IPv6 address
@@ -3943,17 +3813,6 @@ function requireIpv4 () {
3943
3813
  }
3944
3814
  }
3945
3815
  ipv4.Address4 = Address4;
3946
- const MULTICAST_V4 = new Address4('224.0.0.0/4');
3947
- const PRIVATE_V4 = [
3948
- new Address4('10.0.0.0/8'),
3949
- new Address4('172.16.0.0/12'),
3950
- new Address4('192.168.0.0/16'),
3951
- ];
3952
- const LOOPBACK_V4 = new Address4('127.0.0.0/8');
3953
- const LINK_LOCAL_V4 = new Address4('169.254.0.0/16');
3954
- const UNSPECIFIED_V4 = new Address4('0.0.0.0/32');
3955
- const BROADCAST_V4 = new Address4('255.255.255.255/32');
3956
- const CGNAT_V4 = new Address4('100.64.0.0/10');
3957
3816
 
3958
3817
  return ipv4;
3959
3818
  }
@@ -4014,11 +3873,6 @@ function requireConstants () {
4014
3873
  '::1/128': 'Loopback',
4015
3874
  'ff00::/8': 'Multicast',
4016
3875
  'fe80::/10': 'Link-local unicast',
4017
- 'fc00::/7': 'Unique local',
4018
- '2002::/16': '6to4',
4019
- '2001:db8::/32': 'Documentation',
4020
- '64:ff9b::/96': 'NAT64 (well-known)',
4021
- '64:ff9b:1::/48': 'NAT64 (local-use)',
4022
3876
  };
4023
3877
  /**
4024
3878
  * A regular expression that matches bad characters in an IPv6 address
@@ -4058,24 +3912,15 @@ function requireHelpers$1 () {
4058
3912
  if (hasRequiredHelpers$1) return helpers;
4059
3913
  hasRequiredHelpers$1 = 1;
4060
3914
  Object.defineProperty(helpers, "__esModule", { value: true });
4061
- helpers.escapeHtml = escapeHtml;
4062
3915
  helpers.spanAllZeroes = spanAllZeroes;
4063
3916
  helpers.spanAll = spanAll;
4064
3917
  helpers.spanLeadingZeroes = spanLeadingZeroes;
4065
3918
  helpers.simpleGroup = simpleGroup;
4066
- function escapeHtml(s) {
4067
- return s
4068
- .replace(/&/g, '&amp;')
4069
- .replace(/</g, '&lt;')
4070
- .replace(/>/g, '&gt;')
4071
- .replace(/"/g, '&quot;')
4072
- .replace(/'/g, '&#39;');
4073
- }
4074
3919
  /**
4075
3920
  * @returns {String} the string with all zeroes contained in a <span>
4076
3921
  */
4077
3922
  function spanAllZeroes(s) {
4078
- return escapeHtml(s).replace(/(0+)/g, '<span class="zero">$1</span>');
3923
+ return s.replace(/(0+)/g, '<span class="zero">$1</span>');
4079
3924
  }
4080
3925
  /**
4081
3926
  * @returns {String} the string with each character contained in a <span>
@@ -4083,11 +3928,11 @@ function requireHelpers$1 () {
4083
3928
  function spanAll(s, offset = 0) {
4084
3929
  const letters = s.split('');
4085
3930
  return letters
4086
- .map((n, i) => `<span class="digit value-${escapeHtml(n)} position-${i + offset}">${spanAllZeroes(n)}</span>`)
3931
+ .map((n, i) => `<span class="digit value-${n} position-${i + offset}">${spanAllZeroes(n)}</span>`)
4087
3932
  .join('');
4088
3933
  }
4089
3934
  function spanLeadingZeroesSimple(group) {
4090
- return escapeHtml(group).replace(/^(0+)/, '<span class="zero">$1</span>');
3935
+ return group.replace(/^(0+)/, '<span class="zero">$1</span>');
4091
3936
  }
4092
3937
  /**
4093
3938
  * @returns {String} the string with leading zeroes contained in a <span>
@@ -4149,7 +3994,7 @@ function requireRegularExpressions () {
4149
3994
  regularExpressions.padGroup = padGroup;
4150
3995
  regularExpressions.simpleRegularExpression = simpleRegularExpression;
4151
3996
  regularExpressions.possibleElisions = possibleElisions;
4152
- const v6 = __importStar(/*@__PURE__*/ requireConstants());
3997
+ const v6 = __importStar(requireConstants());
4153
3998
  function groupPossibilities(possibilities) {
4154
3999
  return `(${possibilities.join('|')})`;
4155
4000
  }
@@ -4249,15 +4094,14 @@ function requireIpv6 () {
4249
4094
  };
4250
4095
  Object.defineProperty(ipv6, "__esModule", { value: true });
4251
4096
  ipv6.Address6 = void 0;
4252
- const common = __importStar(/*@__PURE__*/ requireCommon());
4253
- const constants4 = __importStar(/*@__PURE__*/ requireConstants$1());
4254
- const constants6 = __importStar(/*@__PURE__*/ requireConstants());
4255
- const helpers = __importStar(/*@__PURE__*/ requireHelpers$1());
4256
- const ipv4_1 = /*@__PURE__*/ requireIpv4();
4257
- const regular_expressions_1 = /*@__PURE__*/ requireRegularExpressions();
4258
- const address_error_1 = /*@__PURE__*/ requireAddressError();
4259
- const common_1 = /*@__PURE__*/ requireCommon();
4260
- const isCorrect6 = common.isCorrect(constants6.BITS);
4097
+ const common = __importStar(requireCommon());
4098
+ const constants4 = __importStar(requireConstants$1());
4099
+ const constants6 = __importStar(requireConstants());
4100
+ const helpers = __importStar(requireHelpers$1());
4101
+ const ipv4_1 = requireIpv4();
4102
+ const regular_expressions_1 = requireRegularExpressions();
4103
+ const address_error_1 = requireAddressError();
4104
+ const common_1 = requireCommon();
4261
4105
  function assert(condition) {
4262
4106
  if (!condition) {
4263
4107
  throw new Error('Assertion failed.');
@@ -4301,6 +4145,7 @@ function requireIpv6 () {
4301
4145
  }
4302
4146
  /**
4303
4147
  * Represents an IPv6 address
4148
+ * @class Address6
4304
4149
  * @param {string} address - An IPv6 address string
4305
4150
  * @param {number} [groups=8] - How many octets to parse
4306
4151
  * @example
@@ -4317,14 +4162,18 @@ function requireIpv6 () {
4317
4162
  // #region Attributes
4318
4163
  /**
4319
4164
  * Returns true if the given address is in the subnet of the current address
4165
+ * @memberof Address6
4166
+ * @instance
4320
4167
  * @returns {boolean}
4321
4168
  */
4322
4169
  this.isInSubnet = common.isInSubnet;
4323
4170
  /**
4324
4171
  * Returns true if the address is correct, false otherwise
4172
+ * @memberof Address6
4173
+ * @instance
4325
4174
  * @returns {boolean}
4326
4175
  */
4327
- this.isCorrect = isCorrect6;
4176
+ this.isCorrect = common.isCorrect(constants6.BITS);
4328
4177
  if (optionalGroups === undefined) {
4329
4178
  this.groups = constants6.GROUPS;
4330
4179
  }
@@ -4355,13 +4204,6 @@ function requireIpv6 () {
4355
4204
  this.addressMinusSuffix = address;
4356
4205
  this.parsedAddress = this.parse(this.addressMinusSuffix);
4357
4206
  }
4358
- /**
4359
- * Returns true if the given string is a valid IPv6 address (with optional
4360
- * CIDR subnet and zone identifier), false otherwise. Host bits in the
4361
- * subnet portion are allowed (e.g. `2001:db8::1/32` is valid); for strict
4362
- * network-address validation compare `correctForm()` to
4363
- * `startAddress().correctForm()`, or use `networkForm()`.
4364
- */
4365
4207
  static isValid(address) {
4366
4208
  try {
4367
4209
  // eslint-disable-next-line no-new
@@ -4373,8 +4215,9 @@ function requireIpv6 () {
4373
4215
  }
4374
4216
  }
4375
4217
  /**
4376
- * Convert a BigInt to a v6 address object. The value must be in the
4377
- * range `[0, 2**128 - 1]`; otherwise `AddressError` is thrown.
4218
+ * Convert a BigInt to a v6 address object
4219
+ * @memberof Address6
4220
+ * @static
4378
4221
  * @param {bigint} bigInt - a BigInt to convert
4379
4222
  * @returns {Address6}
4380
4223
  * @example
@@ -4383,21 +4226,19 @@ function requireIpv6 () {
4383
4226
  * address.correctForm(); // '::e8:d4a5:1000'
4384
4227
  */
4385
4228
  static fromBigInt(bigInt) {
4386
- if (bigInt < 0n || bigInt > (1n << BigInt(constants6.BITS)) - 1n) {
4387
- throw new address_error_1.AddressError('IPv6 BigInt must be in the range 0 to 2**128 - 1');
4388
- }
4389
4229
  const hex = bigInt.toString(16).padStart(32, '0');
4390
4230
  const groups = [];
4391
- for (let i = 0; i < constants6.GROUPS; i++) {
4231
+ let i;
4232
+ for (i = 0; i < constants6.GROUPS; i++) {
4392
4233
  groups.push(hex.slice(i * 4, (i + 1) * 4));
4393
4234
  }
4394
4235
  return new Address6(groups.join(':'));
4395
4236
  }
4396
4237
  /**
4397
- * Parse a URL (with optional bracketed host and port) into an address and
4398
- * port. Returns either `{ address, port }` on success or
4399
- * `{ error, address: null, port: null }` if the URL could not be parsed.
4400
- * Ports are returned as numbers (or `null` if absent or out of range).
4238
+ * Convert a URL (with optional port number) to an address object
4239
+ * @memberof Address6
4240
+ * @static
4241
+ * @param {string} url - a URL with optional port number
4401
4242
  * @example
4402
4243
  * var addressAndPort = Address6.fromURL('http://[ffff::]:8080/foo/');
4403
4244
  * addressAndPort.address.correctForm(); // 'ffff::'
@@ -4456,92 +4297,10 @@ function requireIpv6 () {
4456
4297
  port,
4457
4298
  };
4458
4299
  }
4459
- /**
4460
- * Construct an `Address6` from an address and a hex subnet mask given as
4461
- * separate strings (e.g. as returned by Node's `os.networkInterfaces()`).
4462
- * Throws `AddressError` if the mask is non-contiguous (e.g.
4463
- * `ffff::ffff`).
4464
- * @example
4465
- * var address = Address6.fromAddressAndMask('fe80::1', 'ffff:ffff:ffff:ffff::');
4466
- * address.subnetMask; // 64
4467
- */
4468
- static fromAddressAndMask(address, mask) {
4469
- const bits = common.prefixLengthFromMask(new Address6(mask).bigInt(), constants6.BITS);
4470
- return new Address6(`${address}/${bits}`);
4471
- }
4472
- /**
4473
- * Construct an `Address6` from an address and a Cisco-style wildcard mask
4474
- * given as separate strings (e.g. `::ffff:ffff:ffff:ffff` for a `/64`).
4475
- * The wildcard mask is the bitwise inverse of the subnet mask. Throws
4476
- * `AddressError` if the mask is non-contiguous.
4477
- * @example
4478
- * var address = Address6.fromAddressAndWildcardMask('fe80::1', '::ffff:ffff:ffff:ffff');
4479
- * address.subnetMask; // 64
4480
- */
4481
- static fromAddressAndWildcardMask(address, wildcardMask) {
4482
- const wildcard = new Address6(wildcardMask).bigInt();
4483
- const allOnes = (BigInt(1) << BigInt(constants6.BITS)) - BigInt(1);
4484
- // eslint-disable-next-line no-bitwise
4485
- const mask = wildcard ^ allOnes;
4486
- const bits = common.prefixLengthFromMask(mask, constants6.BITS);
4487
- return new Address6(`${address}/${bits}`);
4488
- }
4489
- /**
4490
- * Construct an `Address6` from a wildcard pattern with trailing `*`
4491
- * groups. The number of trailing wildcards determines the prefix
4492
- * length: each `*` represents 16 bits. `::` is expanded to zero groups
4493
- * (not wildcards) before evaluating trailing wildcards.
4494
- *
4495
- * Only trailing whole-group wildcards are supported. Partial-group
4496
- * wildcards (e.g. `2001:db8::0*`) and interior wildcards (e.g.
4497
- * `*::1`) throw `AddressError`.
4498
- * @example
4499
- * Address6.fromWildcard('2001:db8:*:*:*:*:*:*').subnet; // '/32'
4500
- * Address6.fromWildcard('2001:db8::*').subnet; // '/112'
4501
- * Address6.fromWildcard('*:*:*:*:*:*:*:*').subnet; // '/0'
4502
- */
4503
- static fromWildcard(input) {
4504
- if (input.includes('%') || input.includes('/')) {
4505
- throw new address_error_1.AddressError('Wildcard pattern must not include a zone or CIDR suffix');
4506
- }
4507
- const halves = input.split('::');
4508
- if (halves.length > 2) {
4509
- throw new address_error_1.AddressError("Wildcard pattern cannot contain more than one '::'");
4510
- }
4511
- let groups;
4512
- if (halves.length === 2) {
4513
- const left = halves[0] === '' ? [] : halves[0].split(':');
4514
- const right = halves[1] === '' ? [] : halves[1].split(':');
4515
- const remaining = constants6.GROUPS - left.length - right.length;
4516
- if (remaining < 1) {
4517
- throw new address_error_1.AddressError("Wildcard pattern with '::' has too many groups");
4518
- }
4519
- groups = [...left, ...new Array(remaining).fill('0'), ...right];
4520
- }
4521
- else {
4522
- groups = input.split(':');
4523
- }
4524
- if (groups.length !== constants6.GROUPS) {
4525
- throw new address_error_1.AddressError('Wildcard pattern must have 8 groups');
4526
- }
4527
- let firstWildcard = -1;
4528
- for (let i = 0; i < groups.length; i++) {
4529
- if (groups[i] === '*') {
4530
- if (firstWildcard === -1) {
4531
- firstWildcard = i;
4532
- }
4533
- }
4534
- else if (firstWildcard !== -1) {
4535
- throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing groups (e.g. `2001:db8:*:*:*:*:*:*`)');
4536
- }
4537
- }
4538
- const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard;
4539
- const replaced = groups.map((g) => (g === '*' ? '0' : g));
4540
- const subnetBits = constants6.BITS - trailing * 16;
4541
- return new Address6(`${replaced.join(':')}/${subnetBits}`);
4542
- }
4543
4300
  /**
4544
4301
  * Create an IPv6-mapped address given an IPv4 address
4302
+ * @memberof Address6
4303
+ * @static
4545
4304
  * @param {string} address - An IPv4 address string
4546
4305
  * @returns {Address6}
4547
4306
  * @example
@@ -4556,6 +4315,8 @@ function requireIpv6 () {
4556
4315
  }
4557
4316
  /**
4558
4317
  * Return an address from ip6.arpa form
4318
+ * @memberof Address6
4319
+ * @static
4559
4320
  * @param {string} arpaFormAddress - an 'ip6.arpa' form address
4560
4321
  * @returns {Adress6}
4561
4322
  * @example
@@ -4580,6 +4341,8 @@ function requireIpv6 () {
4580
4341
  }
4581
4342
  /**
4582
4343
  * Return the Microsoft UNC transcription of the address
4344
+ * @memberof Address6
4345
+ * @instance
4583
4346
  * @returns {String} the Microsoft UNC transcription of the address
4584
4347
  */
4585
4348
  microsoftTranscription() {
@@ -4587,6 +4350,8 @@ function requireIpv6 () {
4587
4350
  }
4588
4351
  /**
4589
4352
  * Return the first n bits of the address, defaulting to the subnet mask
4353
+ * @memberof Address6
4354
+ * @instance
4590
4355
  * @param {number} [mask=subnet] - the number of bits to mask
4591
4356
  * @returns {String} the first n bits of the address as a string
4592
4357
  */
@@ -4595,6 +4360,8 @@ function requireIpv6 () {
4595
4360
  }
4596
4361
  /**
4597
4362
  * Return the number of possible subnets of a given size in the address
4363
+ * @memberof Address6
4364
+ * @instance
4598
4365
  * @param {number} [subnetSize=128] - the subnet size
4599
4366
  * @returns {String}
4600
4367
  */
@@ -4610,6 +4377,8 @@ function requireIpv6 () {
4610
4377
  }
4611
4378
  /**
4612
4379
  * Helper function getting start address.
4380
+ * @memberof Address6
4381
+ * @instance
4613
4382
  * @returns {bigint}
4614
4383
  */
4615
4384
  _startAddress() {
@@ -4618,6 +4387,8 @@ function requireIpv6 () {
4618
4387
  /**
4619
4388
  * The first address in the range given by this address' subnet
4620
4389
  * Often referred to as the Network Address.
4390
+ * @memberof Address6
4391
+ * @instance
4621
4392
  * @returns {Address6}
4622
4393
  */
4623
4394
  startAddress() {
@@ -4626,6 +4397,8 @@ function requireIpv6 () {
4626
4397
  /**
4627
4398
  * The first host address in the range given by this address's subnet ie
4628
4399
  * the first address after the Network Address
4400
+ * @memberof Address6
4401
+ * @instance
4629
4402
  * @returns {Address6}
4630
4403
  */
4631
4404
  startAddressExclusive() {
@@ -4634,6 +4407,8 @@ function requireIpv6 () {
4634
4407
  }
4635
4408
  /**
4636
4409
  * Helper function getting end address.
4410
+ * @memberof Address6
4411
+ * @instance
4637
4412
  * @returns {bigint}
4638
4413
  */
4639
4414
  _endAddress() {
@@ -4642,6 +4417,8 @@ function requireIpv6 () {
4642
4417
  /**
4643
4418
  * The last address in the range given by this address' subnet
4644
4419
  * Often referred to as the Broadcast
4420
+ * @memberof Address6
4421
+ * @instance
4645
4422
  * @returns {Address6}
4646
4423
  */
4647
4424
  endAddress() {
@@ -4650,6 +4427,8 @@ function requireIpv6 () {
4650
4427
  /**
4651
4428
  * The last host address in the range given by this address's subnet ie
4652
4429
  * the last address prior to the Broadcast Address
4430
+ * @memberof Address6
4431
+ * @instance
4653
4432
  * @returns {Address6}
4654
4433
  */
4655
4434
  endAddressExclusive() {
@@ -4657,73 +4436,36 @@ function requireIpv6 () {
4657
4436
  return Address6.fromBigInt(this._endAddress() - adjust);
4658
4437
  }
4659
4438
  /**
4660
- * The hex form of the subnet mask, e.g. `ffff:ffff:ffff:ffff::` for a
4661
- * `/64`. Returns an `Address6`; call `.correctForm()` for the string.
4662
- * @returns {Address6}
4663
- */
4664
- subnetMaskAddress() {
4665
- return Address6.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants6.BITS - this.subnetMask)}`));
4666
- }
4667
- /**
4668
- * The Cisco-style wildcard mask, e.g. `::ffff:ffff:ffff:ffff` for a
4669
- * `/64`. This is the bitwise inverse of `subnetMaskAddress()`. Returns
4670
- * an `Address6`; call `.correctForm()` for the string.
4671
- * @returns {Address6}
4672
- */
4673
- wildcardMask() {
4674
- return Address6.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants6.BITS - this.subnetMask)}`));
4675
- }
4676
- /**
4677
- * The network address in CIDR string form, e.g. `2001:db8::/32` for
4678
- * `2001:db8::1/32`. For an address with no explicit subnet the prefix
4679
- * is `/128`, e.g. `networkForm()` on `2001:db8::1` returns
4680
- * `2001:db8::1/128`.
4681
- * @returns {string}
4682
- */
4683
- networkForm() {
4684
- return `${this.startAddress().correctForm()}/${this.subnetMask}`;
4685
- }
4686
- /**
4687
- * Return the scope of the address. The 4-bit scope field
4688
- * ([RFC 4291 §2.7](https://datatracker.ietf.org/doc/html/rfc4291#section-2.7))
4689
- * is only defined for multicast addresses; for unicast addresses the scope
4690
- * is derived from the address type per
4691
- * [RFC 4007 §6](https://datatracker.ietf.org/doc/html/rfc4007#section-6).
4439
+ * Return the scope of the address
4440
+ * @memberof Address6
4441
+ * @instance
4692
4442
  * @returns {String}
4693
4443
  */
4694
4444
  getScope() {
4695
- const type = this.getType();
4696
- if (type === 'Multicast' || type.startsWith('Multicast ')) {
4697
- const scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)];
4698
- return scope || 'Unknown';
4699
- }
4700
- // RFC 4291 §2.5.3: the loopback address is treated as having Link-Local
4701
- // scope. (Multicast scope 1, "Interface-Local", is a different concept
4702
- // used only for loopback transmission of multicast.)
4703
- if (type === 'Link-local unicast' || type === 'Loopback') {
4704
- return 'Link local';
4445
+ let scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)];
4446
+ if (this.getType() === 'Global unicast' && scope !== 'Link local') {
4447
+ scope = 'Global';
4705
4448
  }
4706
- // RFC 4007 §6: the unspecified address has no scope.
4707
- if (type === 'Unspecified') {
4708
- return 'Unknown';
4709
- }
4710
- return 'Global';
4449
+ return scope || 'Unknown';
4711
4450
  }
4712
4451
  /**
4713
4452
  * Return the type of the address
4453
+ * @memberof Address6
4454
+ * @instance
4714
4455
  * @returns {String}
4715
4456
  */
4716
4457
  getType() {
4717
- for (let i = 0; i < TYPE_SUBNETS.length; i++) {
4718
- const entry = TYPE_SUBNETS[i];
4719
- if (this.isInSubnet(entry[0])) {
4720
- return entry[1];
4458
+ for (const subnet of Object.keys(constants6.TYPES)) {
4459
+ if (this.isInSubnet(new Address6(subnet))) {
4460
+ return constants6.TYPES[subnet];
4721
4461
  }
4722
4462
  }
4723
4463
  return 'Global unicast';
4724
4464
  }
4725
4465
  /**
4726
4466
  * Return the bits in the given range as a BigInt
4467
+ * @memberof Address6
4468
+ * @instance
4727
4469
  * @returns {bigint}
4728
4470
  */
4729
4471
  getBits(start, end) {
@@ -4731,6 +4473,8 @@ function requireIpv6 () {
4731
4473
  }
4732
4474
  /**
4733
4475
  * Return the bits in the given range as a base-2 string
4476
+ * @memberof Address6
4477
+ * @instance
4734
4478
  * @returns {String}
4735
4479
  */
4736
4480
  getBitsBase2(start, end) {
@@ -4738,6 +4482,8 @@ function requireIpv6 () {
4738
4482
  }
4739
4483
  /**
4740
4484
  * Return the bits in the given range as a base-16 string
4485
+ * @memberof Address6
4486
+ * @instance
4741
4487
  * @returns {String}
4742
4488
  */
4743
4489
  getBitsBase16(start, end) {
@@ -4751,6 +4497,8 @@ function requireIpv6 () {
4751
4497
  }
4752
4498
  /**
4753
4499
  * Return the bits that are set past the subnet mask length
4500
+ * @memberof Address6
4501
+ * @instance
4754
4502
  * @returns {String}
4755
4503
  */
4756
4504
  getBitsPastSubnet() {
@@ -4758,8 +4506,10 @@ function requireIpv6 () {
4758
4506
  }
4759
4507
  /**
4760
4508
  * Return the reversed ip6.arpa form of the address
4509
+ * @memberof Address6
4761
4510
  * @param {Object} options
4762
4511
  * @param {boolean} options.omitSuffix - omit the "ip6.arpa" suffix
4512
+ * @instance
4763
4513
  * @returns {String}
4764
4514
  */
4765
4515
  reverseForm(options) {
@@ -4785,10 +4535,10 @@ function requireIpv6 () {
4785
4535
  return 'ip6.arpa.';
4786
4536
  }
4787
4537
  /**
4788
- * Returns the address in correct form, per
4789
- * [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952): leading zeros
4790
- * stripped, the longest run of zero groups collapsed to `::`, and hex digits
4791
- * lowercased (e.g. `2001:db8::1`). This is the recommended form for display.
4538
+ * Return the correct form of the address
4539
+ * @memberof Address6
4540
+ * @instance
4541
+ * @returns {String}
4792
4542
  */
4793
4543
  correctForm() {
4794
4544
  let i;
@@ -4832,6 +4582,8 @@ function requireIpv6 () {
4832
4582
  }
4833
4583
  /**
4834
4584
  * Return a zero-padded base-2 string representation of the address
4585
+ * @memberof Address6
4586
+ * @instance
4835
4587
  * @returns {String}
4836
4588
  * @example
4837
4589
  * var address = new Address6('2001:4860:4001:803::1011');
@@ -4840,22 +4592,10 @@ function requireIpv6 () {
4840
4592
  * // 0000000000000000000000000000000000000000000000000001000000010001'
4841
4593
  */
4842
4594
  binaryZeroPad() {
4843
- if (this._binaryZeroPad === undefined) {
4844
- this._binaryZeroPad = this.bigInt().toString(2).padStart(constants6.BITS, '0');
4845
- }
4846
- return this._binaryZeroPad;
4595
+ return this.bigInt().toString(2).padStart(constants6.BITS, '0');
4847
4596
  }
4848
- /**
4849
- * Parses a v4-in-v6 string (e.g. `::ffff:192.168.0.1`) by extracting the
4850
- * trailing IPv4 address into `this.address4` / `this.parsedAddress4` and
4851
- * returning the address with the v4 portion converted to two v6 groups.
4852
- * Used internally by `parse()`.
4853
- */
4854
4597
  // TODO: Improve the semantics of this helper function
4855
4598
  parse4in6(address) {
4856
- if (address.indexOf('.') === -1) {
4857
- return address;
4858
- }
4859
4599
  const groups = address.split(':');
4860
4600
  const lastGroup = groups.slice(-1)[0];
4861
4601
  const address4 = lastGroup.match(constants4.RE_ADDRESS);
@@ -4864,12 +4604,7 @@ function requireIpv6 () {
4864
4604
  this.address4 = new ipv4_1.Address4(this.parsedAddress4);
4865
4605
  for (let i = 0; i < this.address4.groups; i++) {
4866
4606
  if (/^0[0-9]+/.test(this.address4.parsedAddress[i])) {
4867
- // The prefix groups haven't been through the bad-character check
4868
- // yet, so escape them before including in the error HTML.
4869
- const highlighted = this.address4.parsedAddress.map(spanLeadingZeroes4).join('.');
4870
- const prefix = groups.slice(0, -1).map(helpers.escapeHtml).join(':');
4871
- const separator = groups.length > 1 ? ':' : '';
4872
- throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", `${prefix}${separator}${highlighted}`);
4607
+ throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", address.replace(constants4.RE_ADDRESS, this.address4.parsedAddress.map(spanLeadingZeroes4).join('.')));
4873
4608
  }
4874
4609
  }
4875
4610
  this.v4 = true;
@@ -4878,13 +4613,6 @@ function requireIpv6 () {
4878
4613
  }
4879
4614
  return address;
4880
4615
  }
4881
- /**
4882
- * Parses an IPv6 address string into its 8 hexadecimal groups (expanding
4883
- * any `::` elision and any trailing v4-in-v6 portion) and stores the result
4884
- * on `this.parsedAddress`. Called automatically by the constructor; you
4885
- * typically don't need to call it directly. Throws `AddressError` if the
4886
- * input is malformed.
4887
- */
4888
4616
  // TODO: Make private?
4889
4617
  parse(address) {
4890
4618
  address = this.parse4in6(address);
@@ -4934,16 +4662,18 @@ function requireIpv6 () {
4934
4662
  return groups;
4935
4663
  }
4936
4664
  /**
4937
- * Returns the canonical (fully expanded) form of the address: all 8 groups,
4938
- * each padded to 4 hex digits, with no `::` collapsing
4939
- * (e.g. `2001:0db8:0000:0000:0000:0000:0000:0001`). Useful for sorting and
4940
- * byte-exact comparison.
4665
+ * Return the canonical form of the address
4666
+ * @memberof Address6
4667
+ * @instance
4668
+ * @returns {String}
4941
4669
  */
4942
4670
  canonicalForm() {
4943
4671
  return this.parsedAddress.map(paddedHex).join(':');
4944
4672
  }
4945
4673
  /**
4946
4674
  * Return the decimal form of the address
4675
+ * @memberof Address6
4676
+ * @instance
4947
4677
  * @returns {String}
4948
4678
  */
4949
4679
  decimal() {
@@ -4951,6 +4681,8 @@ function requireIpv6 () {
4951
4681
  }
4952
4682
  /**
4953
4683
  * Return the address as a BigInt
4684
+ * @memberof Address6
4685
+ * @instance
4954
4686
  * @returns {bigint}
4955
4687
  */
4956
4688
  bigInt() {
@@ -4958,6 +4690,8 @@ function requireIpv6 () {
4958
4690
  }
4959
4691
  /**
4960
4692
  * Return the last two groups of this address as an IPv4 address string
4693
+ * @memberof Address6
4694
+ * @instance
4961
4695
  * @returns {Address4}
4962
4696
  * @example
4963
4697
  * var address = new Address6('2001:4860:4001::1825:bf11');
@@ -4965,10 +4699,12 @@ function requireIpv6 () {
4965
4699
  */
4966
4700
  to4() {
4967
4701
  const binary = this.binaryZeroPad().split('');
4968
- return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16).padStart(8, '0'));
4702
+ return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16));
4969
4703
  }
4970
4704
  /**
4971
4705
  * Return the v4-in-v6 form of the address
4706
+ * @memberof Address6
4707
+ * @instance
4972
4708
  * @returns {String}
4973
4709
  */
4974
4710
  to4in6() {
@@ -4982,10 +4718,10 @@ function requireIpv6 () {
4982
4718
  return correct + infix + address4.address;
4983
4719
  }
4984
4720
  /**
4985
- * Decodes the Teredo tunneling fields embedded in this address. Returns the
4986
- * Teredo prefix, server IPv4, client IPv4, raw flag bits, cone-NAT flag,
4987
- * UDP port, and Microsoft-format flag breakdown (reserved, universal/local,
4988
- * group/individual, nonce). Only meaningful for addresses in `2001::/32`.
4721
+ * Return an object containing the Teredo properties of the address
4722
+ * @memberof Address6
4723
+ * @instance
4724
+ * @returns {Object}
4989
4725
  */
4990
4726
  inspectTeredo() {
4991
4727
  /*
@@ -5016,7 +4752,7 @@ function requireIpv6 () {
5016
4752
  const server4 = ipv4_1.Address4.fromHex(this.getBitsBase16(32, 64));
5017
4753
  const bitsForClient4 = this.getBits(96, 128);
5018
4754
  // eslint-disable-next-line no-bitwise
5019
- const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16).padStart(8, '0'));
4755
+ const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16));
5020
4756
  const flagsBase2 = this.getBitsBase2(64, 80);
5021
4757
  const coneNat = (0, common_1.testBit)(flagsBase2, 15);
5022
4758
  const reserved = (0, common_1.testBit)(flagsBase2, 14);
@@ -5039,9 +4775,10 @@ function requireIpv6 () {
5039
4775
  };
5040
4776
  }
5041
4777
  /**
5042
- * Decodes the 6to4 tunneling fields embedded in this address. Returns the
5043
- * 6to4 prefix and the embedded IPv4 gateway address. Only meaningful for
5044
- * addresses in `2002::/16`.
4778
+ * Return an object containing the 6to4 properties of the address
4779
+ * @memberof Address6
4780
+ * @instance
4781
+ * @returns {Object}
5045
4782
  */
5046
4783
  inspect6to4() {
5047
4784
  /*
@@ -5057,6 +4794,8 @@ function requireIpv6 () {
5057
4794
  }
5058
4795
  /**
5059
4796
  * Return a v6 6to4 address from a v6 v4inv6 address
4797
+ * @memberof Address6
4798
+ * @instance
5060
4799
  * @returns {Address6}
5061
4800
  */
5062
4801
  to6to4() {
@@ -5073,80 +4812,9 @@ function requireIpv6 () {
5073
4812
  return new Address6(addr6to4);
5074
4813
  }
5075
4814
  /**
5076
- * Embed an IPv4 address into a NAT64 IPv6 address using the encoding
5077
- * defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
5078
- * The default prefix is the well-known prefix `64:ff9b::/96`. The prefix
5079
- * length must be one of 32, 40, 48, 56, 64, or 96; for prefixes shorter
5080
- * than /64 the IPv4 octets are split around the reserved bits 64–71.
5081
- * @example
5082
- * Address6.fromAddress4Nat64('192.0.2.33').correctForm(); // '64:ff9b::c000:221'
5083
- * Address6.fromAddress4Nat64('192.0.2.33', '2001:db8::/32').correctForm(); // '2001:db8:c000:221::'
5084
- */
5085
- static fromAddress4Nat64(address, prefix = '64:ff9b::/96') {
5086
- const v4 = new ipv4_1.Address4(address);
5087
- const prefix6 = new Address6(prefix);
5088
- const pl = prefix6.subnetMask;
5089
- if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) {
5090
- throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96');
5091
- }
5092
- const prefixBits = prefix6.binaryZeroPad();
5093
- const v4Bits = v4.binaryZeroPad();
5094
- let bits;
5095
- if (pl === 96) {
5096
- bits = prefixBits.slice(0, 96) + v4Bits;
5097
- }
5098
- else {
5099
- const beforeU = 64 - pl;
5100
- bits =
5101
- prefixBits.slice(0, pl) +
5102
- v4Bits.slice(0, beforeU) +
5103
- '00000000' +
5104
- v4Bits.slice(beforeU) +
5105
- '0'.repeat(128 - 72 - (32 - beforeU));
5106
- }
5107
- const hex = BigInt(`0b${bits}`).toString(16).padStart(32, '0');
5108
- const groups = [];
5109
- for (let i = 0; i < 8; i++) {
5110
- groups.push(hex.slice(i * 4, (i + 1) * 4));
5111
- }
5112
- return new Address6(groups.join(':'));
5113
- }
5114
- /**
5115
- * Extract the embedded IPv4 address from a NAT64 IPv6 address using the
5116
- * encoding defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
5117
- * The default prefix is the well-known prefix `64:ff9b::/96`. Returns
5118
- * `null` if this address is not contained within the given prefix.
5119
- * @example
5120
- * new Address6('64:ff9b::c000:221').toAddress4Nat64()!.correctForm(); // '192.0.2.33'
5121
- */
5122
- toAddress4Nat64(prefix = '64:ff9b::/96') {
5123
- const prefix6 = new Address6(prefix);
5124
- const pl = prefix6.subnetMask;
5125
- if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) {
5126
- throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96');
5127
- }
5128
- if (!this.isInSubnet(prefix6)) {
5129
- return null;
5130
- }
5131
- const bits = this.binaryZeroPad();
5132
- let v4Bits;
5133
- if (pl === 96) {
5134
- v4Bits = bits.slice(96, 128);
5135
- }
5136
- else {
5137
- const beforeU = 64 - pl;
5138
- v4Bits = bits.slice(pl, pl + beforeU) + bits.slice(72, 72 + (32 - beforeU));
5139
- }
5140
- const octets = [];
5141
- for (let i = 0; i < 4; i++) {
5142
- octets.push(parseInt(v4Bits.slice(i * 8, (i + 1) * 8), 2).toString());
5143
- }
5144
- return new ipv4_1.Address4(octets.join('.'));
5145
- }
5146
- /**
5147
- * Return a byte array.
5148
- *
5149
- * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toByteArray())`.
4815
+ * Return a byte array
4816
+ * @memberof Address6
4817
+ * @instance
5150
4818
  * @returns {Array}
5151
4819
  */
5152
4820
  toByteArray() {
@@ -5160,27 +4828,27 @@ function requireIpv6 () {
5160
4828
  return bytes;
5161
4829
  }
5162
4830
  /**
5163
- * Return an unsigned byte array.
5164
- *
5165
- * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toUnsignedByteArray())`.
4831
+ * Return an unsigned byte array
4832
+ * @memberof Address6
4833
+ * @instance
5166
4834
  * @returns {Array}
5167
4835
  */
5168
4836
  toUnsignedByteArray() {
5169
4837
  return this.toByteArray().map(unsignByte);
5170
4838
  }
5171
4839
  /**
5172
- * Convert a byte array to an Address6 object.
5173
- *
5174
- * To convert from a Node.js `Buffer`, spread it: `Address6.fromByteArray([...buf])`.
4840
+ * Convert a byte array to an Address6 object
4841
+ * @memberof Address6
4842
+ * @static
5175
4843
  * @returns {Address6}
5176
4844
  */
5177
4845
  static fromByteArray(bytes) {
5178
4846
  return this.fromUnsignedByteArray(bytes.map(unsignByte));
5179
4847
  }
5180
4848
  /**
5181
- * Convert an unsigned byte array to an Address6 object.
5182
- *
5183
- * To convert from a Node.js `Buffer`, spread it: `Address6.fromUnsignedByteArray([...buf])`.
4849
+ * Convert an unsigned byte array to an Address6 object
4850
+ * @memberof Address6
4851
+ * @static
5184
4852
  * @returns {Address6}
5185
4853
  */
5186
4854
  static fromUnsignedByteArray(bytes) {
@@ -5195,6 +4863,8 @@ function requireIpv6 () {
5195
4863
  }
5196
4864
  /**
5197
4865
  * Returns true if the address is in the canonical form, false otherwise
4866
+ * @memberof Address6
4867
+ * @instance
5198
4868
  * @returns {boolean}
5199
4869
  */
5200
4870
  isCanonical() {
@@ -5202,6 +4872,8 @@ function requireIpv6 () {
5202
4872
  }
5203
4873
  /**
5204
4874
  * Returns true if the address is a link local address, false otherwise
4875
+ * @memberof Address6
4876
+ * @instance
5205
4877
  * @returns {boolean}
5206
4878
  */
5207
4879
  isLinkLocal() {
@@ -5214,81 +4886,53 @@ function requireIpv6 () {
5214
4886
  }
5215
4887
  /**
5216
4888
  * Returns true if the address is a multicast address, false otherwise
4889
+ * @memberof Address6
4890
+ * @instance
5217
4891
  * @returns {boolean}
5218
4892
  */
5219
4893
  isMulticast() {
5220
- const type = this.getType();
5221
- return type === 'Multicast' || type.startsWith('Multicast ');
4894
+ return this.getType() === 'Multicast';
5222
4895
  }
5223
4896
  /**
5224
- * Returns true if the address was written in v4-in-v6 dotted-quad notation
5225
- * (e.g. `::ffff:127.0.0.1`), false otherwise. This is a notation-level flag
5226
- * and does not reflect whether the address bits lie in the IPv4-mapped
5227
- * (`::ffff:0:0/96`) subnet — for that, see {@link isMapped4}.
4897
+ * Returns true if the address is a v4-in-v6 address, false otherwise
4898
+ * @memberof Address6
4899
+ * @instance
5228
4900
  * @returns {boolean}
5229
4901
  */
5230
4902
  is4() {
5231
4903
  return this.v4;
5232
4904
  }
5233
- /**
5234
- * Returns true if the address is an IPv4-mapped IPv6 address in
5235
- * `::ffff:0:0/96` ([RFC 4291 §2.5.5.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2)),
5236
- * false otherwise. Unlike {@link is4}, this checks the underlying address
5237
- * bits rather than the textual notation, so `::ffff:127.0.0.1` and
5238
- * `::ffff:7f00:1` both return true.
5239
- * @returns {boolean}
5240
- */
5241
- isMapped4() {
5242
- return this.isInSubnet(IPV4_MAPPED_SUBNET);
5243
- }
5244
4905
  /**
5245
4906
  * Returns true if the address is a Teredo address, false otherwise
4907
+ * @memberof Address6
4908
+ * @instance
5246
4909
  * @returns {boolean}
5247
4910
  */
5248
4911
  isTeredo() {
5249
- return this.isInSubnet(TEREDO_SUBNET);
4912
+ return this.isInSubnet(new Address6('2001::/32'));
5250
4913
  }
5251
4914
  /**
5252
4915
  * Returns true if the address is a 6to4 address, false otherwise
4916
+ * @memberof Address6
4917
+ * @instance
5253
4918
  * @returns {boolean}
5254
4919
  */
5255
4920
  is6to4() {
5256
- return this.isInSubnet(SIX_TO_FOUR_SUBNET);
4921
+ return this.isInSubnet(new Address6('2002::/16'));
5257
4922
  }
5258
4923
  /**
5259
4924
  * Returns true if the address is a loopback address, false otherwise
4925
+ * @memberof Address6
4926
+ * @instance
5260
4927
  * @returns {boolean}
5261
4928
  */
5262
4929
  isLoopback() {
5263
4930
  return this.getType() === 'Loopback';
5264
4931
  }
5265
- /**
5266
- * Returns true if the address is a Unique Local Address in `fc00::/7` ([RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193)). ULAs are the IPv6 equivalent of IPv4 [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private addresses.
5267
- * @returns {boolean}
5268
- */
5269
- isULA() {
5270
- return this.isInSubnet(ULA_SUBNET);
5271
- }
5272
- /**
5273
- * Returns true if the address is the unspecified address `::`.
5274
- * @returns {boolean}
5275
- */
5276
- isUnspecified() {
5277
- return this.getType() === 'Unspecified';
5278
- }
5279
- /**
5280
- * Returns true if the address is in the documentation prefix `2001:db8::/32` ([RFC 3849](https://datatracker.ietf.org/doc/html/rfc3849)).
5281
- * @returns {boolean}
5282
- */
5283
- isDocumentation() {
5284
- return this.isInSubnet(DOCUMENTATION_SUBNET);
5285
- }
5286
4932
  // #endregion
5287
4933
  // #region HTML
5288
4934
  /**
5289
- * Returns the address as an HTTP URL with the host bracketed, e.g.
5290
- * `http://[2001:db8::1]/`. If `optionalPort` is provided it is appended,
5291
- * e.g. `http://[2001:db8::1]:8080/`.
4935
+ * @returns {String} the address in link form with a default port of 80
5292
4936
  */
5293
4937
  href(optionalPort) {
5294
4938
  if (optionalPort === undefined) {
@@ -5300,12 +4944,7 @@ function requireIpv6 () {
5300
4944
  return `http://[${this.correctForm()}]${optionalPort}/`;
5301
4945
  }
5302
4946
  /**
5303
- * Returns an HTML `<a>` element whose `href` encodes the address in a URL
5304
- * hash fragment (default prefix `/#address=`). Useful for linking between
5305
- * pages of an address-inspector UI.
5306
- * @param options.className - CSS class for the rendered `<a>` element
5307
- * @param options.prefix - hash prefix prepended to the address (default `/#address=`)
5308
- * @param options.v4 - when true, render the address in v4-in-v6 form
4947
+ * @returns {String} a link suitable for conveying the address via a URL hash
5309
4948
  */
5310
4949
  link(options) {
5311
4950
  if (!options) {
@@ -5325,13 +4964,10 @@ function requireIpv6 () {
5325
4964
  formFunction = this.to4in6;
5326
4965
  }
5327
4966
  const form = formFunction.call(this);
5328
- const safeHref = helpers.escapeHtml(`${options.prefix}${form}`);
5329
- const safeForm = helpers.escapeHtml(form);
5330
4967
  if (options.className) {
5331
- const safeClass = helpers.escapeHtml(options.className);
5332
- return `<a href="${safeHref}" class="${safeClass}">${safeForm}</a>`;
4968
+ return `<a href="${options.prefix}${form}" class="${options.className}">${form}</a>`;
5333
4969
  }
5334
- return `<a href="${safeHref}">${safeForm}</a>`;
4970
+ return `<a href="${options.prefix}${form}">${form}</a>`;
5335
4971
  }
5336
4972
  /**
5337
4973
  * Groups an address
@@ -5340,13 +4976,13 @@ function requireIpv6 () {
5340
4976
  group() {
5341
4977
  if (this.elidedGroups === 0) {
5342
4978
  // The simple case
5343
- return helpers.simpleGroup(this.addressMinusSuffix).join(':');
4979
+ return helpers.simpleGroup(this.address).join(':');
5344
4980
  }
5345
4981
  assert(typeof this.elidedGroups === 'number');
5346
4982
  assert(typeof this.elisionBegin === 'number');
5347
4983
  // The elided case
5348
4984
  const output = [];
5349
- const [left, right] = this.addressMinusSuffix.split('::');
4985
+ const [left, right] = this.address.split('::');
5350
4986
  if (left.length) {
5351
4987
  output.push(...helpers.simpleGroup(left));
5352
4988
  }
@@ -5376,6 +5012,8 @@ function requireIpv6 () {
5376
5012
  /**
5377
5013
  * Generate a regular expression string that can be used to find or validate
5378
5014
  * all variations of this address
5015
+ * @memberof Address6
5016
+ * @instance
5379
5017
  * @param {boolean} substringSearch
5380
5018
  * @returns {string}
5381
5019
  */
@@ -5420,6 +5058,8 @@ function requireIpv6 () {
5420
5058
  /**
5421
5059
  * Generate a regular expression that can be used to find or validate all
5422
5060
  * variations of this address.
5061
+ * @memberof Address6
5062
+ * @instance
5423
5063
  * @param {boolean} substringSearch
5424
5064
  * @returns {RegExp}
5425
5065
  */
@@ -5428,15 +5068,6 @@ function requireIpv6 () {
5428
5068
  }
5429
5069
  }
5430
5070
  ipv6.Address6 = Address6;
5431
- const TYPE_SUBNETS = Object.keys(constants6.TYPES).map((subnet) => [
5432
- new Address6(subnet),
5433
- constants6.TYPES[subnet],
5434
- ]);
5435
- const TEREDO_SUBNET = new Address6('2001::/32');
5436
- const SIX_TO_FOUR_SUBNET = new Address6('2002::/16');
5437
- const ULA_SUBNET = new Address6('fc00::/7');
5438
- const DOCUMENTATION_SUBNET = new Address6('2001:db8::/32');
5439
- const IPV4_MAPPED_SUBNET = new Address6('::ffff:0:0/96');
5440
5071
 
5441
5072
  return ipv6;
5442
5073
  }
@@ -5446,7 +5077,7 @@ var hasRequiredIpAddress;
5446
5077
  function requireIpAddress () {
5447
5078
  if (hasRequiredIpAddress) return ipAddress;
5448
5079
  hasRequiredIpAddress = 1;
5449
- (function (exports) {
5080
+ (function (exports$1) {
5450
5081
  var __createBinding = (ipAddress && ipAddress.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5451
5082
  if (k2 === undefined) k2 = k;
5452
5083
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -5470,16 +5101,16 @@ function requireIpAddress () {
5470
5101
  __setModuleDefault(result, mod);
5471
5102
  return result;
5472
5103
  };
5473
- Object.defineProperty(exports, "__esModule", { value: true });
5474
- exports.v6 = exports.AddressError = exports.Address6 = exports.Address4 = void 0;
5475
- var ipv4_1 = /*@__PURE__*/ requireIpv4();
5476
- Object.defineProperty(exports, "Address4", { enumerable: true, get: function () { return ipv4_1.Address4; } });
5477
- var ipv6_1 = /*@__PURE__*/ requireIpv6();
5478
- Object.defineProperty(exports, "Address6", { enumerable: true, get: function () { return ipv6_1.Address6; } });
5479
- var address_error_1 = /*@__PURE__*/ requireAddressError();
5480
- Object.defineProperty(exports, "AddressError", { enumerable: true, get: function () { return address_error_1.AddressError; } });
5481
- const helpers = __importStar(/*@__PURE__*/ requireHelpers$1());
5482
- exports.v6 = { helpers };
5104
+ Object.defineProperty(exports$1, "__esModule", { value: true });
5105
+ exports$1.v6 = exports$1.AddressError = exports$1.Address6 = exports$1.Address4 = void 0;
5106
+ var ipv4_1 = requireIpv4();
5107
+ Object.defineProperty(exports$1, "Address4", { enumerable: true, get: function () { return ipv4_1.Address4; } });
5108
+ var ipv6_1 = requireIpv6();
5109
+ Object.defineProperty(exports$1, "Address6", { enumerable: true, get: function () { return ipv6_1.Address6; } });
5110
+ var address_error_1 = requireAddressError();
5111
+ Object.defineProperty(exports$1, "AddressError", { enumerable: true, get: function () { return address_error_1.AddressError; } });
5112
+ const helpers = __importStar(requireHelpers$1());
5113
+ exports$1.v6 = { helpers };
5483
5114
 
5484
5115
  } (ipAddress));
5485
5116
  return ipAddress;
@@ -5495,7 +5126,7 @@ function requireHelpers () {
5495
5126
  const util_1 = requireUtil();
5496
5127
  const constants_1 = requireConstants$2();
5497
5128
  const stream = require$$2$1;
5498
- const ip_address_1 = /*@__PURE__*/ requireIpAddress();
5129
+ const ip_address_1 = requireIpAddress();
5499
5130
  const net$1 = net;
5500
5131
  /**
5501
5132
  * Validates the provided SocksClientOptions
@@ -5716,7 +5347,7 @@ var hasRequiredSocksclient;
5716
5347
  function requireSocksclient () {
5717
5348
  if (hasRequiredSocksclient) return socksclient;
5718
5349
  hasRequiredSocksclient = 1;
5719
- (function (exports) {
5350
+ (function (exports$1) {
5720
5351
  var __awaiter = (socksclient && socksclient.__awaiter) || function (thisArg, _arguments, P, generator) {
5721
5352
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5722
5353
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -5726,8 +5357,8 @@ function requireSocksclient () {
5726
5357
  step((generator = generator.apply(thisArg, _arguments || [])).next());
5727
5358
  });
5728
5359
  };
5729
- Object.defineProperty(exports, "__esModule", { value: true });
5730
- exports.SocksClientError = exports.SocksClient = void 0;
5360
+ Object.defineProperty(exports$1, "__esModule", { value: true });
5361
+ exports$1.SocksClientError = exports$1.SocksClient = void 0;
5731
5362
  const events_1 = require$$0;
5732
5363
  const net$1 = net;
5733
5364
  const smart_buffer_1 = requireSmartbuffer();
@@ -5735,8 +5366,8 @@ function requireSocksclient () {
5735
5366
  const helpers_1 = requireHelpers();
5736
5367
  const receivebuffer_1 = requireReceivebuffer();
5737
5368
  const util_1 = requireUtil();
5738
- Object.defineProperty(exports, "SocksClientError", { enumerable: true, get: function () { return util_1.SocksClientError; } });
5739
- const ip_address_1 = /*@__PURE__*/ requireIpAddress();
5369
+ Object.defineProperty(exports$1, "SocksClientError", { enumerable: true, get: function () { return util_1.SocksClientError; } });
5370
+ const ip_address_1 = requireIpAddress();
5740
5371
  class SocksClient extends events_1.EventEmitter {
5741
5372
  constructor(options) {
5742
5373
  super();
@@ -6507,7 +6138,7 @@ function requireSocksclient () {
6507
6138
  return Object.assign({}, this.options);
6508
6139
  }
6509
6140
  }
6510
- exports.SocksClient = SocksClient;
6141
+ exports$1.SocksClient = SocksClient;
6511
6142
 
6512
6143
  } (socksclient));
6513
6144
  return socksclient;
@@ -6518,7 +6149,7 @@ var hasRequiredBuild;
6518
6149
  function requireBuild () {
6519
6150
  if (hasRequiredBuild) return build$1;
6520
6151
  hasRequiredBuild = 1;
6521
- (function (exports) {
6152
+ (function (exports$1) {
6522
6153
  var __createBinding = (build$1 && build$1.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6523
6154
  if (k2 === undefined) k2 = k;
6524
6155
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -6530,11 +6161,11 @@ function requireBuild () {
6530
6161
  if (k2 === undefined) k2 = k;
6531
6162
  o[k2] = m[k];
6532
6163
  }));
6533
- var __exportStar = (build$1 && build$1.__exportStar) || function(m, exports) {
6534
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
6164
+ var __exportStar = (build$1 && build$1.__exportStar) || function(m, exports$1) {
6165
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports$1, p)) __createBinding(exports$1, m, p);
6535
6166
  };
6536
- Object.defineProperty(exports, "__esModule", { value: true });
6537
- __exportStar(requireSocksclient(), exports);
6167
+ Object.defineProperty(exports$1, "__esModule", { value: true });
6168
+ __exportStar(requireSocksclient(), exports$1);
6538
6169
 
6539
6170
  } (build$1));
6540
6171
  return build$1;
@@ -6957,10 +6588,44 @@ function buildCookieHeaderFromSetCookies(setCookies) {
6957
6588
  return sanitizeCookieHeader(setCookies.join('; '));
6958
6589
  }
6959
6590
 
6591
+ /**
6592
+ * Verbose logging helpers.
6593
+ *
6594
+ * The CLI registers a global `--verbose` flag (see `src/index.ts`). Detailed
6595
+ * diagnostic logs — HTTP request/response bodies, the local wasm split
6596
+ * pipeline's `[wasmtool]` / `[wasm-split]` / `[download]` breadcrumbs — should
6597
+ * only surface when the user explicitly opts in, so the default `ttmg dev`
6598
+ * output stays readable.
6599
+ *
6600
+ * Genuine failures must keep using `console.error` directly so they are always
6601
+ * visible regardless of `--verbose`.
6602
+ */
6960
6603
  function isVerboseEnabled() {
6961
6604
  return process.argv.includes('--verbose');
6962
6605
  }
6963
- // ppe_dev_tool
6606
+ function verboseLog(...args) {
6607
+ if (isVerboseEnabled())
6608
+ console.log(...args);
6609
+ }
6610
+ function verboseWarn(...args) {
6611
+ if (isVerboseEnabled())
6612
+ console.warn(...args);
6613
+ }
6614
+
6615
+ /**
6616
+ * PPE / 测试环境开关。
6617
+ *
6618
+ * 所有 CLI 接口(`stark_wasm/v4/*`, `wasm-collect/v1/*`, portal 鉴权等)
6619
+ * 都共用本文件的 `request()`,所以在这里统一注入 PPE header 就能覆盖全量。
6620
+ * 不在 `DEV_HEADERS` 里改是因为那份常量只被 `remotePipeline.ts` 里的老
6621
+ * 远程分包接口 spread 用;新加的 `startSession.ts` / `finishSession.ts` /
6622
+ * `getCollectedFuncIds.ts` 都没 spread,漏一处就会绕开 PPE。
6623
+ *
6624
+ * 要切回线上环境,直接把 `CLI_PPE_ENV` 设成空串即可(下方条件 spread 会
6625
+ * 自动不带这两个 header)。
6626
+ */
6627
+ // const CLI_PPE_ENV: string = 'ppe_wasm_test'; // 走 PPE 测试环境
6628
+ const CLI_PPE_ENV = ''; // 走线上服务(空串 => 下方条件 spread 不注入 PPE header)
6964
6629
  function getAxiosProxyConfig() {
6965
6630
  const config = getTTMGRC();
6966
6631
  // 优先级: http-proxy > socks-proxy > proxy (老字段兼容)
@@ -7105,7 +6770,7 @@ function printApiResponseLog(title, result) {
7105
6770
  ['Return Value', result.data],
7106
6771
  ]);
7107
6772
  }
7108
- async function request({ url, method, data, headers, params, }) {
6773
+ async function request({ url, method, data, headers, params, logRequestBody = true, }) {
7109
6774
  const config = getTTMGRC();
7110
6775
  const cookie = sanitizeCookieHeader(config?.cookie);
7111
6776
  const proxyConfig = getAxiosProxyConfig();
@@ -7113,7 +6778,9 @@ async function request({ url, method, data, headers, params, }) {
7113
6778
  printApiRequestLog({
7114
6779
  url,
7115
6780
  method,
7116
- params: getRequestParams(params, data),
6781
+ params: logRequestBody
6782
+ ? getRequestParams(params, data)
6783
+ : '[omitted: large request body]',
7117
6784
  });
7118
6785
  }
7119
6786
  try {
@@ -7124,8 +6791,11 @@ async function request({ url, method, data, headers, params, }) {
7124
6791
  params,
7125
6792
  headers: {
7126
6793
  Cookie: cookie,
7127
- // 'x-use-ppe': '1',
7128
- // 'x-tt-env': 'ppe_upgrade_script',
6794
+ // 注入 PPE header — 放在 caller headers 之前,允许单个调用点通过
6795
+ // 显式传 `x-tt-env` 来覆盖本次请求(例如某个接口还没在 PPE 上发布)。
6796
+ ...(CLI_PPE_ENV
6797
+ ? { 'x-use-ppe': '1', 'x-tt-env': CLI_PPE_ENV }
6798
+ : {}),
7129
6799
  ...(headers || {}),
7130
6800
  },
7131
6801
  ...proxyConfig,
@@ -7167,7 +6837,6 @@ async function request({ url, method, data, headers, params, }) {
7167
6837
  }
7168
6838
  }
7169
6839
  async function download(url, filePath) {
7170
- // 清理旧文件
7171
6840
  if (fs.existsSync(filePath)) {
7172
6841
  try {
7173
6842
  fs.unlinkSync(filePath);
@@ -7175,16 +6844,31 @@ async function download(url, filePath) {
7175
6844
  catch { }
7176
6845
  }
7177
6846
  const proxyConfig = getAxiosProxyConfig();
6847
+ verboseLog('[download] start', { url: url.slice(0, 120), filePath, hasProxy: !!proxyConfig.httpsAgent });
7178
6848
  try {
7179
6849
  const res = await axios.get(url, {
7180
6850
  responseType: 'stream',
7181
- // 让非 2xx 进入 catch
7182
6851
  validateStatus: s => s >= 200 && s < 300,
6852
+ // Bail out if the server doesn't start responding within 30s instead of
6853
+ // hanging forever (e.g. proxy misrouting a CDN signed URL).
6854
+ timeout: 30000,
7183
6855
  ...proxyConfig,
7184
6856
  });
7185
- // 关键:把“流事件”封装为 Promise,并 await
6857
+ const total = Number(res.headers['content-length'] || 0);
6858
+ let received = 0;
6859
+ let lastLoggedPct = -1;
6860
+ const startedAt = Date.now();
7186
6861
  await new Promise((resolve, reject) => {
7187
6862
  const writer = fs.createWriteStream(filePath);
6863
+ // Inactivity watchdog: if no bytes arrive for 60s mid-stream, abort.
6864
+ let inactivityTimer = null;
6865
+ const resetInactivity = () => {
6866
+ if (inactivityTimer)
6867
+ clearTimeout(inactivityTimer);
6868
+ inactivityTimer = setTimeout(() => {
6869
+ onError(new Error('download stalled: no data for 60s'));
6870
+ }, 60000);
6871
+ };
7188
6872
  const onError = (e) => {
7189
6873
  cleanup();
7190
6874
  try {
@@ -7196,28 +6880,42 @@ async function download(url, filePath) {
7196
6880
  };
7197
6881
  const onClose = () => {
7198
6882
  cleanup();
6883
+ verboseLog(`[download] done: ${received} bytes in ${Date.now() - startedAt}ms`);
7199
6884
  resolve();
7200
6885
  };
7201
6886
  const cleanup = () => {
6887
+ if (inactivityTimer)
6888
+ clearTimeout(inactivityTimer);
7202
6889
  writer.off('error', onError);
7203
6890
  writer.off('close', onClose);
7204
6891
  res.data.off('error', onError);
6892
+ res.data.off('data', onData);
6893
+ };
6894
+ const onData = (chunk) => {
6895
+ received += chunk.length;
6896
+ resetInactivity();
6897
+ if (total > 0) {
6898
+ const pct = Math.floor((received / total) * 10) * 10;
6899
+ if (pct !== lastLoggedPct) {
6900
+ lastLoggedPct = pct;
6901
+ verboseLog(`[download] ${pct}% (${received}/${total})`);
6902
+ }
6903
+ }
7205
6904
  };
7206
6905
  res.data.on('error', onError);
6906
+ res.data.on('data', onData);
7207
6907
  writer.on('error', onError);
7208
6908
  writer.on('close', onClose);
6909
+ resetInactivity();
7209
6910
  res.data.pipe(writer);
7210
6911
  });
7211
- // 成功
7212
6912
  return { ok: true };
7213
6913
  }
7214
6914
  catch (err) {
7215
- // 403 等受控处理
6915
+ verboseLog('[download] failed:', err?.message);
7216
6916
  if (isAxiosError(err) && err.response?.status === 403) {
7217
- // 不抛出,让上层自行决定
7218
6917
  throw new Error('下载链接已过期,请重新进行分包后重试');
7219
6918
  }
7220
- // 其他错误抛出或返回
7221
6919
  throw err;
7222
6920
  }
7223
6921
  }
@@ -9212,11 +8910,19 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
9212
8910
  });
9213
8911
  archive.pipe(output);
9214
8912
  // 1. 基础忽略列表 (建议保留这些基础规则,防止包过大)
8913
+ //
8914
+ // 注意 `${TTMG_TEMP_DIR}/**` 必须显式写出:archiver 用的 picomatch 在
8915
+ // 没有 `/**` 后缀时只匹配根目录下的同名条目,不会递归匹配目录内容。
8916
+ // 历史 bug:只写 `__TTMG_TEMP__` 时,`__TTMG_TEMP__/wasmcode/<basename>.br`
8917
+ //(prepare 阶段缓存的「原始未插桩 wasm 备份」)等内部文件全部被打进 zip
8918
+ // 推到设备,体积虚胖外,host 在 fallback 路径下还可能错误命中未插桩 wasm。
8919
+ // 同时保留裸名 `TTMG_TEMP_DIR` 以兜底空目录场景。
9215
8920
  const defaultIgnores = [
9216
8921
  'node_modules/**',
9217
8922
  '.git/**',
9218
8923
  '.DS_Store',
9219
8924
  ttmgPack.TTMG_TEMP_DIR,
8925
+ `${ttmgPack.TTMG_TEMP_DIR}/**`,
9220
8926
  '*.zip', // 忽略自身生成的 zip
9221
8927
  ];
9222
8928
  // 2. 合并自定义规则
@@ -9650,55 +9356,117 @@ const gameCheckRoute = {
9650
9356
 
9651
9357
  const changelog = [
9652
9358
  {
9653
- title: '0.3.8',
9359
+ title: '0.3.9',
9654
9360
  target: {
9655
9361
  iOS: '>=43.1',
9656
9362
  Android: '>=43.1',
9657
9363
  },
9658
9364
  changes: {
9659
- bugfix: [
9365
+ optimize: [
9660
9366
  {
9661
9367
  desc: {
9662
- 'zh-CN': '修复 ttmg login 后 Cookie 中混入 Set-Cookie 属性(Path/Domain/Max-Age 等)且 Max-Age=0 的失效同名 cookie 污染有效值的问题,导致 CLI 请求接口鉴权异常;现已与浏览器行为对齐,剥离属性并过滤过期 cookie,已登录用户无需重新登录',
9663
- 'en-US': 'Fix ttmg login storing raw Set-Cookie attributes (Path/Domain/Max-Age, etc.) into the Cookie field, where same-name cookies with Max-Age=0 polluted valid ones and caused auth failures in CLI requests. Now aligned with browser behavior: strip attributes and drop expired cookies. No re-login required.',
9368
+ 'zh-CN': '调试 IDE 默认使用浅色主题,暗色模式改为手动开启并记忆选择,避免跟随系统强制进入暗色导致不适应',
9369
+ 'en-US': 'The debugging IDE now defaults to the light theme. Dark mode is opt-in and your choice is remembered, so it no longer follows the system and forces an unexpected dark UI.',
9664
9370
  },
9665
- module: 'login',
9371
+ module: 'new',
9666
9372
  },
9667
- ],
9668
- },
9669
- },
9670
- {
9671
- title: '0.3.7',
9672
- target: {
9673
- iOS: '>=43.1',
9674
- Android: '>=43.1',
9675
- },
9676
- changes: {
9677
- feature: [
9678
9373
  {
9679
9374
  desc: {
9680
- 'zh-CN': '新增能力接入助手,帮助开发者基于小游戏变现类型获取关键能力的接入引导',
9681
- 'en-US': 'Add Capability Integration Assistant to guide developers through key capability integration based on the game monetization model',
9375
+ 'zh-CN': '优化暗色模式下二维码显示,调整背景避免过亮刺眼,同时保证扫码识别率',
9376
+ 'en-US': 'Improve the QR code in dark mode by toning down the background so it is no longer glaring while staying easy to scan.',
9682
9377
  },
9683
- module: 'new',
9378
+ module: 'scanQrcode',
9684
9379
  },
9685
- ],
9686
- optimize: [
9687
9380
  {
9688
9381
  desc: {
9689
- 'zh-CN': '优化上传发布环节,将代码预检与代码上传整合为完整的上传发布流程',
9690
- 'en-US': 'Optimize Upload & Publish by combining code precheck and code upload into a complete publishing flow',
9382
+ 'zh-CN': 'Wasm 分包页面及相关组件补齐多语言,修复页面文案中英文混排的问题',
9383
+ 'en-US': 'Complete localization for the Wasm Code Split page and its components, fixing the mixed Chinese/English text.',
9691
9384
  },
9692
- module: 'upload',
9385
+ module: 'check',
9693
9386
  },
9694
9387
  {
9695
9388
  desc: {
9696
- 'zh-CN': '扩充包体大小预检说明,补充整包、主包、独立分包限制规则与详细文档入口',
9697
- 'en-US': 'Expand package size precheck guidance with project, main package, and independent package limits plus a detailed documentation entry',
9389
+ 'zh-CN': '能力接入助手页面视觉升级:变现模式卡片新增图标与强调色,关键能力标签按「必需 / 可选 / 实验」分级展示,信息层级更清晰',
9390
+ 'en-US': 'Refresh the Capability Integration Assistant: monetization-mode cards get icons and accent colors, and capability tags are grouped as Required / Optional / Experimental for a clearer hierarchy.',
9698
9391
  },
9699
- module: 'check',
9392
+ module: 'new',
9700
9393
  },
9701
- ],
9394
+ {
9395
+ desc: {
9396
+ 'zh-CN': '完成扫码连接后,首页标题与说明文案会切换为「已连接」状态,状态反馈更明确',
9397
+ 'en-US': 'After scanning to connect, the home page title and subtitle switch to a "connected" state for clearer feedback.',
9398
+ },
9399
+ module: 'scanQrcode',
9400
+ },
9401
+ {
9402
+ desc: {
9403
+ 'zh-CN': '新版本提示由红点改为「有更新」文字标签,含义更直观;上传游戏包按钮配色与产品主题色统一',
9404
+ 'en-US': 'Replace the ambiguous red dot for new versions with an explicit "Update available" label, and align the upload button color with the product theme.',
9405
+ },
9406
+ module: 'new',
9407
+ },
9408
+ ],
9409
+ bugfix: [
9410
+ {
9411
+ desc: {
9412
+ 'zh-CN': '修复多个页面中区块标题与正文内容未对齐的问题,整体排版更整齐',
9413
+ 'en-US': 'Fix section titles that were misaligned with their body content across multiple pages for a tidier layout.',
9414
+ },
9415
+ module: 'new',
9416
+ },
9417
+ ],
9418
+ },
9419
+ },
9420
+ {
9421
+ title: '0.3.8',
9422
+ target: {
9423
+ iOS: '>=43.1',
9424
+ Android: '>=43.1',
9425
+ },
9426
+ changes: {
9427
+ bugfix: [
9428
+ {
9429
+ desc: {
9430
+ 'zh-CN': '修复 ttmg login 后 Cookie 中混入 Set-Cookie 属性(Path/Domain/Max-Age 等)且 Max-Age=0 的失效同名 cookie 污染有效值的问题,导致 CLI 请求接口鉴权异常;现已与浏览器行为对齐,剥离属性并过滤过期 cookie,已登录用户无需重新登录',
9431
+ 'en-US': 'Fix ttmg login storing raw Set-Cookie attributes (Path/Domain/Max-Age, etc.) into the Cookie field, where same-name cookies with Max-Age=0 polluted valid ones and caused auth failures in CLI requests. Now aligned with browser behavior: strip attributes and drop expired cookies. No re-login required.',
9432
+ },
9433
+ module: 'login',
9434
+ },
9435
+ ],
9436
+ },
9437
+ },
9438
+ {
9439
+ title: '0.3.7',
9440
+ target: {
9441
+ iOS: '>=43.1',
9442
+ Android: '>=43.1',
9443
+ },
9444
+ changes: {
9445
+ feature: [
9446
+ {
9447
+ desc: {
9448
+ 'zh-CN': '新增能力接入助手,帮助开发者基于小游戏变现类型获取关键能力的接入引导',
9449
+ 'en-US': 'Add Capability Integration Assistant to guide developers through key capability integration based on the game monetization model',
9450
+ },
9451
+ module: 'new',
9452
+ },
9453
+ ],
9454
+ optimize: [
9455
+ {
9456
+ desc: {
9457
+ 'zh-CN': '优化上传发布环节,将代码预检与代码上传整合为完整的上传发布流程',
9458
+ 'en-US': 'Optimize Upload & Publish by combining code precheck and code upload into a complete publishing flow',
9459
+ },
9460
+ module: 'upload',
9461
+ },
9462
+ {
9463
+ desc: {
9464
+ 'zh-CN': '扩充包体大小预检说明,补充整包、主包、独立分包限制规则与详细文档入口',
9465
+ 'en-US': 'Expand package size precheck guidance with project, main package, and independent package limits plus a detailed documentation entry',
9466
+ },
9467
+ module: 'check',
9468
+ },
9469
+ ],
9702
9470
  },
9703
9471
  },
9704
9472
  {
@@ -10482,7 +10250,12 @@ const gameUploadRoute = {
10482
10250
  },
10483
10251
  };
10484
10252
 
10253
+ // 历史遗留:`remotePipeline.ts` 里的老远程分包接口 spread 了 `DEV_HEADERS`。
10254
+ // 目前全局 PPE 走的是 `libs/api/request.ts` 里的 `CLI_PPE_ENV`,这里保持
10255
+ // 相同的值是为了:一旦以后需要按接口粒度覆盖 PPE(例如远程走 PPE、本地
10256
+ // 走线上),只需在这里填回 header、两处值天然一致。
10485
10257
  const BASE_URL = 'https://developers.tiktok.com';
10258
+ const WASM_COLLECT_BASE_URL = `${BASE_URL}/api/wasm-collect/v1`;
10486
10259
  const DEV_HEADERS = {
10487
10260
  // 'x-use-ppe': '1',
10488
10261
  // 'x-tt-env': UNITY_PPE_ENV,
@@ -10505,6 +10278,8 @@ const UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME = {
10505
10278
  WASMSPLITVERSION: `"$WASMSPLITVERSION"`,
10506
10279
  ENABLEWASMSPLIT: `"$ENABLEWASMSPLIT"`,
10507
10280
  IOS_SUB_JS_FILE_CONFIG: `"$IOS_SUB_JS_FILE_CONFIG"`,
10281
+ ENABLEARCHIVEMODE: `"$ENABLEARCHIVEMODE"`,
10282
+ ARCHIVE_CODE_FILE_MD5: `$ARCHIVE_CODE_FILE_MD5`,
10508
10283
  };
10509
10284
 
10510
10285
  const DIR_SPLIT = 'split';
@@ -10532,7 +10307,34 @@ const WASM_SPLIT_SUBPACKAGE_CONFIG = {
10532
10307
  name: 'wasmcode1-ios',
10533
10308
  root: 'wasmcode1-ios/',
10534
10309
  },
10310
+ // archive 模式 split 产物:wasmcode/(主包,包含原文件名以外的 main wasm)+
10311
+ // 下面这两个独立子包目录。回滚 / 取消时必须把它们一起清掉,否则下次构建仍会
10312
+ // 把旧的 sub/archive 文件打进 zip 推到设备。
10313
+ archiveSub: {
10314
+ name: 'wasmcode1',
10315
+ root: 'wasmcode1/',
10316
+ },
10317
+ archiveCode: {
10318
+ name: 'wasmcode-archive',
10319
+ root: 'wasmcode-archive/',
10320
+ },
10535
10321
  };
10322
+ /**
10323
+ * 所有由 split 流程生成、应在 cancel / reset 时被整目录删除的产物目录。
10324
+ * 注意不包含 `origin`(wasmcode/)—— 那是 Unity 原生输出目录,回滚时只能
10325
+ * 把里面的 split 产物 .br 清掉、再把原始 wasm 拷回去,整个删掉会破坏工程。
10326
+ *
10327
+ * 单一事实来源:`restoreFromCache` / `resetWasmSplit` / `cancelSplit` 都从
10328
+ * 这里读取,避免新增模式时漏改某一处导致旧产物泄漏到 zip。
10329
+ */
10330
+ const SPLIT_OUTPUT_DIRS = [
10331
+ WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root,
10332
+ WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root,
10333
+ WASM_SPLIT_SUBPACKAGE_CONFIG.ios.root,
10334
+ WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root,
10335
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveSub.root,
10336
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveCode.root,
10337
+ ];
10536
10338
  const WASM_FILENAME_SUFFIX = '.webgl.wasm.code.unityweb.wasm';
10537
10339
  const BR_SUFFIX = '.br';
10538
10340
  // 输出 JSON 格式
@@ -10543,69 +10345,6 @@ const CONCURRENCY_LIMIT = 2;
10543
10345
  const DOWNLOAD_RETRY = 3;
10544
10346
  const WASM_SPLIT_CONFIG_FILE_NAME = 'webgl-wasm-split.js';
10545
10347
 
10546
- // prepare.ts
10547
- // 若你的 request 是 axios:你可以添加 maxBodyLength/ maxContentLength 等参数
10548
- // 若是 got:可直接传 form 实例
10549
- async function startPrepare(params) {
10550
- const form = new FormData$1();
10551
- form.append('desc', params.desc);
10552
- form.append('wasm_md5', params.wasm_md5);
10553
- form.append('with_ios', 'true');
10554
- // 二进制字段:用 ReadStream(推荐)或 Buffer
10555
- form.append('wasm_file', fs$1.createReadStream(path$1.join(process.cwd(), params.wasm_file_path)), {
10556
- filename: path$1.basename(params.wasm_file_path),
10557
- // 部分后端会依赖 content-type;如果不确定就用 application/octet-stream
10558
- contentType: 'application/wasm',
10559
- });
10560
- /**
10561
- * 兼容 WASM_SYMBOL_FILE_NAME
10562
- * case 1:项目根目录有 webgl.symbols.json 文件
10563
- * case 2:项目根目录没有 webgl.symbols.json 文件,但 TTMG_TEMP_DIR 有
10564
- * 优先读 2
10565
- */
10566
- let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
10567
- if (!fs$1.existsSync(symbolFilePath)) {
10568
- symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
10569
- }
10570
- /**
10571
- * 判断是否有 symbol 文件,有则上传,没有直接接口报错
10572
- */
10573
- if (!fs$1.existsSync(symbolFilePath)) {
10574
- return {
10575
- error: {
10576
- code: 400,
10577
- message: `${WASM_SYMBOL_FILE_NAME} not found at ${path$1.join(process.cwd())},use unity plugin to rebuild`,
10578
- client_key: params.client_key,
10579
- },
10580
- data: null,
10581
- ctx: {
10582
- logid: '',
10583
- httpStatusCode: 400,
10584
- },
10585
- };
10586
- }
10587
- form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
10588
- filename: WASM_SYMBOL_FILE_NAME,
10589
- contentType: 'application/octet-stream',
10590
- });
10591
- // 关键:用 form.getHeaders() 获取带 boundary 的 Content-Type
10592
- const formHeaders = form.getHeaders();
10593
- return request({
10594
- url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
10595
- method: 'POST',
10596
- headers: {
10597
- ...DEV_HEADERS,
10598
- ...formHeaders, // 包含正确的 multipart/form-data; boundary=...
10599
- },
10600
- params: {
10601
- client_key: params.client_key,
10602
- with_ios: true,
10603
- },
10604
- data: form,
10605
- // 若 request 基于 axios,建议加上以下两项以支持大文件:
10606
- });
10607
- }
10608
-
10609
10348
  async function withRetry(fn, retries = 3) {
10610
10349
  let lastErr;
10611
10350
  for (let i = 0; i < retries; i++) {
@@ -10622,18 +10361,26 @@ async function withRetry(fn, retries = 3) {
10622
10361
  }
10623
10362
 
10624
10363
  function updateWasmSplitConfig(fields) {
10364
+ const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
10365
+ let config = fs.readFileSync(configFilePath, 'utf-8');
10625
10366
  for (const field in fields) {
10626
10367
  const value = fields[field];
10627
- const isString = typeof value === 'string';
10628
- const valueStr = isString ? value : String(value);
10629
- const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
10630
10368
  const placeholder = UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME[field];
10631
- const config = fs.readFileSync(configFilePath, 'utf-8');
10632
- // 将占位符替换为 true/false 字面量
10633
- // 用正则?因为 placeholder 是一个字符串,可能包含特殊字符
10634
- const updated = config.replace(placeholder, valueStr);
10635
- fs.writeFileSync(configFilePath, updated, 'utf-8');
10369
+ if (!placeholder)
10370
+ continue;
10371
+ let replacement;
10372
+ if (typeof value === 'boolean' || typeof value === 'number') {
10373
+ replacement = String(value);
10374
+ }
10375
+ else if (typeof value === 'string') {
10376
+ replacement = value;
10377
+ }
10378
+ else {
10379
+ replacement = String(value);
10380
+ }
10381
+ config = config.replace(placeholder, replacement);
10636
10382
  }
10383
+ fs.writeFileSync(configFilePath, config, 'utf-8');
10637
10384
  }
10638
10385
 
10639
10386
  async function compressWasmFile(wasmFilePath, compressedFilePath) {
@@ -10645,7 +10392,11 @@ async function compressWasmFile(wasmFilePath, compressedFilePath) {
10645
10392
  }
10646
10393
  function compressArrayBuffer(arrayBuffer) {
10647
10394
  return new Promise((resolve, reject) => {
10648
- const compressStream = zlib.createBrotliCompress();
10395
+ const compressStream = zlib.createBrotliCompress({
10396
+ params: {
10397
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 9,
10398
+ },
10399
+ });
10649
10400
  compressStream.write(Buffer.from(arrayBuffer));
10650
10401
  compressStream.end();
10651
10402
  const compressedChunks = [];
@@ -10676,189 +10427,982 @@ function keepCacheSync({ entryDir, originalWasmPath, originalSplitConfigPath, })
10676
10427
  if (!fs__namespace.existsSync(gameJsonCachePath)) {
10677
10428
  fs__namespace.copyFileSync(gameJsonPath, gameJsonCachePath);
10678
10429
  }
10430
+ /**
10431
+ * 保存 wasmcode/game.js(如果存在)
10432
+ *
10433
+ * 抖音 / 微信 mini-game 平台对 game.json.subpackages 里声明的每个子包
10434
+ * 都要求根目录有 `game.js`(哪怕是空文件)作为 `tt.loadSubpackage` 的
10435
+ * 入口锚点,否则子包加载直接失败。很多 Unity 项目把 wasmcode 当成
10436
+ * 一个内置子包(参考真实工程的 game.json),原始 wasmcode/ 里就有
10437
+ * 一个空 game.js。split 阶段虽然也会重写它,但回退时如果不把这个
10438
+ * 占位文件还原回来,mini-game 启动会因为找不到 wasmcode/game.js
10439
+ * 而崩。
10440
+ *
10441
+ * 这里只在源文件存在时才备份,避免给"原始就没有 game.js"的工程
10442
+ * 偷偷塞一个空文件污染回退状态。
10443
+ */
10444
+ const originGameJsPath = path__namespace.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, 'game.js');
10445
+ const originGameJsCachePath = path__namespace.join(cacheDir, 'wasmcode-game.js');
10446
+ if (fs__namespace.existsSync(originGameJsPath) &&
10447
+ !fs__namespace.existsSync(originGameJsCachePath)) {
10448
+ fs__namespace.copyFileSync(originGameJsPath, originGameJsCachePath);
10449
+ }
10679
10450
  return {
10680
10451
  cacheDir,
10681
10452
  };
10682
10453
  }
10683
10454
 
10684
- async function downloadPrepared(data) {
10685
- wsServer.sendUnitySplitStatus({
10686
- status: 'star_fetch_prepared_wasm_url',
10687
- });
10688
- const res = await request({
10689
- url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
10690
- method: 'POST',
10691
- headers: DEV_HEADERS,
10692
- data,
10693
- });
10694
- wsServer.sendUnitySplitStatus({
10695
- status: 'fetch_prepared_wasm_url_done',
10696
- });
10697
- try {
10698
- const downloadUrl = res?.data?.result?.download_url;
10699
- const willReplaceWasmPath = path.join(process.cwd(), data.wasm_path);
10700
- if (downloadUrl) {
10701
- const { cacheDir } = keepCacheSync({
10702
- entryDir: process.cwd(),
10703
- originalWasmPath: data.wasm_path,
10704
- originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
10705
- });
10706
- if (downloadUrl.includes('.br')) {
10707
- const tempWasmPath = path.join(cacheDir, '__temp__.wasm.br');
10708
- wsServer.sendUnitySplitStatus({
10709
- status: 'start_download_prepared_wasm',
10710
- url: downloadUrl,
10711
- });
10712
- await download(downloadUrl, tempWasmPath);
10713
- /**
10714
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
10715
- */
10716
- fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
10717
- wsServer.sendUnitySplitStatus({
10718
- status: 'download_prepared_wasm_done',
10719
- url: downloadUrl,
10720
- });
10721
- }
10722
- else {
10723
- const tempWasmPath = path.join(cacheDir, '__temp__.wasm');
10724
- wsServer.sendUnitySplitStatus({
10725
- status: 'start_download_prepared_wasm',
10726
- url: downloadUrl,
10727
- });
10728
- await download(downloadUrl, tempWasmPath);
10729
- wsServer.sendUnitySplitStatus({
10730
- status: 'download_prepared_wasm_done',
10731
- url: downloadUrl,
10732
- });
10733
- /**
10734
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
10735
- */
10736
- wsServer.sendUnitySplitStatus({
10737
- status: 'start_compress_prepared_wasm',
10738
- });
10739
- await compressWasmFile(tempWasmPath, willReplaceWasmPath);
10740
- wsServer.sendUnitySplitStatus({
10741
- status: 'compress_prepared_wasm_done',
10742
- url: downloadUrl,
10743
- });
10744
- wsServer.sendUnitySplitStatus({
10745
- status: 'write_compress_prepared_wasm_done',
10746
- });
10455
+ /**
10456
+ * Restore webgl-wasm-split.js from the cached original (with placeholders).
10457
+ * Called at the start of each prepare so that pipeline-specific values can be
10458
+ * applied deterministically, regardless of previous runs.
10459
+ * No-op if the cache does not yet exist (first run).
10460
+ */
10461
+ function restoreSplitConfigFromCache(entryDir = process.cwd()) {
10462
+ const cachedConfigPath = path.join(entryDir, WASM_SPLIT_CACHE_DIR, path.basename(WASM_SPLIT_CONFIG_FILE_NAME));
10463
+ const targetConfigPath = path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME);
10464
+ if (fs.existsSync(cachedConfigPath)) {
10465
+ fs.copyFileSync(cachedConfigPath, targetConfigPath);
10466
+ }
10467
+ }
10468
+
10469
+ /**
10470
+ * Restore the project from the backup cache:
10471
+ * - original (unmodified) wasm file back into its `wasmcode/<file>.br` location
10472
+ * - webgl-wasm-split.js back to its template (with placeholders)
10473
+ * - game.json back to its pre-split version
10474
+ * - wasmcode/game.js — restore from cache if backed up, otherwise write empty
10475
+ * (mini-game runtime requires every subpackage root to contain `game.js` as
10476
+ * a `tt.loadSubpackage` entry anchor; many Unity projects ship with
10477
+ * wasmcode declared as a subpackage in game.json, so a missing wasmcode/game.js
10478
+ * after rollback breaks subpackage loading at boot)
10479
+ * - remove ALL generated sub-package directories (see SPLIT_OUTPUT_DIRS) —
10480
+ * includes both legacy (wasmcode-android / wasmcode1-android / wasmcode-ios /
10481
+ * wasmcode1-ios) and archive-mode (wasmcode1 / wasmcode-archive) outputs
10482
+ * - clean stale split-produced .br files inside wasmcode/ (split phase writes
10483
+ * `${main_wasm_md5}.webgl...br` next to the original `${orig_md5}.webgl...br`;
10484
+ * we wipe everything in there and re-copy the cached original so the dir
10485
+ * ends up byte-identical to the pre-prepare state)
10486
+ *
10487
+ * Shared by both local and remote reset/rollback flows. Single source of
10488
+ * truth for "what does cancel actually undo" — adding a new split-output
10489
+ * dir means appending it to SPLIT_OUTPUT_DIRS, no other call site needs
10490
+ * to change.
10491
+ */
10492
+ function restoreFromCache(entryDir = process.cwd()) {
10493
+ const cacheDir = path.join(entryDir, WASM_SPLIT_CACHE_DIR);
10494
+ // 1) Wipe stale split residue inside wasmcode/ first, THEN restore the
10495
+ // original. Order matters: if we restore first then wipe, we'd delete
10496
+ // the very file we just brought back.
10497
+ const originDir = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root);
10498
+ if (fs.existsSync(originDir)) {
10499
+ for (const entry of fs.readdirSync(originDir)) {
10500
+ // Only clean files split is known to write — `.br` (main wasm) and
10501
+ // the empty `game.js` placeholder. Touching anything else risks
10502
+ // nuking developer-authored content that happens to live in
10503
+ // wasmcode/ for unrelated reasons.
10504
+ if (entry.endsWith('.br') || entry === 'game.js') {
10505
+ fs.rmSync(path.join(originDir, entry), { force: true });
10747
10506
  }
10748
- wsServer.sendUnitySplitStatus({
10749
- status: 'start_update_wasm_split_config',
10750
- });
10751
- /**
10752
- * 读取 webgl-wasm-split.js内容,将 enableWasmCollect 设为 true
10753
- */
10754
- updateWasmSplitConfig({
10755
- ENABLEWASMCOLLECT: true,
10756
- });
10757
- wsServer.sendUnitySplitStatus({
10758
- status: 'update_wasm_split_config_done',
10759
- });
10760
- return {
10761
- isSuccess: true,
10762
- ctx: res?.ctx,
10763
- };
10507
+ }
10508
+ }
10509
+ if (fs.existsSync(cacheDir)) {
10510
+ const targetWasmBrPath = fs
10511
+ .readdirSync(cacheDir)
10512
+ .find(item => item.endsWith('.br'));
10513
+ if (targetWasmBrPath) {
10514
+ const destWasmBrPath = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
10515
+ ensureDirSync(path.dirname(destWasmBrPath));
10516
+ fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
10517
+ }
10518
+ }
10519
+ const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
10520
+ if (fs.existsSync(splitConfigCachePath)) {
10521
+ fs.copyFileSync(splitConfigCachePath, path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME));
10522
+ }
10523
+ const gameJsonCachePath = path.join(cacheDir, 'game.json');
10524
+ if (fs.existsSync(gameJsonCachePath)) {
10525
+ fs.copyFileSync(gameJsonCachePath, path.join(entryDir, 'game.json'));
10526
+ }
10527
+ // Restore wasmcode/game.js. We just deleted whatever was there in step 1,
10528
+ // so we always need to put something back when wasmcode is a subpackage.
10529
+ // Strategy:
10530
+ // - Prefer the cache (keepCacheSync stashes pre-split contents to
10531
+ // `__unity_cache__/wasmcode-game.js` when the original existed)
10532
+ // - Fall back to writing an empty file. Rationale: if we got here the
10533
+ // dir exists, the .br is in place, and the project's game.json — now
10534
+ // restored above — likely still lists wasmcode as a subpackage (true
10535
+ // for every Unity template we ship). An empty game.js is exactly what
10536
+ // downloadSplited.ts also writes; it satisfies the platform requirement
10537
+ // without changing semantics for projects that don't use wasmcode as
10538
+ // a subpackage (the file is harmless empty).
10539
+ const originGameJsCachePath = path.join(cacheDir, 'wasmcode-game.js');
10540
+ const originGameJsDestPath = path.join(originDir, 'game.js');
10541
+ if (fs.existsSync(originDir)) {
10542
+ if (fs.existsSync(originGameJsCachePath)) {
10543
+ fs.copyFileSync(originGameJsCachePath, originGameJsDestPath);
10764
10544
  }
10765
10545
  else {
10766
- return {
10767
- isSuccess: false,
10768
- error: {
10769
- code: res.data?.code,
10770
- message: res.data?.message,
10771
- },
10772
- ctx: res?.ctx,
10773
- };
10546
+ fs.writeFileSync(originGameJsDestPath, '', 'utf-8');
10774
10547
  }
10775
10548
  }
10776
- catch (error) {
10777
- return {
10778
- isSuccess: false,
10779
- error: {
10780
- code: res.data?.code,
10781
- message: error.message,
10782
- },
10783
- ctx: res?.ctx,
10784
- };
10549
+ for (const subDir of SPLIT_OUTPUT_DIRS) {
10550
+ const full = path.join(entryDir, subDir);
10551
+ if (fs.existsSync(full)) {
10552
+ fs.rmSync(full, { recursive: true, force: true });
10553
+ }
10785
10554
  }
10786
10555
  }
10787
10556
 
10788
- async function getCollectedFuncIds({ client_key, wasm_md5, }) {
10789
- return request({
10790
- url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
10791
- method: 'GET',
10792
- headers: DEV_HEADERS,
10793
- params: {
10794
- client_key,
10795
- wasm_md5,
10796
- },
10557
+ async function decompressWasmFile(inputPath, outputPath) {
10558
+ const compressed = await fs.promises.readFile(inputPath);
10559
+ const decompressed = await new Promise((resolve, reject) => {
10560
+ zlib.brotliDecompress(compressed, (err, result) => {
10561
+ if (err)
10562
+ reject(err);
10563
+ else
10564
+ resolve(result);
10565
+ });
10797
10566
  });
10567
+ await fs.promises.writeFile(outputPath, decompressed);
10798
10568
  }
10799
10569
 
10800
- async function setCollect({ client_key, wasm_md5, }) {
10801
- return request({
10802
- url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
10803
- method: 'POST',
10804
- data: {
10805
- client_key,
10806
- wasm_md5,
10807
- },
10808
- headers: DEV_HEADERS,
10809
- });
10570
+ function computeFileMd5Sync(filePath) {
10571
+ const content = fs.readFileSync(filePath);
10572
+ return crypto.createHash('md5').update(content).digest('hex');
10810
10573
  }
10811
10574
 
10812
- async function getCollecttingInfo({ client_key, wasm_md5, }) {
10813
- return request({
10814
- url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
10815
- method: 'GET',
10816
- headers: DEV_HEADERS,
10817
- params: {
10818
- client_key,
10819
- wasm_md5,
10820
- },
10821
- });
10575
+ let cached = null;
10576
+ function getGameJson() {
10577
+ if (cached)
10578
+ return cached;
10579
+ const filePath = path$1.join(process.cwd(), 'game.json');
10580
+ if (fs$1.existsSync(filePath)) {
10581
+ try {
10582
+ cached = JSON.parse(fs$1.readFileSync(filePath, 'utf-8'));
10583
+ }
10584
+ catch {
10585
+ cached = {};
10586
+ }
10587
+ }
10588
+ else {
10589
+ cached = {};
10590
+ }
10591
+ return cached;
10822
10592
  }
10823
10593
 
10824
- // /api/stark_wasm/v4/post/split
10825
- async function startSplit({ client_key, wasm_md5, }) {
10826
- return request({
10827
- url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
10828
- method: 'POST',
10829
- headers: {
10830
- ...DEV_HEADERS,
10831
- },
10832
- data: {
10833
- client_key,
10834
- wasm_md5,
10835
- },
10836
- });
10594
+ function metaFilePath(entryDir = process.cwd()) {
10595
+ return path__namespace$1.join(entryDir, TTMG_TEMP_DIR, 'prepared-meta.json');
10837
10596
  }
10838
-
10839
- /*
10840
- How it works:
10841
- `this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
10842
- */
10843
-
10844
- class Node {
10845
- value;
10846
- next;
10847
-
10848
- constructor(value) {
10849
- this.value = value;
10850
- }
10597
+ function writePreparedMeta(meta, entryDir = process.cwd()) {
10598
+ const target = metaFilePath(entryDir);
10599
+ fs__namespace$1.mkdirSync(path__namespace$1.dirname(target), { recursive: true });
10600
+ const payload = {
10601
+ ...meta,
10602
+ preparedAt: new Date().toISOString(),
10603
+ };
10604
+ fs__namespace$1.writeFileSync(target, JSON.stringify(payload, null, 2), 'utf-8');
10851
10605
  }
10852
-
10853
- class Queue {
10854
- #head;
10855
- #tail;
10856
- #size;
10857
-
10858
- constructor() {
10859
- this.clear();
10860
- }
10861
-
10606
+ function readPreparedMeta(entryDir = process.cwd()) {
10607
+ const target = metaFilePath(entryDir);
10608
+ if (!fs__namespace$1.existsSync(target))
10609
+ return null;
10610
+ try {
10611
+ const raw = fs__namespace$1.readFileSync(target, 'utf-8');
10612
+ const parsed = JSON.parse(raw);
10613
+ if (typeof parsed?.preparedWasmMd5 === 'string' &&
10614
+ typeof parsed?.codePath === 'string' &&
10615
+ parsed.preparedWasmMd5.length === 32) {
10616
+ return parsed;
10617
+ }
10618
+ return null;
10619
+ }
10620
+ catch {
10621
+ return null;
10622
+ }
10623
+ }
10624
+ /**
10625
+ * Return the current md5 of the wasm file referenced by `prepared-meta.json`
10626
+ * or null if the file is missing / meta isn't present. Caller compares the
10627
+ * result to `meta.preparedWasmMd5` — mismatch means the project's wasm
10628
+ * has drifted from the prepared output (Unity re-build etc.) and the
10629
+ * project should be walked back through the prepare step before collect
10630
+ * can produce useful data.
10631
+ */
10632
+ function computeCurrentProjectWasmMd5(entryDir = process.cwd()) {
10633
+ const meta = readPreparedMeta(entryDir);
10634
+ if (!meta)
10635
+ return null;
10636
+ const absolutePath = path__namespace$1.join(entryDir, meta.codePath);
10637
+ if (!fs__namespace$1.existsSync(absolutePath))
10638
+ return null;
10639
+ const currentMd5 = crypto$1
10640
+ .createHash('md5')
10641
+ .update(fs__namespace$1.readFileSync(absolutePath))
10642
+ .digest('hex');
10643
+ return { meta, currentMd5 };
10644
+ }
10645
+
10646
+ const state = {
10647
+ pipelineMode: 'local',
10648
+ originalWasmPath: '',
10649
+ preparedWasmPath: '',
10650
+ codePath: '',
10651
+ splitOutputDir: '',
10652
+ splitMeta: null,
10653
+ totalWasmFuncCount: 0,
10654
+ wasmSize: 0,
10655
+ isArchiveMode: true,
10656
+ };
10657
+ function getLocalState() {
10658
+ return state;
10659
+ }
10660
+ function setLocalState(partial) {
10661
+ Object.assign(state, partial);
10662
+ }
10663
+
10664
+ async function startPrepare$1(params) {
10665
+ const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
10666
+ ensureDirSync(tempDir);
10667
+ const inputPath = path$1.join(process.cwd(), params.wasm_file_path);
10668
+ let rawWasmPath = path$1.join(tempDir, 'original.wasm');
10669
+ if (inputPath.endsWith('.br')) {
10670
+ await decompressWasmFile(inputPath, rawWasmPath);
10671
+ }
10672
+ else {
10673
+ fs$1.copyFileSync(inputPath, rawWasmPath);
10674
+ }
10675
+ const preparedWasmPath = path$1.join(tempDir, 'prepared.wasm');
10676
+ try {
10677
+ const result = ttmgWasmtool.prepare(rawWasmPath, preparedWasmPath);
10678
+ verboseLog(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
10679
+ const gameJson = getGameJson();
10680
+ const totalWasmFuncCount = gameJson.wasmFuncCount ?? 0;
10681
+ const wasmSize = fs$1.existsSync(inputPath)
10682
+ ? fs$1.statSync(inputPath).size
10683
+ : 0;
10684
+ setLocalState({
10685
+ originalWasmPath: rawWasmPath,
10686
+ preparedWasmPath,
10687
+ codePath: params.wasm_file_path,
10688
+ totalWasmFuncCount,
10689
+ wasmSize,
10690
+ });
10691
+ keepCacheSync({
10692
+ entryDir: process.cwd(),
10693
+ originalWasmPath: params.wasm_file_path,
10694
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
10695
+ });
10696
+ // Start from cached (placeholder) config so pipeline switching is deterministic
10697
+ restoreSplitConfigFromCache();
10698
+ const willReplaceWasmPath = path$1.join(process.cwd(), params.wasm_file_path);
10699
+ // Diagnostic: prove prepare actually produced a different binary
10700
+ // (size should grow noticeably because every function body is prefixed
10701
+ // with a scwebgl.logCall(funcIndex) call).
10702
+ const rawSize = fs$1.existsSync(rawWasmPath) ? fs$1.statSync(rawWasmPath).size : 0;
10703
+ const preparedSize = fs$1.existsSync(preparedWasmPath)
10704
+ ? fs$1.statSync(preparedWasmPath).size
10705
+ : 0;
10706
+ const rawMd5 = fs$1.existsSync(rawWasmPath)
10707
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(rawWasmPath)).digest('hex')
10708
+ : '<missing>';
10709
+ const preparedMd5 = fs$1.existsSync(preparedWasmPath)
10710
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(preparedWasmPath)).digest('hex')
10711
+ : '<missing>';
10712
+ verboseLog(`[wasmtool] prepare sanity: raw(size=${rawSize} md5=${rawMd5}) -> prepared(size=${preparedSize} md5=${preparedMd5}) delta=${preparedSize - rawSize}`);
10713
+ if (preparedSize <= rawSize || preparedMd5 === rawMd5) {
10714
+ verboseWarn('[wasmtool] WARNING: prepared wasm is not larger / md5 is unchanged vs raw wasm. Instrumentation likely did not happen.');
10715
+ }
10716
+ verboseLog('[wasmtool] compressing prepared wasm (quality=9)...');
10717
+ await compressWasmFile(preparedWasmPath, willReplaceWasmPath);
10718
+ verboseLog('[wasmtool] compressed and written to project');
10719
+ // Diagnostic: confirm the file the client actually fetches was overwritten,
10720
+ // and compare to the cached original brotli so we can prove on-disk replacement.
10721
+ const replacedSize = fs$1.existsSync(willReplaceWasmPath)
10722
+ ? fs$1.statSync(willReplaceWasmPath).size
10723
+ : 0;
10724
+ const replacedMd5 = fs$1.existsSync(willReplaceWasmPath)
10725
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(willReplaceWasmPath)).digest('hex')
10726
+ : '<missing>';
10727
+ const cachedOriginalBr = path$1.join(process.cwd(), TTMG_TEMP_DIR, 'wasmcode', path$1.basename(params.wasm_file_path));
10728
+ const cachedOriginalSize = fs$1.existsSync(cachedOriginalBr)
10729
+ ? fs$1.statSync(cachedOriginalBr).size
10730
+ : 0;
10731
+ const cachedOriginalMd5 = fs$1.existsSync(cachedOriginalBr)
10732
+ ? crypto$1
10733
+ .createHash('md5')
10734
+ .update(fs$1.readFileSync(cachedOriginalBr))
10735
+ .digest('hex')
10736
+ : '<missing>';
10737
+ verboseLog(`[wasmtool] on-disk replace check: project=${params.wasm_file_path} size=${replacedSize} md5=${replacedMd5} | cached-original size=${cachedOriginalSize} md5=${cachedOriginalMd5}`);
10738
+ if (replacedMd5 === cachedOriginalMd5) {
10739
+ verboseWarn('[wasmtool] WARNING: project wasm md5 matches cached-original md5. The file was not actually replaced with the instrumented build.');
10740
+ }
10741
+ else {
10742
+ verboseLog('[wasmtool] OK: project wasm differs from cached-original — instrumented wasm is on disk.');
10743
+ }
10744
+ // Local pipeline uses the new wasm-collect/v1/report API + archive sub-wasm.
10745
+ // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
10746
+ // reports the correct wasm_md5 during the collect phase.
10747
+ updateWasmSplitConfig({
10748
+ ENABLEWASMCOLLECT: true,
10749
+ ENABLEARCHIVEMODE: true,
10750
+ ORIGINALWASMMD5: params.wasm_md5,
10751
+ });
10752
+ verboseLog('[wasmtool] wasm split config updated (local pipeline: archive=true)');
10753
+ // Disk-persisted anchor for "wasm drift" detection in
10754
+ // `game-wasm-split-config` route. Stores the md5 that prepare just
10755
+ // wrote into the project alongside the project-relative path. The
10756
+ // route reads this back on every Modal open, recomputes the md5 of
10757
+ // the file on disk, and if they differ (Unity re-build, git
10758
+ // checkout, etc.) suppresses `enableWasmCollect=true` in the
10759
+ // response so the IDE goes back through prepare instead of dropping
10760
+ // the user straight into Collect with an un-instrumented wasm on
10761
+ // the device. See `preparedMeta.ts` for full rationale.
10762
+ writePreparedMeta({
10763
+ preparedWasmMd5: replacedMd5,
10764
+ codePath: params.wasm_file_path,
10765
+ });
10766
+ verboseLog(`[wasmtool] prepared-meta written: md5=${replacedMd5} codePath=${params.wasm_file_path}`);
10767
+ return {
10768
+ data: {
10769
+ code: 0,
10770
+ message: 'success',
10771
+ result: { md5: params.wasm_md5 },
10772
+ },
10773
+ error: null,
10774
+ ctx: { logid: 'local', httpStatusCode: 200 },
10775
+ };
10776
+ }
10777
+ catch (err) {
10778
+ return {
10779
+ data: null,
10780
+ error: {
10781
+ code: 500,
10782
+ message: err instanceof Error ? err.message : String(err),
10783
+ },
10784
+ ctx: { logid: 'local', httpStatusCode: 500 },
10785
+ };
10786
+ }
10787
+ }
10788
+
10789
+ /**
10790
+ * Local pipeline: startPrepareLocal already compressed/replaced the wasm and
10791
+ * updated webgl-wasm-split.js, so this step is a no-op that just emits UI
10792
+ * status events for parity with the remote flow.
10793
+ */
10794
+ async function downloadPrepared$1(_data) {
10795
+ const { preparedWasmPath } = getLocalState();
10796
+ if (!preparedWasmPath) {
10797
+ return {
10798
+ isSuccess: false,
10799
+ error: { code: 404, message: 'Prepared wasm not found. Run prepare first.' },
10800
+ };
10801
+ }
10802
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
10803
+ return { isSuccess: true, ctx: { logid: 'local' } };
10804
+ }
10805
+
10806
+ async function getCollectedFuncIds$1({ client_key, wasm_md5, }) {
10807
+ const res = await request({
10808
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
10809
+ method: 'GET',
10810
+ params: {
10811
+ app_id: client_key,
10812
+ wasm_md5,
10813
+ },
10814
+ });
10815
+ const funcCount = res?.data?.func_count ?? 0;
10816
+ return {
10817
+ data: {
10818
+ code: res?.data?.code ?? 0,
10819
+ message: 'success',
10820
+ result: {
10821
+ collected_func_count: funcCount,
10822
+ data_size: funcCount,
10823
+ real_data_size: funcCount,
10824
+ collect_state: res?.data?.collect_state,
10825
+ },
10826
+ },
10827
+ error: res.error,
10828
+ ctx: res.ctx,
10829
+ };
10830
+ }
10831
+
10832
+ /**
10833
+ * POST /start — opens a collect session (Portal-authenticated).
10834
+ *
10835
+ * Idempotent on the server: re-opening an already-open session just refreshes
10836
+ * `started_at`; only `reset: true` wipes history.
10837
+ *
10838
+ * Default `reset` is `false` to mirror the server-side default documented in
10839
+ * `wasm_api.md` §5.1 — "页面刷新 / 恢复" must NOT silently destroy data. The
10840
+ * "fresh run" semantic (e.g. user clicks "重新开始分包") is the responsibility
10841
+ * of the caller, which must explicitly pass `reset: true`. See `setCollect`
10842
+ * for the CLI-level wiring of those two paths.
10843
+ *
10844
+ * NOTE on naming: the server route is flat (`/start`, not `/session/start`).
10845
+ * Our local symbol stays `startWasmSession` because it's the "start collect
10846
+ * session" lifecycle primitive from the IDE's perspective.
10847
+ */
10848
+ async function startWasmSession({ client_key, wasm_md5, reset, }) {
10849
+ const res = await request({
10850
+ url: `${WASM_COLLECT_BASE_URL}/start`,
10851
+ method: 'POST',
10852
+ data: {
10853
+ app_id: client_key,
10854
+ wasm_md5,
10855
+ reset: reset ?? false,
10856
+ },
10857
+ });
10858
+ return {
10859
+ data: res.data
10860
+ ? {
10861
+ code: res.data.code ?? 0,
10862
+ message: res.data.message || 'success',
10863
+ result: {
10864
+ collect_state: res.data.collect_state,
10865
+ started_at: res.data.started_at,
10866
+ },
10867
+ }
10868
+ : null,
10869
+ error: res.error,
10870
+ ctx: res.ctx,
10871
+ };
10872
+ }
10873
+
10874
+ /**
10875
+ * 打开 server 端 collect session(本地 pipeline 的 `/start`)。
10876
+ *
10877
+ * 这是**鉴权门**:`/start` 走 Portal 鉴权中间件,登录态失效会返回 `-401`
10878
+ * (或带登录关键字的 `-1`)。鉴权失败必须立即把错误回给 IDE 并中止——
10879
+ * 否则用户进了"正在收集",但 plugin 之后所有 `/report` 都会被 fail-close
10880
+ * 丢弃,函数数量永远 0,且很难归因。
10881
+ *
10882
+ * IDE 通过独立路由 `/game/wasm-collect-start` 调用本函数,所以它在浏览器
10883
+ * Network 里是一条可见且**明确叫 start** 的请求——出鉴权问题时一眼能定位到
10884
+ * 是开 session 这步失败,而不是被误认为 collect 轮询接口的问题。symbols
10885
+ * 上传是另一步(`uploadCollectSymbols`),与开 session 解耦。
10886
+ *
10887
+ * 两种语义(与 `wasm_api.md` §5.1 对齐):
10888
+ * - 默认(`resume` 缺省 / false)→ `reset: true`,服务端清空历史。
10889
+ * - `resume: true` → `reset: false`,幂等打开、保留已有 func_ids。
10890
+ */
10891
+ async function openCollectSession$1({ client_key, wasm_md5, resume, }) {
10892
+ const startRes = await startWasmSession({
10893
+ client_key,
10894
+ wasm_md5,
10895
+ reset: !resume,
10896
+ });
10897
+ if (startRes.error || !startRes.data || startRes.data.code !== 0) {
10898
+ // 结构化日志带 logid——找后端排查鉴权/会话问题时最有用的字段。
10899
+ const code = startRes.error?.code ?? startRes.data?.code ?? -1;
10900
+ const message = startRes.error?.message ||
10901
+ startRes.data?.message ||
10902
+ 'Open collect session failed';
10903
+ const logid = startRes.ctx?.logid || 'n/a';
10904
+ console.error(`[wasm-collect] /start failed: code=${code} message=${message} logid=${logid}`);
10905
+ return {
10906
+ data: startRes.data ?? null,
10907
+ error: startRes.error ?? { code, message },
10908
+ ctx: startRes.ctx,
10909
+ };
10910
+ }
10911
+ return {
10912
+ data: { code: 0, message: 'success', result: startRes.data.result },
10913
+ error: null,
10914
+ ctx: startRes.ctx,
10915
+ };
10916
+ }
10917
+ /**
10918
+ * 上传符号表(`/symbols`)。只在 session 已打开后调用。
10919
+ *
10920
+ * 符号表只是给 server 端后续调试用的 debug 信息,丢了不影响分包主链路,
10921
+ * 所以这里故意不 await、失败仅 warn,且总是返回 `{code: 0}`——它不应该
10922
+ * 阻塞或失败掉"开始收集"流程。
10923
+ */
10924
+ async function uploadCollectSymbols$1({ client_key, wasm_md5, }) {
10925
+ let symbolPath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
10926
+ if (!fs$1.existsSync(symbolPath)) {
10927
+ symbolPath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
10928
+ }
10929
+ if (fs$1.existsSync(symbolPath)) {
10930
+ const symbols = fs$1.readFileSync(symbolPath, 'utf-8');
10931
+ request({
10932
+ url: `${WASM_COLLECT_BASE_URL}/symbols`,
10933
+ method: 'POST',
10934
+ // symbols 是整张符号表,verbose 下打出来会刷屏,关掉它的入参日志。
10935
+ logRequestBody: false,
10936
+ data: {
10937
+ app_id: client_key,
10938
+ wasm_md5,
10939
+ symbols,
10940
+ },
10941
+ }).catch(err => {
10942
+ verboseWarn('[wasmtool] Failed to upload symbols:', err);
10943
+ });
10944
+ }
10945
+ return {
10946
+ data: { code: 0, message: 'success', result: {} },
10947
+ error: null,
10948
+ ctx: { logid: 'local', httpStatusCode: 200 },
10949
+ };
10950
+ }
10951
+
10952
+ async function getCollecttingInfo$1({ client_key, wasm_md5, }) {
10953
+ const res = await request({
10954
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
10955
+ method: 'GET',
10956
+ params: {
10957
+ app_id: client_key,
10958
+ wasm_md5,
10959
+ },
10960
+ });
10961
+ const { totalWasmFuncCount } = getLocalState();
10962
+ // Fall back to game.json.wasmFuncCount so the total survives CLI restarts.
10963
+ const gameJsonFuncCount = Number(getGameJson()?.wasmFuncCount) || 0;
10964
+ return {
10965
+ data: {
10966
+ code: res?.data?.code ?? 0,
10967
+ message: 'success',
10968
+ result: {
10969
+ app_id: client_key,
10970
+ wasm_md5,
10971
+ collected_func_count: res?.data?.func_count ?? 0,
10972
+ total_wasm_func_count: gameJsonFuncCount || totalWasmFuncCount || 0,
10973
+ collect_state: res?.data?.collect_state,
10974
+ },
10975
+ },
10976
+ error: res.error,
10977
+ ctx: res.ctx,
10978
+ };
10979
+ }
10980
+
10981
+ /**
10982
+ * POST /finish — closes a collect session and returns the final `func_count`
10983
+ * so the IDE can surface "本次共收集 N 个函数" in the success dialog.
10984
+ * Idempotent on the server.
10985
+ *
10986
+ * NOTE on naming: the server route is flat (`/finish`, not `/session/finish`).
10987
+ * The local symbol keeps `finishWasmSession` for symmetry with `startWasmSession`.
10988
+ */
10989
+ async function finishWasmSession({ client_key, wasm_md5, }) {
10990
+ const res = await request({
10991
+ url: `${WASM_COLLECT_BASE_URL}/finish`,
10992
+ method: 'POST',
10993
+ data: {
10994
+ app_id: client_key,
10995
+ wasm_md5,
10996
+ },
10997
+ });
10998
+ return {
10999
+ data: res.data
11000
+ ? {
11001
+ code: res.data.code ?? 0,
11002
+ message: res.data.message || 'success',
11003
+ result: {
11004
+ collect_state: res.data.collect_state,
11005
+ func_count: res.data.func_count ?? 0,
11006
+ finished_at: res.data.finished_at,
11007
+ },
11008
+ }
11009
+ : null,
11010
+ error: res.error,
11011
+ ctx: res.ctx,
11012
+ };
11013
+ }
11014
+
11015
+ async function startSplit$1({ client_key, wasm_md5, }) {
11016
+ const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
11017
+ const splitOutputDir = path$1.join(tempDir, 'split-output');
11018
+ if (fs$1.existsSync(splitOutputDir)) {
11019
+ fs$1.rmSync(splitOutputDir, { recursive: true, force: true });
11020
+ }
11021
+ ensureDirSync(splitOutputDir);
11022
+ const { originalWasmPath, isArchiveMode: archive } = getLocalState();
11023
+ const rawWasmPath = originalWasmPath || path$1.join(tempDir, 'original.wasm');
11024
+ if (!fs$1.existsSync(rawWasmPath)) {
11025
+ return {
11026
+ data: null,
11027
+ error: {
11028
+ code: 404,
11029
+ message: 'Original wasm not found. Run prepare first.',
11030
+ },
11031
+ ctx: { logid: 'local', httpStatusCode: 404 },
11032
+ };
11033
+ }
11034
+ const exportRes = await request({
11035
+ url: `${WASM_COLLECT_BASE_URL}/export`,
11036
+ method: 'GET',
11037
+ params: {
11038
+ app_id: client_key,
11039
+ wasm_md5,
11040
+ strategy: 'union',
11041
+ },
11042
+ });
11043
+ const funcIds = exportRes?.data?.func_ids;
11044
+ const bootFuncIds = exportRes?.data?.boot_func_ids ?? [];
11045
+ if (!funcIds?.length) {
11046
+ return {
11047
+ data: null,
11048
+ error: {
11049
+ code: 400,
11050
+ message: 'No collected func IDs found.',
11051
+ },
11052
+ ctx: { logid: 'local', httpStatusCode: 400 },
11053
+ };
11054
+ }
11055
+ verboseLog(`[wasmtool] splitting with ${funcIds.length} func IDs` +
11056
+ (bootFuncIds.length > 0
11057
+ ? `, ${bootFuncIds.length} boot-phase func IDs (→ alwaysInclude)`
11058
+ : ', no boot-phase info (legacy server, falling back to callClosure only)') +
11059
+ `, archive=${archive}`);
11060
+ try {
11061
+ const result = ttmgWasmtool.split({
11062
+ input: rawWasmPath,
11063
+ funcIds,
11064
+ // Boot-phase func ids → `alwaysInclude`. They are a subset of
11065
+ // `funcIds` so this doesn't grow `collect_count`, but it DOES seed
11066
+ // the direct-call closure BFS with the exact set needed for first
11067
+ // frame, and the split tool's `alwaysIncludeAdded` counter is the
11068
+ // observability signal when zero (= server didn't return boot info).
11069
+ alwaysInclude: bootFuncIds.length > 0 ? bootFuncIds : undefined,
11070
+ // Always-on direct-call closure over (collect ∪ alwaysInclude ∪
11071
+ // start_func). Folds in func ids that collect missed (untaken
11072
+ // branches, race conditions during collect) so first-screen code
11073
+ // paths don't trap on archive trampolines. See the split tool's
11074
+ // `closure_added` counter for the per-build size impact.
11075
+ callClosure: true,
11076
+ // Always-on indirect-call type-closure scoped to the boot subset.
11077
+ // Catches IL2CPP virtual / interface / delegate dispatch which is
11078
+ // the dominant source of remaining `firstFrame=BEFORE` archive
11079
+ // trampoline hits after the runtime collect + direct closure
11080
+ // passes (see `indirectClosureAdded` for the per-build size
11081
+ // impact). Defaults to `true` in the wasmtool but we set it
11082
+ // explicitly so a future tool default change can't silently turn
11083
+ // it off in our pipeline.
11084
+ callIndirectClosure: true,
11085
+ outputDir: splitOutputDir,
11086
+ archive,
11087
+ compress: true,
11088
+ quality: 9,
11089
+ });
11090
+ if (result.code !== 0) {
11091
+ return {
11092
+ data: null,
11093
+ error: { code: result.code, message: result.errMsg },
11094
+ ctx: { logid: 'local', httpStatusCode: 500 },
11095
+ };
11096
+ }
11097
+ const mainBrPath = result.mainWasmPath + '.br';
11098
+ const actualMainPath = fs$1.existsSync(mainBrPath)
11099
+ ? mainBrPath
11100
+ : result.mainWasmPath;
11101
+ const mainWasmMd5 = computeFileMd5Sync(actualMainPath);
11102
+ const subBrPath = result.subWasmPath ? result.subWasmPath + '.br' : '';
11103
+ const actualSubPath = subBrPath && fs$1.existsSync(subBrPath)
11104
+ ? subBrPath
11105
+ : result.subWasmPath;
11106
+ const subWasmMd5 = actualSubPath
11107
+ ? computeFileMd5Sync(actualSubPath)
11108
+ : '';
11109
+ let archiveMd5 = '';
11110
+ if (archive && result.archivePath) {
11111
+ const archiveBrPath = result.archivePath + '.br';
11112
+ const actualArchivePath = fs$1.existsSync(archiveBrPath)
11113
+ ? archiveBrPath
11114
+ : result.archivePath;
11115
+ verboseLog(`[wasmtool] archivePath=${result.archivePath}, brExists=${fs$1.existsSync(archiveBrPath)}, actualExists=${fs$1.existsSync(actualArchivePath)}`);
11116
+ if (fs$1.existsSync(actualArchivePath)) {
11117
+ archiveMd5 = computeFileMd5Sync(actualArchivePath);
11118
+ verboseLog(`[wasmtool] archive_md5=${archiveMd5}`);
11119
+ }
11120
+ }
11121
+ else {
11122
+ verboseLog(`[wasmtool] skip archive md5: archive=${archive}, archivePath=${result.archivePath}`);
11123
+ }
11124
+ const globalVarList = result.globalVarList
11125
+ .split(';')
11126
+ .filter(Boolean)
11127
+ .map((entry) => {
11128
+ const [name, type, mutable] = entry.trim().split(',');
11129
+ return { name, type, mutable: mutable === '1' };
11130
+ });
11131
+ const splitMeta = {
11132
+ original_wasm_md5: wasm_md5,
11133
+ main_wasm_md5: mainWasmMd5,
11134
+ main_wasm_h5_md5: mainWasmMd5,
11135
+ sub_wasm_md5: subWasmMd5,
11136
+ archive_md5: archiveMd5,
11137
+ table_size: result.tableSize,
11138
+ global_var_list: globalVarList,
11139
+ version: Date.now(),
11140
+ total_wasm_count: result.totalWasmCount,
11141
+ main_wasm_count: result.mainWasmCount,
11142
+ time_cost: result.timeCost,
11143
+ archive,
11144
+ local_main_wasm_path: result.mainWasmPath,
11145
+ local_sub_wasm_path: result.subWasmPath,
11146
+ local_func_meta_path: result.funcMetaPath,
11147
+ local_archive_path: result.archivePath,
11148
+ // Composition breakdown of main_funcs — the single most useful
11149
+ // piece of information when triaging "why is my main package X MB"
11150
+ // (or, conversely, "why are first-screen sub-package batches still
11151
+ // loading"). collect = runtime-observed, always_include =
11152
+ // boot_func_ids, closure = BFS direct callees, indirect_closure =
11153
+ // type-matching pass scoped to boot funcs (covers IL2CPP virtual
11154
+ // dispatch), export = wasm exports. These sum with imports to
11155
+ // main_wasm_count.
11156
+ collect_func_count: result.collectFuncCount,
11157
+ always_include_added: result.alwaysIncludeAdded,
11158
+ closure_added: result.closureAdded,
11159
+ indirect_closure_added: result.indirectClosureAdded,
11160
+ indirect_closure_types: result.indirectClosureTypes,
11161
+ export_added: result.exportAdded,
11162
+ };
11163
+ setLocalState({ splitOutputDir, splitMeta });
11164
+ verboseLog(`[wasmtool] split done: total=${result.totalWasmCount}, main=${result.mainWasmCount} ` +
11165
+ `(collect=${result.collectFuncCount}, +alwaysInclude=${result.alwaysIncludeAdded}, ` +
11166
+ `+closure=${result.closureAdded}, +indirectClosure=${result.indirectClosureAdded}` +
11167
+ `[types=${result.indirectClosureTypes}], +exports=${result.exportAdded}), ` +
11168
+ `time=${result.timeCost}s`);
11169
+ // Split landed — close the collect session so the plugin stops reporting.
11170
+ // Awaited (not fire-and-forget) so IDE can rely on "wasm-split returned
11171
+ // success" meaning "session definitively closed". If /finish itself
11172
+ // fails (e.g. portal cookie expired mid-run) we still return split
11173
+ // success to the IDE — the plugin already has the MD5-bound session
11174
+ // state from the earlier /report responses and will time out on TTL
11175
+ // anyway; failing split for a finalizer hiccup would be worse UX.
11176
+ let funcCount;
11177
+ try {
11178
+ const finishRes = await finishWasmSession({ client_key, wasm_md5 });
11179
+ if (finishRes.error || !finishRes.data || finishRes.data.code !== 0) {
11180
+ // Soft failure: split already succeeded from the user's POV, but
11181
+ // this is the main diagnostic breadcrumb if someone later reports
11182
+ // "plugin kept uploading after 分包完成". Always include logid so
11183
+ // backend can cross-reference without having to know our build.
11184
+ const code = finishRes.error?.code ?? finishRes.data?.code ?? -1;
11185
+ const message = finishRes.error?.message ||
11186
+ finishRes.data?.message ||
11187
+ 'finish session non-success';
11188
+ const logid = finishRes.ctx?.logid || 'n/a';
11189
+ console.error(`[wasm-split] /finish failed (split still succeeded): code=${code} message=${message} logid=${logid}`);
11190
+ }
11191
+ else {
11192
+ funcCount = finishRes.data.result?.func_count;
11193
+ }
11194
+ }
11195
+ catch (e) {
11196
+ const msg = e instanceof Error ? e.message : String(e);
11197
+ console.error(`[wasm-split] /finish threw (split still succeeded): ${msg}`);
11198
+ }
11199
+ return {
11200
+ data: { code: 0, message: 'success', func_count: funcCount },
11201
+ error: null,
11202
+ ctx: { logid: 'local', httpStatusCode: 200 },
11203
+ };
11204
+ }
11205
+ catch (err) {
11206
+ return {
11207
+ data: null,
11208
+ error: {
11209
+ code: 500,
11210
+ message: err instanceof Error ? err.message : String(err),
11211
+ },
11212
+ ctx: { logid: 'local', httpStatusCode: 500 },
11213
+ };
11214
+ }
11215
+ }
11216
+
11217
+ const ARCHIVE_SUBPACKAGE_CONFIG = [
11218
+ WASM_SPLIT_SUBPACKAGE_CONFIG.origin,
11219
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveSub,
11220
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveCode,
11221
+ ];
11222
+ function updateSubpackageConfigSync(archive = false) {
11223
+ const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
11224
+ const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
11225
+ const gameJson = JSON.parse(raw);
11226
+ delete gameJson.wasmFuncCount;
11227
+ const fieldName = SUBPACKAGE_FIELD_NAMES.find(k => k in gameJson) ??
11228
+ SUBPACKAGE_FIELD_NAMES[0];
11229
+ if (!gameJson[fieldName])
11230
+ gameJson[fieldName] = [];
11231
+ const subpackages = gameJson[fieldName];
11232
+ const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
11233
+ if (archive) {
11234
+ ARCHIVE_SUBPACKAGE_CONFIG.forEach(pkg => filtered.push(pkg));
11235
+ }
11236
+ else {
11237
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
11238
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
11239
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
11240
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
11241
+ }
11242
+ const map = new Map(filtered.map(s => [s.name, s]));
11243
+ gameJson[fieldName] = Array.from(map.values());
11244
+ fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11245
+ }
11246
+
11247
+ async function downloadSplited$1(_context) {
11248
+ const cwd = process.cwd();
11249
+ const { splitMeta } = getLocalState();
11250
+ if (!splitMeta) {
11251
+ return {
11252
+ data: { isSuccess: false },
11253
+ error: { message: 'No local split result found. Run split first.' },
11254
+ ctx: _context,
11255
+ };
11256
+ }
11257
+ const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11258
+ ensureDirSync(splitTempDir);
11259
+ const isArchive = splitMeta.archive;
11260
+ const mainAndroidDir = path.join(splitTempDir, isArchive ? 'wasmcode' : WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
11261
+ const subAndroidDir = path.join(splitTempDir, isArchive ? 'wasmcode1' : WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
11262
+ const mainIosDir = isArchive
11263
+ ? mainAndroidDir
11264
+ : path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
11265
+ const subIosDir = path.join(splitTempDir, isArchive ? 'wasmcode-archive' : WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
11266
+ const dirs = [...new Set([mainAndroidDir, subAndroidDir, mainIosDir, subIosDir])];
11267
+ dirs.forEach(ensureDirSync);
11268
+ try {
11269
+ verboseLog('[wasmtool] organizing split output...');
11270
+ const mainWasmMd5 = splitMeta.main_wasm_md5;
11271
+ const subWasmMd5 = splitMeta.sub_wasm_md5;
11272
+ const mainWasmH5Md5 = splitMeta.main_wasm_h5_md5;
11273
+ const localMainPath = splitMeta.local_main_wasm_path;
11274
+ const mainBrPath = localMainPath + BR_SUFFIX;
11275
+ const actualMainPath = fs.existsSync(mainBrPath) ? mainBrPath : localMainPath;
11276
+ if (actualMainPath && fs.existsSync(actualMainPath)) {
11277
+ const isBr = actualMainPath.endsWith(BR_SUFFIX);
11278
+ const ext = isBr
11279
+ ? `${WASM_FILENAME_SUFFIX}${BR_SUFFIX}`
11280
+ : WASM_FILENAME_SUFFIX;
11281
+ const mainAndroidDest = path.join(mainAndroidDir, `${mainWasmMd5}${ext}`);
11282
+ fs.copyFileSync(actualMainPath, mainAndroidDest);
11283
+ wsServer.sendUnitySplitStatus({
11284
+ status: 'download_android_main_wasm_done',
11285
+ });
11286
+ if (mainIosDir !== mainAndroidDir) {
11287
+ const mainIosDest = path.join(mainIosDir, `${mainWasmH5Md5}${ext}`);
11288
+ fs.copyFileSync(actualMainPath, mainIosDest);
11289
+ }
11290
+ wsServer.sendUnitySplitStatus({
11291
+ status: 'download_ios_main_wasm_done',
11292
+ });
11293
+ }
11294
+ const localSubPath = splitMeta.local_sub_wasm_path;
11295
+ const subBrPath = localSubPath + BR_SUFFIX;
11296
+ const actualSubPath = fs.existsSync(subBrPath) ? subBrPath : localSubPath;
11297
+ if (actualSubPath && fs.existsSync(actualSubPath)) {
11298
+ const isBr = actualSubPath.endsWith(BR_SUFFIX);
11299
+ const ext = isBr
11300
+ ? `${WASM_FILENAME_SUFFIX}${BR_SUFFIX}`
11301
+ : WASM_FILENAME_SUFFIX;
11302
+ const subAndroidDest = path.join(subAndroidDir, `${subWasmMd5}${ext}`);
11303
+ fs.copyFileSync(actualSubPath, subAndroidDest);
11304
+ wsServer.sendUnitySplitStatus({
11305
+ status: 'download_android_sub_wasm_code_done',
11306
+ });
11307
+ }
11308
+ const localArchivePath = splitMeta.local_archive_path;
11309
+ if (isArchive && localArchivePath) {
11310
+ const archiveBrPath = localArchivePath + BR_SUFFIX;
11311
+ const actualArchivePath = fs.existsSync(archiveBrPath) ? archiveBrPath : localArchivePath;
11312
+ verboseLog(`[wasmtool] archive copy: archive_md5=${splitMeta.archive_md5}, localPath=${localArchivePath}, brExists=${fs.existsSync(archiveBrPath)}, actual=${actualArchivePath}`);
11313
+ if (fs.existsSync(actualArchivePath)) {
11314
+ const archiveMd5 = splitMeta.archive_md5 || '';
11315
+ const archiveBaseName = path.basename(actualArchivePath);
11316
+ const destName = archiveMd5 ? `${archiveMd5}.${archiveBaseName}` : archiveBaseName;
11317
+ const archiveDest = path.join(subIosDir, destName);
11318
+ verboseLog(`[wasmtool] archive dest: ${archiveDest}`);
11319
+ fs.copyFileSync(actualArchivePath, archiveDest);
11320
+ }
11321
+ }
11322
+ dirs.forEach((dir) => {
11323
+ fs.writeFileSync(path.join(dir, 'game.js'), '', { encoding: 'utf-8' });
11324
+ });
11325
+ verboseLog('[wasmtool] copy split output to root...');
11326
+ wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
11327
+ for (const file of fs.readdirSync(splitTempDir)) {
11328
+ const srcPath = path.join(splitTempDir, file);
11329
+ const destPath = path.join(cwd, file);
11330
+ if (fs.existsSync(destPath)) {
11331
+ await promises.rm(destPath, { recursive: true, force: true });
11332
+ }
11333
+ await promises.cp(srcPath, destPath, { recursive: true, force: true });
11334
+ }
11335
+ wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
11336
+ verboseLog('[wasmtool] updating subpackage config...');
11337
+ updateSubpackageConfigSync(isArchive);
11338
+ verboseLog('[wasmtool] updating wasm split config...');
11339
+ wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11340
+ updateWasmSplitConfig({
11341
+ ENABLEWASMCOLLECT: true,
11342
+ ORIGINALWASMMD5: `${splitMeta.original_wasm_md5}`,
11343
+ WASMTABLESIZE: splitMeta.table_size,
11344
+ GLOBALVARLIST: JSON.stringify(splitMeta.global_var_list ?? []),
11345
+ SUBJSURL: '',
11346
+ IOS_CODE_FILE_MD5: `${splitMeta.main_wasm_h5_md5}`,
11347
+ ANDROID_CODE_FILE_MD5: `${splitMeta.main_wasm_md5}`,
11348
+ ANDROID_SUB_CODE_FILE_MD5: `${splitMeta.sub_wasm_md5}`,
11349
+ ARCHIVE_CODE_FILE_MD5: `${splitMeta.archive_md5 || ''}`,
11350
+ WASMSPLITVERSION: `${splitMeta.version}`,
11351
+ USINGWASMH5: Boolean(splitMeta.main_wasm_h5_md5),
11352
+ ENABLEWASMSPLIT: true,
11353
+ ENABLEARCHIVEMODE: isArchive,
11354
+ });
11355
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11356
+ return {
11357
+ data: { isSuccess: true },
11358
+ ctx: splitMeta,
11359
+ };
11360
+ }
11361
+ catch (err) {
11362
+ wsServer.sendUnitySplitStatus({
11363
+ status: 'wasm_split_failed',
11364
+ errorMsg: err instanceof Error ? err.message : String(err),
11365
+ });
11366
+ return {
11367
+ data: { isSuccess: false },
11368
+ error: { message: err instanceof Error ? err.message : String(err) },
11369
+ ctx: splitMeta,
11370
+ };
11371
+ }
11372
+ finally {
11373
+ await promises.rm(splitTempDir, { recursive: true, force: true });
11374
+ if (!isArchive) {
11375
+ await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
11376
+ recursive: true,
11377
+ force: true,
11378
+ });
11379
+ }
11380
+ }
11381
+ }
11382
+
11383
+ /*
11384
+ How it works:
11385
+ `this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
11386
+ */
11387
+
11388
+ class Node {
11389
+ value;
11390
+ next;
11391
+
11392
+ constructor(value) {
11393
+ this.value = value;
11394
+ }
11395
+ }
11396
+
11397
+ class Queue {
11398
+ #head;
11399
+ #tail;
11400
+ #size;
11401
+
11402
+ constructor() {
11403
+ this.clear();
11404
+ }
11405
+
10862
11406
  enqueue(value) {
10863
11407
  const node = new Node(value);
10864
11408
 
@@ -11002,75 +11546,28 @@ function pLimit(concurrency) {
11002
11546
  return generator;
11003
11547
  }
11004
11548
 
11005
- function updateSubpackageConfigSync() {
11006
- const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
11007
- const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
11008
- const gameJson = JSON.parse(raw);
11009
- /**
11010
- * wasm 分包完整流程完成后,删除一次性校验字段,
11011
- * 避免后续调试阶段继续触发 wasmFuncCount 的提示。
11012
- */
11013
- delete gameJson.wasmFuncCount;
11014
- const fieldName = SUBPACKAGE_FIELD_NAMES.find(k => k in gameJson) ??
11015
- SUBPACKAGE_FIELD_NAMES[0];
11016
- if (!gameJson[fieldName])
11017
- gameJson[fieldName] = [];
11018
- const subpackages = gameJson[fieldName];
11019
- // 删除老的 'wasmcode'
11020
- const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
11021
- /**
11022
- * 基于 SUBPACKAGE_CONFIG_FILE_NAME 更新 subpackages
11023
- */
11024
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
11025
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
11026
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
11027
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
11028
- // 合并去重:存在则更新 root,不存在则新增
11029
- const map = new Map(filtered.map(s => [s.name, s]));
11030
- gameJson[fieldName] = Array.from(map.values());
11031
- fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11032
- }
11033
-
11034
- async function downloadAndCompress(opts) {
11035
- const { startDownloadStatus, downloadDoneStatus, startCompressStatus, compressDoneStatus, url, out, enableCompress = false, } = opts;
11549
+ async function downloadOne(opts) {
11550
+ const { startStatus, doneStatus, url, out } = opts;
11036
11551
  if (!url)
11037
11552
  return;
11038
11553
  const willDownloadedFileIsBr = url.includes(BR_SUFFIX);
11039
- const wasmBrOutName = willDownloadedFileIsBr ? out + BR_SUFFIX : out;
11040
- // 下载
11041
- wsServer.sendUnitySplitStatus({ status: startDownloadStatus });
11042
- console.log(`download url: ${url}`);
11554
+ const finalOut = willDownloadedFileIsBr && !out.endsWith(BR_SUFFIX) ? out + BR_SUFFIX : out;
11555
+ wsServer.sendUnitySplitStatus({ status: startStatus });
11556
+ verboseLog(`[remote-split-download] fetching -> ${finalOut}`);
11043
11557
  const t0 = Date.now();
11044
- await withRetry(() => download(url, wasmBrOutName), DOWNLOAD_RETRY);
11045
- try {
11046
- const st = await promises.stat(wasmBrOutName);
11047
- if (!st.size)
11048
- throw new Error(`Empty download: ${wasmBrOutName}`);
11049
- wsServer.sendUnitySplitStatus({ status: downloadDoneStatus, url });
11050
- console.log(`download done: ${path.basename(wasmBrOutName)} size=${st.size}B time=${Date.now() - t0}ms`);
11051
- }
11052
- catch (e) {
11053
- await promises.rm(wasmBrOutName);
11054
- throw e;
11055
- }
11056
- if (enableCompress) {
11057
- console.log(`compress start: ${path.basename(out)}${BR_SUFFIX}`);
11058
- // 压缩
11059
- wsServer.sendUnitySplitStatus({ status: startCompressStatus });
11060
- const t1 = Date.now();
11061
- await compressWasmFile(out, wasmBrOutName);
11062
- wsServer.sendUnitySplitStatus({ status: compressDoneStatus });
11063
- console.log(`compress done: ${path.basename(wasmBrOutName)} time=${Date.now() - t1}ms`);
11064
- }
11065
- /**
11066
- * 在当前文件所在目录下写入一个空的 game.js
11067
- */
11068
- fs__namespace.writeFileSync(path.join(path.dirname(out), 'game.js'), '', {
11069
- encoding: 'utf-8',
11070
- });
11071
- }
11072
-
11073
- async function downloadSplited(context) {
11558
+ await withRetry(() => download(url, finalOut), DOWNLOAD_RETRY);
11559
+ const st = await promises.stat(finalOut);
11560
+ if (!st.size) {
11561
+ await promises.rm(finalOut, { force: true });
11562
+ throw new Error(`Empty download: ${finalOut}`);
11563
+ }
11564
+ verboseLog(`[remote-split-download] done: ${path.basename(finalOut)} size=${st.size}B time=${Date.now() - t0}ms`);
11565
+ wsServer.sendUnitySplitStatus({ status: doneStatus, url });
11566
+ // Legacy behaviour: write an empty game.js next to each downloaded artifact
11567
+ // so the subpackage loader doesn't complain about missing js entries.
11568
+ fs.writeFileSync(path.join(path.dirname(out), 'game.js'), '', 'utf-8');
11569
+ }
11570
+ async function downloadSplitedRemote(context) {
11074
11571
  const cwd = process.cwd();
11075
11572
  const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11076
11573
  ensureDirSync(splitTempDir);
@@ -11079,138 +11576,385 @@ async function downloadSplited(context) {
11079
11576
  const mainIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
11080
11577
  const subIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
11081
11578
  [mainAndroidDir, subAndroidDir, mainIosDir, subIosDir].forEach(ensureDirSync);
11082
- const mainAndroidWasmCodeTempPath = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11083
- const subAndroidWasmCodeTempPath = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11084
- const mainIosWasmCodeTempPath = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
11579
+ const mainAndroidOut = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11580
+ const subAndroidOut = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11581
+ const mainIosOut = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
11085
11582
  const limit = pLimit(CONCURRENCY_LIMIT);
11086
11583
  try {
11087
- console.log('downloadWasmSplit', context);
11088
- // 原有状态文案,按你之前的写法
11089
- wsServer.sendUnitySplitStatus({
11090
- status: 'start_download_android_main_wasm',
11584
+ verboseLog('[remote-split-download] start', {
11585
+ original_wasm_md5: context.original_wasm_md5,
11586
+ main_wasm_md5: context.main_wasm_md5,
11587
+ sub_wasm_md5: context.sub_wasm_md5,
11588
+ main_wasm_h5_md5: context.main_wasm_h5_md5,
11091
11589
  });
11092
- wsServer.sendUnitySplitStatus({
11093
- status: 'start_download_android_sub_wasm_code',
11094
- });
11095
- wsServer.sendUnitySplitStatus({ status: 'start_download_ios_main_wasm' });
11096
- /**
11097
- * 需要做个保护,只有 有 URL 时才下载
11098
- */
11099
- // 并发下载 + 压缩(带重试)
11100
11590
  await Promise.all([
11101
- limit(() => downloadAndCompress({
11102
- startDownloadStatus: 'start_download_android_main_wasm',
11103
- downloadDoneStatus: 'download_android_main_wasm_done',
11104
- startCompressStatus: 'start_compress_android_main_wasm',
11105
- compressDoneStatus: 'compress_android_main_wasm_done',
11591
+ limit(() => downloadOne({
11592
+ startStatus: 'start_download_android_main_wasm',
11593
+ doneStatus: 'download_android_main_wasm_done',
11106
11594
  url: context.main_wasm_download_url,
11107
- out: mainAndroidWasmCodeTempPath,
11595
+ out: mainAndroidOut,
11108
11596
  })),
11109
- limit(() => downloadAndCompress({
11110
- startDownloadStatus: 'start_download_android_sub_wasm_code',
11111
- downloadDoneStatus: 'download_android_sub_wasm_code_done',
11112
- startCompressStatus: 'start_compress_android_sub_wasm_code',
11113
- compressDoneStatus: 'compress_android_sub_wasm_code_done',
11597
+ limit(() => downloadOne({
11598
+ startStatus: 'start_download_android_sub_wasm_code',
11599
+ doneStatus: 'download_android_sub_wasm_code_done',
11114
11600
  url: context.sub_wasm_download_url,
11115
- out: subAndroidWasmCodeTempPath,
11601
+ out: subAndroidOut,
11116
11602
  })),
11117
- limit(() => downloadAndCompress({
11118
- startDownloadStatus: 'start_download_ios_main_wasm',
11119
- downloadDoneStatus: 'download_ios_main_wasm_done',
11120
- startCompressStatus: 'start_compress_ios_main_wasm',
11121
- compressDoneStatus: 'compress_ios_main_wasm_done',
11603
+ limit(() => downloadOne({
11604
+ startStatus: 'start_download_ios_main_wasm',
11605
+ doneStatus: 'download_ios_main_wasm_done',
11122
11606
  url: context.main_wasm_h5_download_url,
11123
- out: mainIosWasmCodeTempPath,
11607
+ out: mainIosOut,
11124
11608
  })),
11125
- // 下载 ios sub js range json
11126
- limit(() => downloadAndCompress({
11127
- startDownloadStatus: 'start_download_ios_range_json',
11128
- downloadDoneStatus: 'download_ios_range_json_done',
11609
+ limit(() => downloadOne({
11610
+ startStatus: 'start_download_ios_range_json',
11611
+ doneStatus: 'download_ios_range_json_done',
11129
11612
  url: context.sub_js_range_download_url,
11130
11613
  out: path.join(subIosDir, 'func_bytes_range.json'),
11131
11614
  })),
11132
- // 下载 ios sub js data br
11133
- limit(() => downloadAndCompress({
11134
- startDownloadStatus: 'start_download_ios_js_data_br',
11135
- downloadDoneStatus: 'download_ios_js_data_br_done',
11615
+ limit(() => downloadOne({
11616
+ startStatus: 'start_download_ios_js_data_br',
11617
+ doneStatus: 'download_ios_js_data_br_done',
11136
11618
  url: context.sub_js_data_download_url,
11137
11619
  out: path.join(subIosDir, 'subjs.data'),
11138
11620
  })),
11139
11621
  ]);
11140
- // 复制 split/* 到项目根目录(递归、覆盖)——避免 EISDIR
11141
- console.log('copy splitTempDir to root start');
11622
+ verboseLog('[remote-split-download] copying split output to project root...');
11142
11623
  wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
11143
11624
  for (const file of fs.readdirSync(splitTempDir)) {
11144
11625
  const srcPath = path.join(splitTempDir, file);
11145
11626
  const destPath = path.join(cwd, file);
11146
- // 如果目标路径有文件或目录,先删除
11147
11627
  if (fs.existsSync(destPath)) {
11148
11628
  await promises.rm(destPath, { recursive: true, force: true });
11149
11629
  }
11150
11630
  await promises.cp(srcPath, destPath, { recursive: true, force: true });
11151
11631
  }
11152
11632
  wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
11153
- console.log('copy splitTempDir to root end');
11154
- // 更新分包配置(幂等)
11155
- console.log('updateSubpackageConfigSync start');
11156
- updateSubpackageConfigSync();
11157
- console.log('updateSubpackageConfigSync end');
11158
- // 更新 wasm split 配置(保持原始状态文案)
11159
- console.log('updateWasmSplitConfig start');
11633
+ verboseLog('[remote-split-download] updating subpackage config...');
11634
+ updateSubpackageConfigSync(false);
11635
+ verboseLog('[remote-split-download] updating webgl-wasm-split.js...');
11160
11636
  wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11161
11637
  updateWasmSplitConfig({
11162
11638
  ENABLEWASMCOLLECT: true,
11163
- ORIGINALWASMMD5: `${context.original_wasm_md5}`,
11639
+ ENABLEWASMSPLIT: true,
11640
+ ENABLEARCHIVEMODE: false,
11641
+ ORIGINALWASMMD5: `${context.original_wasm_md5 ?? ''}`,
11164
11642
  WASMTABLESIZE: context.table_size,
11165
11643
  GLOBALVARLIST: JSON.stringify(context.global_var_list ?? []),
11166
- SUBJSURL: `${context.sub_js_download_url}`,
11167
- IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5}`,
11168
- ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5}`,
11169
- ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5}`,
11170
- WASMSPLITVERSION: `${context.version}`,
11644
+ SUBJSURL: `${context.sub_js_download_url ?? ''}`,
11645
+ IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5 ?? ''}`,
11646
+ ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5 ?? ''}`,
11647
+ ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5 ?? ''}`,
11648
+ WASMSPLITVERSION: `${context.version ?? ''}`,
11171
11649
  USINGWASMH5: Boolean(context.main_wasm_h5_md5),
11172
- ENABLEWASMSPLIT: true,
11173
- // IOS_SUB_JS_FILE_CONFIG: JSON.stringify(context.merged_js ?? {}),
11174
11650
  });
11175
11651
  wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11176
- console.log('updateWasmSplitConfig end');
11177
- return {
11178
- data: {
11179
- isSuccess: true,
11180
- },
11181
- ctx: context,
11182
- };
11652
+ verboseLog('[remote-split-download] all done');
11653
+ return { data: { isSuccess: true }, ctx: context };
11183
11654
  }
11184
11655
  catch (err) {
11185
- wsServer.sendUnitySplitStatus({
11186
- status: 'wasm_split_failed',
11187
- errorMsg: err instanceof Error ? err.message : String(err),
11188
- });
11656
+ const message = err instanceof Error ? err.message : String(err);
11657
+ verboseLog('[remote-split-download] failed:', message);
11658
+ wsServer.sendUnitySplitStatus({ status: 'wasm_split_failed', errorMsg: message });
11189
11659
  return {
11190
- data: {
11191
- isSuccess: false,
11192
- },
11193
- error: {
11194
- message: err instanceof Error ? err.message : String(err),
11195
- },
11660
+ data: { isSuccess: false },
11661
+ error: { message },
11196
11662
  ctx: context,
11197
11663
  };
11198
11664
  }
11199
11665
  finally {
11200
- // 清理临时目录与旧 wasmcode 目录
11201
- console.log('delete splitTempDir start');
11202
11666
  await promises.rm(splitTempDir, { recursive: true, force: true });
11203
- console.log('delete splitTempDir end');
11204
- console.log('delete wasmcode start');
11667
+ // Legacy flow: the server-produced `wasmcode/` placeholder at the project
11668
+ // root is no longer needed once we've laid down the 4 platform dirs.
11205
11669
  await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
11206
11670
  recursive: true,
11207
11671
  force: true,
11208
11672
  });
11209
- console.log('delete wasmcode end');
11210
11673
  }
11211
11674
  }
11212
-
11213
- async function getSplitResult({ client_key, wasm_md5, wasm_path, }) {
11675
+
11676
+ async function getSplitResult$1(_params) {
11677
+ const { splitMeta } = getLocalState();
11678
+ if (!splitMeta) {
11679
+ return {
11680
+ data: null,
11681
+ error: {
11682
+ code: 404,
11683
+ message: 'No local split result found. Run split first.',
11684
+ },
11685
+ ctx: { logid: 'local', httpStatusCode: 404 },
11686
+ };
11687
+ }
11688
+ return {
11689
+ data: {
11690
+ code: 0,
11691
+ message: 'success',
11692
+ result: splitMeta,
11693
+ },
11694
+ error: null,
11695
+ ctx: { logid: 'local', httpStatusCode: 200 },
11696
+ };
11697
+ }
11698
+
11699
+ /**
11700
+ * Local-only undo of a prepare run. Routes through the shared
11701
+ * `restoreFromCache` helper so this stays in sync with `resetWasmSplit`
11702
+ * and never falls behind when new split-output dirs are added.
11703
+ *
11704
+ * `wasmCodePath` is kept on the signature for backward-compat with the
11705
+ * existing /game/wasm-cancel route shape, but it's no longer needed —
11706
+ * `restoreFromCache` already resolves the wasm path from the cache
11707
+ * directory contents (whatever `keepCacheSync` recorded as the original).
11708
+ */
11709
+ function cancelSplit(_params) {
11710
+ restoreFromCache();
11711
+ // Drop the prepared-meta anchor for the same reason resetWasmSplit
11712
+ // does: cancel rolled the wasm back to its un-instrumented state, so
11713
+ // the recorded "preparedWasmMd5" is no longer accurate. Leaving it
11714
+ // would trip the split-config drift guard on the very next Modal open.
11715
+ const preparedMetaPath = path__namespace.join(process.cwd(), TTMG_TEMP_DIR, 'prepared-meta.json');
11716
+ if (fs__namespace.existsSync(preparedMetaPath)) {
11717
+ fs__namespace.rmSync(preparedMetaPath, { force: true });
11718
+ }
11719
+ }
11720
+
11721
+ async function resetWasmSplit$1(data) {
11722
+ const res = await request({
11723
+ url: `${WASM_COLLECT_BASE_URL}/reset`,
11724
+ method: 'POST',
11725
+ data: {
11726
+ app_id: data.clientkey,
11727
+ wasm_md5: data.wasmMd5,
11728
+ },
11729
+ });
11730
+ // Reuse the shared restore helper so local + remote rollback cleans
11731
+ // exactly the same set of files. Previously this function had its own
11732
+ // inline copy that only removed wasmcode-android / wasmcode1-android /
11733
+ // wasmcode-ios — missing wasmcode1 / wasmcode-archive (archive mode)
11734
+ // and wasmcode1-ios (legacy iOS sub), which left stale split outputs
11735
+ // on disk and caused them to be re-uploaded on the next build.
11736
+ restoreFromCache();
11737
+ // Drop the prepared-meta anchor as well — rollback restores the
11738
+ // original wasm into the project, so any md5 we previously recorded
11739
+ // for the prepared build is no longer valid. Leaving it behind would
11740
+ // make the split-config drift guard fire on the very next Modal open
11741
+ // and force the user through a redundant prepare cycle.
11742
+ const preparedMetaPath = path.join(process.cwd(), TTMG_TEMP_DIR, 'prepared-meta.json');
11743
+ if (fs.existsSync(preparedMetaPath)) {
11744
+ fs.rmSync(preparedMetaPath, { force: true });
11745
+ }
11746
+ return res;
11747
+ }
11748
+
11749
+ function getSplitConfig() {
11750
+ const configFilePath = path__namespace.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
11751
+ try {
11752
+ // 1. 检查文件是否存在
11753
+ if (!fs__namespace.existsSync(configFilePath)) {
11754
+ console.error(`Config file not found at: ${configFilePath}`);
11755
+ return null;
11756
+ }
11757
+ // 2. 同步读取文件内容为字符串
11758
+ const fileContent = fs__namespace.readFileSync(configFilePath, 'utf-8');
11759
+ // 3. 构造一个函数来执行并返回 module.exports 的内容
11760
+ // 这是一种比直接用 eval() 更安全的方式,因为它限制了代码的执行作用域
11761
+ const evaluateModule = new Function('module', `${fileContent}; return module.exports;`);
11762
+ // 准备一个临时的 module 对象
11763
+ const tempModule = { exports: {} };
11764
+ // 4. 执行函数,并将配置赋值给 config 变量
11765
+ const config = evaluateModule(tempModule);
11766
+ // 5. 检查是否成功获取了配置
11767
+ if (typeof config === 'object' && config !== null) {
11768
+ return config;
11769
+ }
11770
+ else {
11771
+ console.error('Failed to extract a valid config object from the file.');
11772
+ return null;
11773
+ }
11774
+ }
11775
+ catch (error) {
11776
+ console.error('Error reading or evaluating split config file:', error);
11777
+ return null;
11778
+ }
11779
+ }
11780
+
11781
+ var WasmStatus;
11782
+ (function (WasmStatus) {
11783
+ WasmStatus[WasmStatus["IdleStatus"] = 0] = "IdleStatus";
11784
+ WasmStatus[WasmStatus["WasmPreparingStatus"] = 1] = "WasmPreparingStatus";
11785
+ WasmStatus[WasmStatus["WasmPreparedStatus"] = 2] = "WasmPreparedStatus";
11786
+ WasmStatus[WasmStatus["WasmSplitStatus"] = 3] = "WasmSplitStatus";
11787
+ WasmStatus[WasmStatus["WasmSplittingStatus"] = 4] = "WasmSplittingStatus";
11788
+ WasmStatus[WasmStatus["WasmSplitDoneStatus"] = 5] = "WasmSplitDoneStatus";
11789
+ WasmStatus[WasmStatus["WasmSplitReadyToPrepareStatus"] = 6] = "WasmSplitReadyToPrepareStatus";
11790
+ WasmStatus[WasmStatus["WasmSplitPreparingStatus"] = 7] = "WasmSplitPreparingStatus";
11791
+ WasmStatus[WasmStatus["WasmSplitPreparedStatus"] = 8] = "WasmSplitPreparedStatus";
11792
+ WasmStatus[WasmStatus["WasmCollectingStatus"] = 9] = "WasmCollectingStatus";
11793
+ WasmStatus[WasmStatus["WasmUploadFailStatus"] = -1] = "WasmUploadFailStatus";
11794
+ WasmStatus[WasmStatus["WasmDownloadFailStatus"] = -2] = "WasmDownloadFailStatus";
11795
+ WasmStatus[WasmStatus["WasmFileNotExistStatus"] = -3] = "WasmFileNotExistStatus";
11796
+ WasmStatus[WasmStatus["WasmSplitFailStatus"] = -4] = "WasmSplitFailStatus";
11797
+ WasmStatus[WasmStatus["WasmSplitUpdateDBFailedStatus"] = -5] = "WasmSplitUpdateDBFailedStatus";
11798
+ WasmStatus[WasmStatus["WasmSplitPrepareFailedStatus"] = -6] = "WasmSplitPrepareFailedStatus";
11799
+ })(WasmStatus || (WasmStatus = {}));
11800
+
11801
+ const getTaskStatus$1 = async (params) => {
11802
+ const { preparedWasmPath, splitMeta } = getLocalState();
11803
+ let status = WasmStatus.IdleStatus;
11804
+ if (splitMeta) {
11805
+ status = WasmStatus.WasmSplitDoneStatus;
11806
+ }
11807
+ else if (preparedWasmPath) {
11808
+ status = WasmStatus.WasmSplitPreparedStatus;
11809
+ }
11810
+ return {
11811
+ data: {
11812
+ code: 0,
11813
+ message: 'success',
11814
+ result: {
11815
+ status,
11816
+ wasm_md5: params.wasm_md5,
11817
+ },
11818
+ },
11819
+ error: null,
11820
+ ctx: { logid: 'local', httpStatusCode: 200 },
11821
+ };
11822
+ };
11823
+
11824
+ const getTaskInfo$1 = async (params) => {
11825
+ const res = await request({
11826
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
11827
+ method: 'GET',
11828
+ params: {
11829
+ app_id: params.client_key,
11830
+ wasm_md5: params.wasm_md5,
11831
+ },
11832
+ });
11833
+ const { totalWasmFuncCount, preparedWasmPath, wasmSize } = getLocalState();
11834
+ // Prefer game.json as the source of truth so wasm_size / total_wasm_func_count
11835
+ // survive CLI restarts. localState values are only populated during the
11836
+ // prepare step of the current session; after a restart they default to 0.
11837
+ // game.json carries wasmCodeSize/wasmFuncCount emitted at build time, so
11838
+ // re-entering the collect step still shows the correct totals.
11839
+ const gameJson = getGameJson();
11840
+ const gameJsonWasmSize = Number(gameJson?.wasmCodeSize) || 0;
11841
+ const gameJsonFuncCount = Number(gameJson?.wasmFuncCount) || 0;
11842
+ return {
11843
+ data: {
11844
+ code: res?.data?.code ?? 0,
11845
+ message: 'success',
11846
+ result: {
11847
+ app_id: params.client_key,
11848
+ wasm_md5: params.wasm_md5,
11849
+ is_prepared: Boolean(preparedWasmPath),
11850
+ collected_func_count: res?.data?.func_count ?? 0,
11851
+ total_wasm_func_count: gameJsonFuncCount || totalWasmFuncCount || 0,
11852
+ wasm_size: gameJsonWasmSize || wasmSize || 0,
11853
+ },
11854
+ },
11855
+ error: res.error,
11856
+ ctx: res.ctx,
11857
+ };
11858
+ };
11859
+
11860
+ async function startPrepareRemote(params) {
11861
+ // Back up the original wasm + split config on the first run so cancel/rollback
11862
+ // works even if the user aborts before the server-side prepare finishes.
11863
+ keepCacheSync({
11864
+ entryDir: process.cwd(),
11865
+ originalWasmPath: params.wasm_file_path,
11866
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
11867
+ });
11868
+ const form = new FormData$1();
11869
+ form.append('desc', params.desc);
11870
+ form.append('wasm_md5', params.wasm_md5);
11871
+ form.append('with_ios', 'true');
11872
+ form.append('wasm_file', fs$1.createReadStream(path$1.join(process.cwd(), params.wasm_file_path)), { filename: path$1.basename(params.wasm_file_path), contentType: 'application/wasm' });
11873
+ let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
11874
+ if (!fs$1.existsSync(symbolFilePath)) {
11875
+ symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
11876
+ }
11877
+ if (!fs$1.existsSync(symbolFilePath)) {
11878
+ return {
11879
+ error: { code: 400, message: `${WASM_SYMBOL_FILE_NAME} not found`, client_key: params.client_key },
11880
+ data: null,
11881
+ ctx: { logid: '', httpStatusCode: 400 },
11882
+ };
11883
+ }
11884
+ form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
11885
+ filename: WASM_SYMBOL_FILE_NAME,
11886
+ contentType: 'application/octet-stream',
11887
+ });
11888
+ const formHeaders = form.getHeaders();
11889
+ return request({
11890
+ url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
11891
+ method: 'POST',
11892
+ headers: { ...DEV_HEADERS, ...formHeaders },
11893
+ params: { client_key: params.client_key, with_ios: true },
11894
+ data: form,
11895
+ });
11896
+ }
11897
+ async function setCollectRemote({ client_key, wasm_md5 }) {
11898
+ return request({
11899
+ url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
11900
+ method: 'POST',
11901
+ data: { client_key, wasm_md5 },
11902
+ headers: DEV_HEADERS,
11903
+ });
11904
+ }
11905
+ async function getCollectedFuncIdsRemote({ client_key, wasm_md5 }) {
11906
+ return request({
11907
+ url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
11908
+ method: 'GET',
11909
+ headers: DEV_HEADERS,
11910
+ params: { client_key, wasm_md5 },
11911
+ });
11912
+ }
11913
+ async function getCollecttingInfoRemote({ client_key, wasm_md5 }) {
11914
+ return request({
11915
+ url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
11916
+ method: 'GET',
11917
+ headers: DEV_HEADERS,
11918
+ params: { client_key, wasm_md5 },
11919
+ });
11920
+ }
11921
+ async function startSplitRemote({ client_key, wasm_md5 }) {
11922
+ return request({
11923
+ url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
11924
+ method: 'POST',
11925
+ headers: { ...DEV_HEADERS },
11926
+ data: { client_key, wasm_md5 },
11927
+ });
11928
+ }
11929
+ async function getTaskInfoRemote(params) {
11930
+ return request({
11931
+ url: `${BASE_URL}/api/stark_wasm/v4/get/taskinfo`,
11932
+ method: 'GET',
11933
+ headers: DEV_HEADERS,
11934
+ params,
11935
+ });
11936
+ }
11937
+ async function getTaskStatusRemote(params) {
11938
+ return request({
11939
+ url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
11940
+ method: 'GET',
11941
+ headers: DEV_HEADERS,
11942
+ params,
11943
+ });
11944
+ }
11945
+ async function resetWasmSplitRemote(data) {
11946
+ const res = await request({
11947
+ url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
11948
+ method: 'POST',
11949
+ headers: { ...DEV_HEADERS },
11950
+ data: { client_key: data.clientkey, wasm_md5: data.wasmMd5 },
11951
+ });
11952
+ // Restore project files (wasm / webgl-wasm-split.js / game.json) so the
11953
+ // next prepare starts from the original placeholders.
11954
+ restoreFromCache();
11955
+ return res;
11956
+ }
11957
+ async function getSplitResultRemote({ client_key, wasm_md5, wasm_path }) {
11214
11958
  return request({
11215
11959
  url: `${BASE_URL}/api/stark_wasm/v4/post/download`,
11216
11960
  method: 'POST',
@@ -11218,162 +11962,121 @@ async function getSplitResult({ client_key, wasm_md5, wasm_path, }) {
11218
11962
  data: { client_key, wasm_md5, wasm_path },
11219
11963
  });
11220
11964
  }
11221
-
11222
- function cancelSplit(params) {
11223
- /**
11224
- * 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
11225
- */
11226
- const { wasmCodePath } = params;
11227
- const cacheDir = path__namespace.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
11228
- /**
11229
- * 恢复 br 文件
11230
- */
11231
- if (fs__namespace.existsSync(cacheDir)) {
11232
- /**
11233
- * 判断是否有缓存的 br 文件
11234
- */
11235
- const targetWasmBrPath = path__namespace.join(cacheDir, path__namespace.basename(wasmCodePath));
11236
- if (fs__namespace.existsSync(targetWasmBrPath)) {
11237
- const destWasmBrPath = path__namespace.join(process.cwd(), wasmCodePath);
11238
- // 规避没有文件夹的情况
11239
- ensureDirSync(path__namespace.dirname(destWasmBrPath));
11240
- fs__namespace.copyFileSync(targetWasmBrPath, destWasmBrPath);
11241
- }
11242
- }
11243
- /**
11244
- * 恢复 webgl-wasm-split.js 文件
11245
- */
11246
- const splitConfigCachePath = path__namespace.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
11247
- if (fs__namespace.existsSync(splitConfigCachePath)) {
11248
- fs__namespace.copyFileSync(splitConfigCachePath, path__namespace.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
11249
- }
11250
- /**
11251
- * 恢复 game.json 文件
11252
- */
11253
- const gameJsonCachePath = path__namespace.join(cacheDir, 'game.json');
11254
- if (fs__namespace.existsSync(gameJsonCachePath)) {
11255
- fs__namespace.copyFileSync(gameJsonCachePath, path__namespace.join(process.cwd(), 'game.json'));
11256
- }
11257
- }
11258
-
11259
- async function resetWasmSplit(data) {
11965
+ /**
11966
+ * Remote pipeline: after the server finishes preparing (instrumenting) the wasm,
11967
+ * fetch the download URL, download the prepared wasm, replace the project file,
11968
+ * and update webgl-wasm-split.js for the LEGACY reporting flow.
11969
+ */
11970
+ async function downloadPreparedRemote(data) {
11971
+ wsServer.sendUnitySplitStatus({ status: 'star_fetch_prepared_wasm_url' });
11260
11972
  const res = await request({
11261
- url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
11973
+ url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
11262
11974
  method: 'POST',
11263
- headers: {
11264
- ...DEV_HEADERS,
11265
- },
11266
- data: {
11267
- client_key: data.clientkey,
11268
- wasm_md5: data.wasmMd5,
11269
- },
11975
+ headers: DEV_HEADERS,
11976
+ data,
11270
11977
  });
11271
- /**
11272
- * 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
11273
- */
11274
- const cacheDir = path.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
11275
- /**
11276
- * 恢复 br 文件
11277
- */
11278
- if (fs.existsSync(cacheDir)) {
11279
- /**
11280
- * 判断是否有缓存的 br 文件
11281
- */
11282
- /**
11283
- * 判断 cache 文件夹下有没有 .br 文件
11284
- *
11285
- */
11286
- const targetWasmBrPath = fs
11287
- .readdirSync(cacheDir)
11288
- .find(item => item.endsWith('.br'));
11289
- if (targetWasmBrPath) {
11290
- const destWasmBrPath = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
11291
- // 规避没有文件夹的情况
11292
- ensureDirSync(path.dirname(destWasmBrPath));
11293
- fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
11294
- }
11295
- }
11296
- /**
11297
- * 恢复 webgl-wasm-split.js 文件
11298
- */
11299
- const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
11300
- if (fs.existsSync(splitConfigCachePath)) {
11301
- fs.copyFileSync(splitConfigCachePath, path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
11302
- }
11303
- /**
11304
- * 恢复 game.json 文件
11305
- */
11306
- const gameJsonCachePath = path.join(cacheDir, 'game.json');
11307
- if (fs.existsSync(gameJsonCachePath)) {
11308
- fs.copyFileSync(gameJsonCachePath, path.join(process.cwd(), 'game.json'));
11309
- }
11310
- /**
11311
- * 删除历史分包产物
11312
- */
11313
- const androidSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
11314
- if (fs.existsSync(androidSubpackageDir)) {
11315
- fs.rmSync(androidSubpackageDir, { recursive: true });
11316
- }
11317
- const androidSubpackageSubDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
11318
- if (fs.existsSync(androidSubpackageSubDir)) {
11319
- fs.rmSync(androidSubpackageSubDir, { recursive: true });
11320
- }
11321
- const iosSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.ios.root);
11322
- if (fs.existsSync(iosSubpackageDir)) {
11323
- fs.rmSync(iosSubpackageDir, { recursive: true });
11324
- }
11325
- return res;
11326
- }
11327
-
11328
- function getSplitConfig() {
11329
- const configFilePath = path__namespace.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
11978
+ wsServer.sendUnitySplitStatus({ status: 'fetch_prepared_wasm_url_done' });
11330
11979
  try {
11331
- // 1. 检查文件是否存在
11332
- if (!fs__namespace.existsSync(configFilePath)) {
11333
- console.error(`Config file not found at: ${configFilePath}`);
11334
- return null;
11980
+ const downloadUrl = res?.data?.result?.download_url;
11981
+ if (!downloadUrl) {
11982
+ verboseLog('[remote-download-prepared] no download_url in response');
11983
+ return {
11984
+ isSuccess: false,
11985
+ error: { code: res.data?.code, message: res.data?.message || 'No download_url returned' },
11986
+ ctx: res?.ctx,
11987
+ };
11335
11988
  }
11336
- // 2. 同步读取文件内容为字符串
11337
- const fileContent = fs__namespace.readFileSync(configFilePath, 'utf-8');
11338
- // 3. 构造一个函数来执行并返回 module.exports 的内容
11339
- // 这是一种比直接用 eval() 更安全的方式,因为它限制了代码的执行作用域
11340
- const evaluateModule = new Function('module', `${fileContent}; return module.exports;`);
11341
- // 准备一个临时的 module 对象
11342
- const tempModule = { exports: {} };
11343
- // 4. 执行函数,并将配置赋值给 config 变量
11344
- const config = evaluateModule(tempModule);
11345
- // 5. 检查是否成功获取了配置
11346
- if (typeof config === 'object' && config !== null) {
11347
- return config;
11989
+ const willReplaceWasmPath = path$1.join(process.cwd(), data.wasm_path);
11990
+ const { cacheDir } = keepCacheSync({
11991
+ entryDir: process.cwd(),
11992
+ originalWasmPath: data.wasm_path,
11993
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
11994
+ });
11995
+ verboseLog(`[remote-download-prepared] target=${willReplaceWasmPath}`);
11996
+ if (downloadUrl.includes('.br')) {
11997
+ const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm.br');
11998
+ verboseLog('[remote-download-prepared] downloading (br) ->', tempWasmPath);
11999
+ wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
12000
+ const startedAt = Date.now();
12001
+ await download(downloadUrl, tempWasmPath);
12002
+ verboseLog(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
12003
+ fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
12004
+ wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
11348
12005
  }
11349
12006
  else {
11350
- console.error('Failed to extract a valid config object from the file.');
11351
- return null;
12007
+ const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm');
12008
+ verboseLog('[remote-download-prepared] downloading (raw) ->', tempWasmPath);
12009
+ wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
12010
+ const startedAt = Date.now();
12011
+ await download(downloadUrl, tempWasmPath);
12012
+ verboseLog(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
12013
+ wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
12014
+ wsServer.sendUnitySplitStatus({ status: 'start_compress_prepared_wasm' });
12015
+ await compressWasmFile(tempWasmPath, willReplaceWasmPath);
12016
+ verboseLog('[remote-download-prepared] compressed and written to project');
12017
+ wsServer.sendUnitySplitStatus({ status: 'compress_prepared_wasm_done', url: downloadUrl });
12018
+ wsServer.sendUnitySplitStatus({ status: 'write_compress_prepared_wasm_done' });
11352
12019
  }
12020
+ wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
12021
+ // Remote (legacy) pipeline: enable collect but disable archive mode so the
12022
+ // plugin reports to the legacy stark_wasm/v4 collect API.
12023
+ // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
12024
+ // sends the correct wasm_md5 in every collect report.
12025
+ restoreSplitConfigFromCache();
12026
+ updateWasmSplitConfig({
12027
+ ENABLEWASMCOLLECT: true,
12028
+ ENABLEARCHIVEMODE: false,
12029
+ ORIGINALWASMMD5: res?.data?.result?.original_wasm_md5 ||
12030
+ res?.data?.result?.md5 ||
12031
+ data.wasm_md5,
12032
+ });
12033
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
12034
+ verboseLog('[remote-download-prepared] split config updated, returning success');
12035
+ return { isSuccess: true, ctx: res?.ctx };
11353
12036
  }
11354
12037
  catch (error) {
11355
- console.error('Error reading or evaluating split config file:', error);
11356
- return null;
12038
+ verboseLog('[remote-download-prepared] error:', error);
12039
+ return {
12040
+ isSuccess: false,
12041
+ error: { code: res.data?.code, message: error instanceof Error ? error.message : String(error) },
12042
+ ctx: res?.ctx,
12043
+ };
11357
12044
  }
11358
12045
  }
11359
12046
 
11360
- const getTaskStatus = (params) => {
11361
- return request({
11362
- url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
11363
- method: 'GET',
11364
- headers: DEV_HEADERS,
11365
- params,
11366
- });
11367
- };
11368
-
11369
- const getTaskInfo = async (params) => {
11370
- return request({
11371
- url: `${BASE_URL}/api/stark_wasm/v4/get/taskinfo`,
11372
- method: 'GET',
11373
- headers: DEV_HEADERS,
11374
- params,
12047
+ function isLocal() {
12048
+ return getLocalState().pipelineMode === 'local';
12049
+ }
12050
+ const startPrepare = (params) => isLocal() ? startPrepare$1(params) : startPrepareRemote(params);
12051
+ const downloadPrepared = (params) => isLocal() ? downloadPrepared$1() : downloadPreparedRemote(params);
12052
+ // 开 collect session(鉴权门)。IDE 经 `/game/wasm-collect-start` 调用,
12053
+ // 所以它在浏览器 Network 里是一条独立可见、明确叫 start 的请求。
12054
+ // - 本地 → `/start`
12055
+ // - 远程 → 老的 `set_collecting`(远程没有独立 /start,set_collecting 即开窗)
12056
+ const openCollectSession = (params) => isLocal() ? openCollectSession$1(params) : setCollectRemote(params);
12057
+ // 上传符号表。本地走 `/symbols`(非阻塞、失败不致命);远程在 prepare 阶段
12058
+ // 已随表单上传过 symbol 文件,这里是 no-op,直接返回成功保持两条 pipeline
12059
+ // 在 IDE 层对称。
12060
+ const uploadCollectSymbols = (params) => isLocal()
12061
+ ? uploadCollectSymbols$1(params)
12062
+ : Promise.resolve({
12063
+ data: { code: 0, message: 'success', result: {} },
12064
+ error: null,
12065
+ ctx: { logid: 'remote-noop', httpStatusCode: 200 },
11375
12066
  });
11376
- };
12067
+ const getCollectedFuncIds = (params) => isLocal() ? getCollectedFuncIds$1(params) : getCollectedFuncIdsRemote(params);
12068
+ const getCollecttingInfo = (params) => isLocal() ? getCollecttingInfo$1(params) : getCollecttingInfoRemote(params);
12069
+ const startSplit = (params) => isLocal() ? startSplit$1(params) : startSplitRemote(params);
12070
+ const downloadSplited = (context) => isLocal() ? downloadSplited$1(context) : downloadSplitedRemote(context);
12071
+ const getSplitResult = (params) => isLocal() ? getSplitResult$1() : getSplitResultRemote(params);
12072
+ const getTaskInfo = (params) => isLocal() ? getTaskInfo$1(params) : getTaskInfoRemote(params);
12073
+ const getTaskStatus = (params) => isLocal() ? getTaskStatus$1(params) : getTaskStatusRemote(params);
12074
+ const resetWasmSplit = (data) => isLocal() ? resetWasmSplit$1(data) : resetWasmSplitRemote(data);
12075
+ // Collect session (`/start` / `/finish`) is an implementation detail of the
12076
+ // local `wasm-collect/v1` pipeline — it's invoked inside `setCollectLocal`
12077
+ // and `startSplitLocal` respectively. The remote `stark_wasm/v4` pipeline
12078
+ // has no session concept. IDE never calls these directly, so there is no
12079
+ // dispatcher exposed here.
11377
12080
 
11378
12081
  const gameWasmCancelRoute = {
11379
12082
  method: 'post',
@@ -11381,9 +12084,7 @@ const gameWasmCancelRoute = {
11381
12084
  handler: async (req, res) => {
11382
12085
  const { codePath } = req.body;
11383
12086
  console.log('wasm-cancel', req.body);
11384
- await cancelSplit({
11385
- wasmCodePath: codePath,
11386
- });
12087
+ await cancelSplit();
11387
12088
  res.send({
11388
12089
  code: successCode,
11389
12090
  msg: 'cancel success',
@@ -11477,26 +12178,35 @@ const gameWasmPrepareResultRoute = {
11477
12178
  method: 'post',
11478
12179
  path: '/game/wasm-prepare-result',
11479
12180
  handler: async (req, res) => {
11480
- console.log('wasm-prepare-result-request', req.body);
11481
- const { clientKey, codeMd5 } = req.body;
11482
- const response = await getTaskStatus({
11483
- client_key: clientKey,
11484
- wasm_md5: codeMd5,
11485
- });
11486
- if (response.error) {
11487
- res.send({
11488
- code: errorCode,
11489
- error: response.error,
11490
- ctx: response.ctx,
11491
- });
11492
- }
11493
- else {
12181
+ const { pipelineMode } = getLocalState();
12182
+ if (pipelineMode === 'local') {
11494
12183
  res.send({
11495
12184
  code: successCode,
11496
- data: response.data?.result || {},
11497
- ctx: response.ctx,
12185
+ data: { status: WasmStatus.WasmSplitPreparedStatus },
12186
+ ctx: { logid: 'local' },
11498
12187
  });
12188
+ return;
12189
+ }
12190
+ const { codeMd5, clientKey } = req.body;
12191
+ const result = await getTaskStatus({
12192
+ client_key: clientKey,
12193
+ wasm_md5: codeMd5,
12194
+ });
12195
+ // For the remote pipeline, forward the full `result` payload so the IDE
12196
+ // gets both `status` and the accompanying `package` info, matching the
12197
+ // legacy behaviour that the UI was written against.
12198
+ if (result?.error) {
12199
+ console.log('[wasm-prepare-result] remote error', result.error);
12200
+ res.send({ code: errorCode, error: result.error, ctx: result?.ctx });
12201
+ return;
11499
12202
  }
12203
+ const data = result?.data?.result ?? { status: WasmStatus.IdleStatus };
12204
+ console.log(`[wasm-prepare-result] remote status=${data?.status}`);
12205
+ res.send({
12206
+ code: successCode,
12207
+ data,
12208
+ ctx: result?.ctx ?? { logid: 'remote' },
12209
+ });
11500
12210
  },
11501
12211
  };
11502
12212
 
@@ -11505,7 +12215,8 @@ const gameWasmPrepareRoute = {
11505
12215
  path: '/game/wasm-prepare',
11506
12216
  handler: async (req, res) => {
11507
12217
  const { codePath, desc, codeMd5, clientKey } = req.body;
11508
- console.log('wasm-prepare-start', req.body);
12218
+ const { pipelineMode } = getLocalState();
12219
+ console.log(`wasm-prepare-start [mode=${pipelineMode}]`, req.body);
11509
12220
  const result = await startPrepare({
11510
12221
  client_key: clientKey,
11511
12222
  desc,
@@ -11540,13 +12251,20 @@ const gameWasmPrepareRoute = {
11540
12251
  },
11541
12252
  };
11542
12253
 
12254
+ /**
12255
+ * 上传符号表(`/symbols`)。只在 session 已通过 `/game/wasm-collect-start`
12256
+ * 打开后调用,与"开 session"解耦:
12257
+ * - 鉴权门是 `/game/wasm-collect-start`(→ `/start`),不在这条。
12258
+ * - 这步是非关键的 debug 信息上传,本地非阻塞/失败不致命,远程为 no-op。
12259
+ * 因此即便失败也只回 errorCode 让调用方静默处理,不应阻断"开始收集"。
12260
+ */
11543
12261
  const gameWasmSetCollectRoute = {
11544
12262
  method: 'post',
11545
12263
  path: '/game/wasm-set-collect',
11546
12264
  handler: async (req, res) => {
11547
12265
  const { clientKey, codeMd5 } = req.body;
11548
- console.log('wasm-set-collect', req.body);
11549
- const response = await setCollect({
12266
+ console.log('wasm-set-collect (upload symbols)', req.body);
12267
+ const response = await uploadCollectSymbols({
11550
12268
  client_key: clientKey,
11551
12269
  wasm_md5: codeMd5,
11552
12270
  });
@@ -11567,16 +12285,27 @@ const gameWasmSetCollectRoute = {
11567
12285
  },
11568
12286
  };
11569
12287
 
11570
- const gameWasmSplitDownloadResultRoute = {
12288
+ /**
12289
+ * 打开 collect session —— 对应后端 `/start`(本地)/ `set_collecting`(远程)。
12290
+ *
12291
+ * 单独成一条 IDE 可见路由(而不是埋在 set-collect 里),是为了让浏览器
12292
+ * Network 里有一条**明确叫 start** 的请求:`/start` 走 Portal 鉴权中间件,
12293
+ * 鉴权失败(`-401`)时这条请求会带着透传上来的 `error.code:-401` 返回,
12294
+ * IDE 全局拦截器据此弹"登录态失效"toast,且排查时能一眼定位到是开 session
12295
+ * 这步报的鉴权,而非 collect 轮询接口。
12296
+ *
12297
+ * `resume` 可选:仅本地 pipeline 有意义,缺省 → `reset:true`(重新开始)。
12298
+ */
12299
+ const gameWasmCollectStartRoute = {
11571
12300
  method: 'post',
11572
- path: '/game/wasm-split-download-result',
12301
+ path: '/game/wasm-collect-start',
11573
12302
  handler: async (req, res) => {
11574
- const { clientKey, codeMd5, codePath } = req.body;
11575
- console.log('game/wasm-split-download-result-start', req.body);
11576
- const response = await getSplitResult({
12303
+ const { clientKey, codeMd5, resume } = req.body;
12304
+ console.log('wasm-collect-start', req.body);
12305
+ const response = await openCollectSession({
11577
12306
  client_key: clientKey,
11578
12307
  wasm_md5: codeMd5,
11579
- wasm_path: codePath,
12308
+ resume,
11580
12309
  });
11581
12310
  if (response.error) {
11582
12311
  res.send({
@@ -11586,35 +12315,55 @@ const gameWasmSplitDownloadResultRoute = {
11586
12315
  });
11587
12316
  }
11588
12317
  else {
11589
- const splitResult = (response.data?.result || {});
11590
- const requiredDownloadFields = [
11591
- 'main_wasm_download_url',
11592
- 'main_wasm_h5_download_url',
11593
- // 'sub_wasm_download_url',
11594
- // 'sub_js_download_url',
11595
- // 'sub_js_data_download_url',
11596
- // 'sub_js_range_download_url',
11597
- ];
11598
- const missingFields = requiredDownloadFields.filter(field => {
11599
- const value = splitResult[field];
11600
- return typeof value !== 'string' || value.trim() === '';
12318
+ res.send({
12319
+ code: successCode,
12320
+ data: response.data || {},
12321
+ ctx: response.ctx,
11601
12322
  });
11602
- if (missingFields.length > 0) {
12323
+ }
12324
+ },
12325
+ };
12326
+
12327
+ const gameWasmSplitDownloadResultRoute = {
12328
+ method: 'post',
12329
+ path: '/game/wasm-split-download-result',
12330
+ handler: async (req, res) => {
12331
+ const { pipelineMode, splitMeta } = getLocalState();
12332
+ if (pipelineMode === 'local') {
12333
+ if (!splitMeta) {
11603
12334
  res.send({
11604
12335
  code: errorCode,
11605
- error: {
11606
- message: `Missing required wasm split fields: ${missingFields.join(', ')}`,
11607
- },
11608
- data: response.data || {},
11609
- ctx: response.ctx,
12336
+ error: { message: 'No local split result found. Run split first.' },
11610
12337
  });
11611
12338
  return;
11612
12339
  }
11613
12340
  res.send({
11614
12341
  code: successCode,
11615
- data: response.data || {},
12342
+ data: { result: splitMeta },
11616
12343
  msg: 'download success',
11617
- ctx: response.ctx,
12344
+ ctx: { logid: 'local' },
12345
+ });
12346
+ return;
12347
+ }
12348
+ const { clientKey, codeMd5, codePath } = req.body;
12349
+ const result = await getSplitResult({
12350
+ client_key: clientKey,
12351
+ wasm_md5: codeMd5,
12352
+ wasm_path: codePath,
12353
+ });
12354
+ if (result.error) {
12355
+ res.send({
12356
+ code: errorCode,
12357
+ error: result.error,
12358
+ ctx: result.ctx,
12359
+ });
12360
+ }
12361
+ else {
12362
+ res.send({
12363
+ code: successCode,
12364
+ data: result.data || {},
12365
+ msg: 'download success',
12366
+ ctx: result.ctx,
11618
12367
  });
11619
12368
  }
11620
12369
  },
@@ -11703,26 +12452,32 @@ const gameWasmSplitResultRoute = {
11703
12452
  method: 'post',
11704
12453
  path: '/game/wasm-split-result',
11705
12454
  handler: async (req, res) => {
11706
- const { codeMd5, clientKey } = req.body;
11707
- console.log('wasm-split-result', req.body);
11708
- const response = await getTaskStatus({
11709
- client_key: clientKey,
11710
- wasm_md5: codeMd5,
11711
- });
11712
- if (response.error) {
11713
- res.send({
11714
- code: errorCode,
11715
- error: response.error,
11716
- ctx: response.ctx,
11717
- });
11718
- }
11719
- else {
12455
+ const { pipelineMode } = getLocalState();
12456
+ if (pipelineMode === 'local') {
11720
12457
  res.send({
11721
12458
  code: successCode,
11722
- data: response.data?.result || {},
11723
- ctx: response.ctx,
12459
+ data: { status: WasmStatus.WasmSplitDoneStatus },
12460
+ ctx: { logid: 'local' },
11724
12461
  });
12462
+ return;
11725
12463
  }
12464
+ const { clientKey, codeMd5 } = req.body;
12465
+ const result = await getTaskStatus({
12466
+ client_key: clientKey,
12467
+ wasm_md5: codeMd5,
12468
+ });
12469
+ if (result?.error) {
12470
+ console.log('[wasm-split-result] remote error', result.error);
12471
+ res.send({ code: errorCode, error: result.error, ctx: result?.ctx });
12472
+ return;
12473
+ }
12474
+ const data = result?.data?.result ?? { status: WasmStatus.IdleStatus };
12475
+ console.log(`[wasm-split-result] remote status=${data?.status}`);
12476
+ res.send({
12477
+ code: successCode,
12478
+ data,
12479
+ ctx: result?.ctx ?? { logid: 'remote' },
12480
+ });
11726
12481
  },
11727
12482
  };
11728
12483
 
@@ -11733,10 +12488,52 @@ const gameWasmSplitConfigRoute = {
11733
12488
  const config = getSplitConfig();
11734
12489
  if (!config) {
11735
12490
  res.send({ code: errorCode, data: 'Failed to parse split config' });
12491
+ return;
11736
12492
  }
11737
- else {
11738
- res.send({ code: successCode, data: config });
12493
+ // When the CLI is restarted mid-session, localState.pipelineMode resets to
12494
+ // 'local' even though the project on disk may have been prepared in remote
12495
+ // mode. Re-infer it from the persisted split config so subsequent
12496
+ // dispatches (taskinfo / collect / split download) pick the right backend.
12497
+ // Heuristic: the local pipeline always writes enableArchiveMode=true, the
12498
+ // legacy remote pipeline always writes enableArchiveMode=false.
12499
+ if (config.enableWasmCollect) {
12500
+ const inferredMode = config.enableArchiveMode === true ? 'local' : 'remote';
12501
+ const current = getLocalState().pipelineMode;
12502
+ if (current !== inferredMode) {
12503
+ setLocalState({ pipelineMode: inferredMode });
12504
+ console.log(`[pipeline] inferred mode=${inferredMode} from webgl-wasm-split.js (was ${current})`);
12505
+ }
12506
+ }
12507
+ // ── wasm drift guard ─────────────────────────────────────────────
12508
+ //
12509
+ // `webgl-wasm-split.js` is persisted state about "which stage was
12510
+ // last completed", but it can desync from reality: the user's Unity
12511
+ // build re-emits `wasmcode/<file>.br` with a fresh, un-instrumented
12512
+ // binary while the config still claims `enableWasmCollect=true`. The
12513
+ // IDE's `canCollect()` then returns true, the prepare step gets
12514
+ // skipped, and the device loads a wasm that has no `scwebgl.logCall`
12515
+ // import — the `[wasmcollect] FATAL: no scwebgl.logCall import`
12516
+ // failure.
12517
+ //
12518
+ // To guard: compare the wasm file currently on disk to the md5 that
12519
+ // startPrepare wrote into `.ttmg-temp/prepared-meta.json`. If they
12520
+ // differ, demote `enableWasmCollect` back to its placeholder string
12521
+ // in the response so `canCollect()` → false and the IDE walks the
12522
+ // user through prepare again. We never touch the real config file
12523
+ // on disk — this is a transient correction at the read boundary, so
12524
+ // the next successful prepare seamlessly re-aligns everything.
12525
+ if (config.enableWasmCollect === true) {
12526
+ const wasmMeta = computeCurrentProjectWasmMd5();
12527
+ if (wasmMeta && wasmMeta.currentMd5 !== wasmMeta.meta.preparedWasmMd5) {
12528
+ console.warn(`[wasmtool] wasm drift detected: project wasm md5=${wasmMeta.currentMd5} but prepared meta expected ${wasmMeta.meta.preparedWasmMd5} (path=${wasmMeta.meta.codePath}). Forcing IDE back to prepare.`);
12529
+ // Mirror the string-placeholder shape the template uses before
12530
+ // prepare writes a real boolean — matches what `canCollect`
12531
+ // expects and is indistinguishable from "never prepared" from
12532
+ // the IDE's perspective.
12533
+ config.enableWasmCollect = '$ENABLEWASMCOLLECT';
12534
+ }
11739
12535
  }
12536
+ res.send({ code: successCode, data: config });
11740
12537
  },
11741
12538
  };
11742
12539
 
@@ -11804,6 +12601,59 @@ function getGameFallbackRoute(publicPath) {
11804
12601
  };
11805
12602
  }
11806
12603
 
12604
+ /**
12605
+ * Explicit "user clicked a lang toggle in the IDE" endpoint. Unlike
12606
+ * `/game/config-fillback` — which intentionally no-ops when the CLI
12607
+ * already has a lang configured — this route always writes the incoming
12608
+ * lang to TTMGRC so the next IDE bootstrap's `setCurrentLang(cliLang)`
12609
+ * call won't stomp the user's fresh choice.
12610
+ */
12611
+ const gameLanguageRoute = {
12612
+ method: 'post',
12613
+ path: '/game/language',
12614
+ handler: async (req, res) => {
12615
+ const incomingLang = req.body?.lang;
12616
+ const nextLang = resolveSupportedLanguage(incomingLang);
12617
+ if (!nextLang) {
12618
+ res.send({
12619
+ code: successCode,
12620
+ data: {
12621
+ lang: null,
12622
+ updated: false,
12623
+ },
12624
+ });
12625
+ return;
12626
+ }
12627
+ setTTMGRC({ lang: nextLang });
12628
+ res.send({
12629
+ code: successCode,
12630
+ data: {
12631
+ lang: nextLang,
12632
+ updated: true,
12633
+ },
12634
+ });
12635
+ },
12636
+ };
12637
+
12638
+ const gamePipelineModeRoute = {
12639
+ method: 'post',
12640
+ path: '/game/pipeline-mode',
12641
+ handler: (req, res) => {
12642
+ const { mode } = req.body;
12643
+ setLocalState({ pipelineMode: mode });
12644
+ console.log(`[pipeline] mode set to: ${mode}`);
12645
+ res.send({ code: successCode, data: { mode } });
12646
+ },
12647
+ };
12648
+ const gamePipelineModeGetRoute = {
12649
+ method: 'get',
12650
+ path: '/game/pipeline-mode',
12651
+ handler: (_req, res) => {
12652
+ const { pipelineMode } = getLocalState();
12653
+ res.send({ code: successCode, data: { mode: pipelineMode } });
12654
+ },
12655
+ };
12656
+
11807
12657
  const routes = [
11808
12658
  gameAssetPreviewUrlRoute,
11809
12659
  gameAssetsRoute,
@@ -11824,6 +12674,7 @@ const routes = [
11824
12674
  gameWasmPrepareDownloadRoute,
11825
12675
  gameWasmSplitRoute,
11826
12676
  gameWasmTaskinfoRoute,
12677
+ gameWasmCollectStartRoute,
11827
12678
  gameWasmSetCollectRoute,
11828
12679
  gameWasmCollectFuncidsRoute,
11829
12680
  gameWasmCollectInfoRoute,
@@ -11832,6 +12683,9 @@ const routes = [
11832
12683
  gameWasmSplitDownloadRoute,
11833
12684
  gameWasmCancelRoute,
11834
12685
  gameWasmSplitResetRoute,
12686
+ gamePipelineModeRoute,
12687
+ gamePipelineModeGetRoute,
12688
+ gameLanguageRoute,
11835
12689
  ];
11836
12690
  function registerRoutes(app, options) {
11837
12691
  const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
@@ -11855,6 +12709,14 @@ async function start() {
11855
12709
  const { url, version } = await listen(app, { maxRetries: 20 });
11856
12710
  console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(t('native.server.readyIn')), chalk.bold(`${Date.now() - startTime}ms`));
11857
12711
  showTips({ server: url });
12712
+ // 联调场景下(root `dev:debug` 脚本)我们用 vite dev server 提供 IDE,
12713
+ // 不需要 CLI 自己再开浏览器到 dist/public 里那份打包后的旧 IDE。设置
12714
+ // `TTMG_DEV_NO_OPEN=1` 抑制 openUrl,由调度脚本统一负责开 5173。
12715
+ // 普通用户场景(npm 安装 ttmg)不会设置这个 env var,行为不变。
12716
+ if (process.env.TTMG_DEV_NO_OPEN === '1') {
12717
+ console.log(chalk.gray(`[dev-debug] TTMG_DEV_NO_OPEN=1 detected, skipping browser auto-open. Use vite dev server (likely http://localhost:5173) instead.`));
12718
+ return;
12719
+ }
11858
12720
  openUrl(url);
11859
12721
  }
11860
12722
 
@@ -12277,7 +13139,7 @@ async function upload({ clientKey, note = '--', dir, }) {
12277
13139
  }
12278
13140
  }
12279
13141
 
12280
- var version = "0.3.8";
13142
+ var version = "0.3.9-beta.2";
12281
13143
  var pkg = {
12282
13144
  version: version};
12283
13145