@ttmg/cli 0.3.8-beta.3 → 0.3.9-beta.wasm.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/index.js +1928 -1205
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +7 -2
  5. package/dist/public/assets/{Detail-DS6U5mRS.js → Detail-CqXvsvPM.js} +1 -1
  6. package/dist/public/assets/Detail-CqXvsvPM.js.br +0 -0
  7. package/dist/public/assets/{MonetizationMode-C8WTPdaL.js → MonetizationMode-2bp9UZWM.js} +1 -1
  8. package/dist/public/assets/{MonetizationModeSummary-BS7EmGz7.js → MonetizationModeSummary-DBtIQq9Z.js} +1 -1
  9. package/dist/public/assets/{baseForm-Dj23Zmdr.js → baseForm-BFkpAlns.js} +4 -4
  10. package/dist/public/assets/baseForm-BFkpAlns.js.br +0 -0
  11. package/dist/public/assets/{index-Co_mqsxn.js → index-B6ZeUJlM.js} +1 -1
  12. package/dist/public/assets/index-BFePHKNX.js +1 -0
  13. package/dist/public/assets/index-BFePHKNX.js.br +0 -0
  14. package/dist/public/assets/{index-BOpd1gIe.js → index-BJQfbNPd.js} +1 -1
  15. package/dist/public/assets/{index-DZTvsbCe.js → index-BPzYPKPA.js} +1 -1
  16. package/dist/public/assets/index-BRFS2ZhY.css +1 -0
  17. package/dist/public/assets/index-BRFS2ZhY.css.br +0 -0
  18. package/dist/public/assets/{index-oYIBlQRY.js → index-BUDAPXlB.js} +1 -1
  19. package/dist/public/assets/{index-C8Mc6d6E.js → index-BYgShzrj.js} +1 -1
  20. package/dist/public/assets/{index-CCoPaW9N.js → index-Ba1XAQLw.js} +1 -1
  21. package/dist/public/assets/{index-C6Obic7Y.js → index-BpefkOoB.js} +1 -1
  22. package/dist/public/assets/index-BvxhyFWU.js +14 -0
  23. package/dist/public/assets/index-BvxhyFWU.js.br +0 -0
  24. package/dist/public/assets/index-BwbPFgZF.css +1 -0
  25. package/dist/public/assets/{index-C6fJhGVf.js → index-CL2qDQto.js} +1 -1
  26. package/dist/public/assets/index-CMUKwrEm.js +1 -0
  27. package/dist/public/assets/{index-sGtn40-w.js → index-CRAXXzpR.js} +1 -1
  28. package/dist/public/assets/index-CRAXXzpR.js.br +0 -0
  29. package/dist/public/assets/{index-D2LsTDVa.css → index-CgAbOvxk.css} +1 -1
  30. package/dist/public/assets/index-CgAbOvxk.css.br +0 -0
  31. package/dist/public/assets/{index-C898tUMR.js → index-DY13Lo-E.js} +1 -1
  32. package/dist/public/assets/index-DY13Lo-E.js.br +0 -0
  33. package/dist/public/assets/index-DqFmR7Qk.css +1 -0
  34. package/dist/public/assets/{index-Ca4ZdkdC.js → index-FFfimhRp.js} +1 -1
  35. package/dist/public/assets/index-FFfimhRp.js.br +0 -0
  36. package/dist/public/assets/{index-D_Y0HD2X.js → index-OLBa9viz.js} +1 -1
  37. package/dist/public/assets/index-OsVhWD4K.js +1 -0
  38. package/dist/public/assets/{index-UZ08kMPF.js → index-f0PBkQd9.js} +1 -1
  39. package/dist/public/assets/{index-TXPH6UQv.js → index-lKrpiZfQ.js} +1 -1
  40. package/dist/public/assets/index-lKrpiZfQ.js.br +0 -0
  41. package/dist/public/assets/{times-_NUvbbyU.js → times-C1GmugF6.js} +1 -1
  42. package/dist/public/index.html +10 -3
  43. package/dist/scripts/dev-debug.js +191 -0
  44. package/package.json +7 -2
  45. package/dist/public/assets/Detail-DS6U5mRS.js.br +0 -0
  46. package/dist/public/assets/baseForm-Dj23Zmdr.js.br +0 -0
  47. package/dist/public/assets/index-BVIWa0s0.js +0 -14
  48. package/dist/public/assets/index-BVIWa0s0.js.br +0 -0
  49. package/dist/public/assets/index-BY1PUmxY.js +0 -1
  50. package/dist/public/assets/index-BY1PUmxY.js.br +0 -0
  51. package/dist/public/assets/index-C898tUMR.js.br +0 -0
  52. package/dist/public/assets/index-Ca4ZdkdC.js.br +0 -0
  53. package/dist/public/assets/index-D2LsTDVa.css.br +0 -0
  54. package/dist/public/assets/index-DPSts5Re.css +0 -1
  55. package/dist/public/assets/index-DPSts5Re.css.br +0 -0
  56. package/dist/public/assets/index-TXPH6UQv.js.br +0 -0
  57. package/dist/public/assets/index-sGtn40-w.js.br +0 -0
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;
@@ -6960,7 +6591,20 @@ function buildCookieHeaderFromSetCookies(setCookies) {
6960
6591
  function isVerboseEnabled() {
6961
6592
  return process.argv.includes('--verbose');
6962
6593
  }
6963
- // ppe_dev_tool
6594
+ /**
6595
+ * PPE / 测试环境开关。
6596
+ *
6597
+ * 所有 CLI 接口(`stark_wasm/v4/*`, `wasm-collect/v1/*`, portal 鉴权等)
6598
+ * 都共用本文件的 `request()`,所以在这里统一注入 PPE header 就能覆盖全量。
6599
+ * 不在 `DEV_HEADERS` 里改是因为那份常量只被 `remotePipeline.ts` 里的老
6600
+ * 远程分包接口 spread 用;新加的 `startSession.ts` / `finishSession.ts` /
6601
+ * `getCollectedFuncIds.ts` 都没 spread,漏一处就会绕开 PPE。
6602
+ *
6603
+ * 要切回线上环境,直接把 `CLI_PPE_ENV` 设成空串即可(下方条件 spread 会
6604
+ * 自动不带这两个 header)。
6605
+ */
6606
+ // const CLI_PPE_ENV: string = 'ppe_wasm_test'; // 走 PPE 测试环境
6607
+ const CLI_PPE_ENV = ''; // 走线上服务(空串 => 下方条件 spread 不注入 PPE header)
6964
6608
  function getAxiosProxyConfig() {
6965
6609
  const config = getTTMGRC();
6966
6610
  // 优先级: http-proxy > socks-proxy > proxy (老字段兼容)
@@ -7116,21 +6760,6 @@ async function request({ url, method, data, headers, params, }) {
7116
6760
  params: getRequestParams(params, data),
7117
6761
  });
7118
6762
  }
7119
- // 打印请求信息
7120
- console.log('\n========== API Request ==========');
7121
- console.log('URL:', url);
7122
- console.log('Method:', method);
7123
- console.log('Headers:', JSON.stringify({
7124
- Cookie: cookie,
7125
- ...(headers || {}),
7126
- }, null, 2));
7127
- if (params) {
7128
- console.log('Query Params:', JSON.stringify(params, null, 2));
7129
- }
7130
- if (data) {
7131
- console.log('Request Body:', JSON.stringify(data, null, 2));
7132
- }
7133
- console.log('=================================\n');
7134
6763
  try {
7135
6764
  const res = await axios({
7136
6765
  url,
@@ -7139,8 +6768,11 @@ async function request({ url, method, data, headers, params, }) {
7139
6768
  params,
7140
6769
  headers: {
7141
6770
  Cookie: cookie,
7142
- // 'x-use-ppe': '1',
7143
- // 'x-tt-env': 'ppe_upgrade_script',
6771
+ // 注入 PPE header — 放在 caller headers 之前,允许单个调用点通过
6772
+ // 显式传 `x-tt-env` 来覆盖本次请求(例如某个接口还没在 PPE 上发布)。
6773
+ ...(CLI_PPE_ENV
6774
+ ? { 'x-use-ppe': '1', 'x-tt-env': CLI_PPE_ENV }
6775
+ : {}),
7144
6776
  ...(headers || {}),
7145
6777
  },
7146
6778
  ...proxyConfig,
@@ -7182,7 +6814,6 @@ async function request({ url, method, data, headers, params, }) {
7182
6814
  }
7183
6815
  }
7184
6816
  async function download(url, filePath) {
7185
- // 清理旧文件
7186
6817
  if (fs.existsSync(filePath)) {
7187
6818
  try {
7188
6819
  fs.unlinkSync(filePath);
@@ -7190,16 +6821,31 @@ async function download(url, filePath) {
7190
6821
  catch { }
7191
6822
  }
7192
6823
  const proxyConfig = getAxiosProxyConfig();
6824
+ console.log('[download] start', { url: url.slice(0, 120), filePath, hasProxy: !!proxyConfig.httpsAgent });
7193
6825
  try {
7194
6826
  const res = await axios.get(url, {
7195
6827
  responseType: 'stream',
7196
- // 让非 2xx 进入 catch
7197
6828
  validateStatus: s => s >= 200 && s < 300,
6829
+ // Bail out if the server doesn't start responding within 30s instead of
6830
+ // hanging forever (e.g. proxy misrouting a CDN signed URL).
6831
+ timeout: 30000,
7198
6832
  ...proxyConfig,
7199
6833
  });
7200
- // 关键:把“流事件”封装为 Promise,并 await
6834
+ const total = Number(res.headers['content-length'] || 0);
6835
+ let received = 0;
6836
+ let lastLoggedPct = -1;
6837
+ const startedAt = Date.now();
7201
6838
  await new Promise((resolve, reject) => {
7202
6839
  const writer = fs.createWriteStream(filePath);
6840
+ // Inactivity watchdog: if no bytes arrive for 60s mid-stream, abort.
6841
+ let inactivityTimer = null;
6842
+ const resetInactivity = () => {
6843
+ if (inactivityTimer)
6844
+ clearTimeout(inactivityTimer);
6845
+ inactivityTimer = setTimeout(() => {
6846
+ onError(new Error('download stalled: no data for 60s'));
6847
+ }, 60000);
6848
+ };
7203
6849
  const onError = (e) => {
7204
6850
  cleanup();
7205
6851
  try {
@@ -7211,28 +6857,42 @@ async function download(url, filePath) {
7211
6857
  };
7212
6858
  const onClose = () => {
7213
6859
  cleanup();
6860
+ console.log(`[download] done: ${received} bytes in ${Date.now() - startedAt}ms`);
7214
6861
  resolve();
7215
6862
  };
7216
6863
  const cleanup = () => {
6864
+ if (inactivityTimer)
6865
+ clearTimeout(inactivityTimer);
7217
6866
  writer.off('error', onError);
7218
6867
  writer.off('close', onClose);
7219
6868
  res.data.off('error', onError);
6869
+ res.data.off('data', onData);
6870
+ };
6871
+ const onData = (chunk) => {
6872
+ received += chunk.length;
6873
+ resetInactivity();
6874
+ if (total > 0) {
6875
+ const pct = Math.floor((received / total) * 10) * 10;
6876
+ if (pct !== lastLoggedPct) {
6877
+ lastLoggedPct = pct;
6878
+ console.log(`[download] ${pct}% (${received}/${total})`);
6879
+ }
6880
+ }
7220
6881
  };
7221
6882
  res.data.on('error', onError);
6883
+ res.data.on('data', onData);
7222
6884
  writer.on('error', onError);
7223
6885
  writer.on('close', onClose);
6886
+ resetInactivity();
7224
6887
  res.data.pipe(writer);
7225
6888
  });
7226
- // 成功
7227
6889
  return { ok: true };
7228
6890
  }
7229
6891
  catch (err) {
7230
- // 403 等受控处理
6892
+ console.log('[download] failed:', err?.message);
7231
6893
  if (isAxiosError(err) && err.response?.status === 403) {
7232
- // 不抛出,让上层自行决定
7233
6894
  throw new Error('下载链接已过期,请重新进行分包后重试');
7234
6895
  }
7235
- // 其他错误抛出或返回
7236
6896
  throw err;
7237
6897
  }
7238
6898
  }
@@ -9227,11 +8887,19 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
9227
8887
  });
9228
8888
  archive.pipe(output);
9229
8889
  // 1. 基础忽略列表 (建议保留这些基础规则,防止包过大)
8890
+ //
8891
+ // 注意 `${TTMG_TEMP_DIR}/**` 必须显式写出:archiver 用的 picomatch 在
8892
+ // 没有 `/**` 后缀时只匹配根目录下的同名条目,不会递归匹配目录内容。
8893
+ // 历史 bug:只写 `__TTMG_TEMP__` 时,`__TTMG_TEMP__/wasmcode/<basename>.br`
8894
+ //(prepare 阶段缓存的「原始未插桩 wasm 备份」)等内部文件全部被打进 zip
8895
+ // 推到设备,体积虚胖外,host 在 fallback 路径下还可能错误命中未插桩 wasm。
8896
+ // 同时保留裸名 `TTMG_TEMP_DIR` 以兜底空目录场景。
9230
8897
  const defaultIgnores = [
9231
8898
  'node_modules/**',
9232
8899
  '.git/**',
9233
8900
  '.DS_Store',
9234
8901
  ttmgPack.TTMG_TEMP_DIR,
8902
+ `${ttmgPack.TTMG_TEMP_DIR}/**`,
9235
8903
  '*.zip', // 忽略自身生成的 zip
9236
8904
  ];
9237
8905
  // 2. 合并自定义规则
@@ -9664,6 +9332,24 @@ const gameCheckRoute = {
9664
9332
  };
9665
9333
 
9666
9334
  const changelog = [
9335
+ {
9336
+ title: '0.3.8',
9337
+ target: {
9338
+ iOS: '>=43.1',
9339
+ Android: '>=43.1',
9340
+ },
9341
+ changes: {
9342
+ bugfix: [
9343
+ {
9344
+ desc: {
9345
+ 'zh-CN': '修复 ttmg login 后 Cookie 中混入 Set-Cookie 属性(Path/Domain/Max-Age 等)且 Max-Age=0 的失效同名 cookie 污染有效值的问题,导致 CLI 请求接口鉴权异常;现已与浏览器行为对齐,剥离属性并过滤过期 cookie,已登录用户无需重新登录',
9346
+ '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.',
9347
+ },
9348
+ module: 'login',
9349
+ },
9350
+ ],
9351
+ },
9352
+ },
9667
9353
  {
9668
9354
  title: '0.3.7',
9669
9355
  target: {
@@ -10479,7 +10165,12 @@ const gameUploadRoute = {
10479
10165
  },
10480
10166
  };
10481
10167
 
10168
+ // 历史遗留:`remotePipeline.ts` 里的老远程分包接口 spread 了 `DEV_HEADERS`。
10169
+ // 目前全局 PPE 走的是 `libs/api/request.ts` 里的 `CLI_PPE_ENV`,这里保持
10170
+ // 相同的值是为了:一旦以后需要按接口粒度覆盖 PPE(例如远程走 PPE、本地
10171
+ // 走线上),只需在这里填回 header、两处值天然一致。
10482
10172
  const BASE_URL = 'https://developers.tiktok.com';
10173
+ const WASM_COLLECT_BASE_URL = `${BASE_URL}/api/wasm-collect/v1`;
10483
10174
  const DEV_HEADERS = {
10484
10175
  // 'x-use-ppe': '1',
10485
10176
  // 'x-tt-env': UNITY_PPE_ENV,
@@ -10502,6 +10193,8 @@ const UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME = {
10502
10193
  WASMSPLITVERSION: `"$WASMSPLITVERSION"`,
10503
10194
  ENABLEWASMSPLIT: `"$ENABLEWASMSPLIT"`,
10504
10195
  IOS_SUB_JS_FILE_CONFIG: `"$IOS_SUB_JS_FILE_CONFIG"`,
10196
+ ENABLEARCHIVEMODE: `"$ENABLEARCHIVEMODE"`,
10197
+ ARCHIVE_CODE_FILE_MD5: `$ARCHIVE_CODE_FILE_MD5`,
10505
10198
  };
10506
10199
 
10507
10200
  const DIR_SPLIT = 'split';
@@ -10529,7 +10222,34 @@ const WASM_SPLIT_SUBPACKAGE_CONFIG = {
10529
10222
  name: 'wasmcode1-ios',
10530
10223
  root: 'wasmcode1-ios/',
10531
10224
  },
10225
+ // archive 模式 split 产物:wasmcode/(主包,包含原文件名以外的 main wasm)+
10226
+ // 下面这两个独立子包目录。回滚 / 取消时必须把它们一起清掉,否则下次构建仍会
10227
+ // 把旧的 sub/archive 文件打进 zip 推到设备。
10228
+ archiveSub: {
10229
+ name: 'wasmcode1',
10230
+ root: 'wasmcode1/',
10231
+ },
10232
+ archiveCode: {
10233
+ name: 'wasmcode-archive',
10234
+ root: 'wasmcode-archive/',
10235
+ },
10532
10236
  };
10237
+ /**
10238
+ * 所有由 split 流程生成、应在 cancel / reset 时被整目录删除的产物目录。
10239
+ * 注意不包含 `origin`(wasmcode/)—— 那是 Unity 原生输出目录,回滚时只能
10240
+ * 把里面的 split 产物 .br 清掉、再把原始 wasm 拷回去,整个删掉会破坏工程。
10241
+ *
10242
+ * 单一事实来源:`restoreFromCache` / `resetWasmSplit` / `cancelSplit` 都从
10243
+ * 这里读取,避免新增模式时漏改某一处导致旧产物泄漏到 zip。
10244
+ */
10245
+ const SPLIT_OUTPUT_DIRS = [
10246
+ WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root,
10247
+ WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root,
10248
+ WASM_SPLIT_SUBPACKAGE_CONFIG.ios.root,
10249
+ WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root,
10250
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveSub.root,
10251
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveCode.root,
10252
+ ];
10533
10253
  const WASM_FILENAME_SUFFIX = '.webgl.wasm.code.unityweb.wasm';
10534
10254
  const BR_SUFFIX = '.br';
10535
10255
  // 输出 JSON 格式
@@ -10540,69 +10260,6 @@ const CONCURRENCY_LIMIT = 2;
10540
10260
  const DOWNLOAD_RETRY = 3;
10541
10261
  const WASM_SPLIT_CONFIG_FILE_NAME = 'webgl-wasm-split.js';
10542
10262
 
10543
- // prepare.ts
10544
- // 若你的 request 是 axios:你可以添加 maxBodyLength/ maxContentLength 等参数
10545
- // 若是 got:可直接传 form 实例
10546
- async function startPrepare(params) {
10547
- const form = new FormData$1();
10548
- form.append('desc', params.desc);
10549
- form.append('wasm_md5', params.wasm_md5);
10550
- form.append('with_ios', 'true');
10551
- // 二进制字段:用 ReadStream(推荐)或 Buffer
10552
- form.append('wasm_file', fs$1.createReadStream(path$1.join(process.cwd(), params.wasm_file_path)), {
10553
- filename: path$1.basename(params.wasm_file_path),
10554
- // 部分后端会依赖 content-type;如果不确定就用 application/octet-stream
10555
- contentType: 'application/wasm',
10556
- });
10557
- /**
10558
- * 兼容 WASM_SYMBOL_FILE_NAME
10559
- * case 1:项目根目录有 webgl.symbols.json 文件
10560
- * case 2:项目根目录没有 webgl.symbols.json 文件,但 TTMG_TEMP_DIR 有
10561
- * 优先读 2
10562
- */
10563
- let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
10564
- if (!fs$1.existsSync(symbolFilePath)) {
10565
- symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
10566
- }
10567
- /**
10568
- * 判断是否有 symbol 文件,有则上传,没有直接接口报错
10569
- */
10570
- if (!fs$1.existsSync(symbolFilePath)) {
10571
- return {
10572
- error: {
10573
- code: 400,
10574
- message: `${WASM_SYMBOL_FILE_NAME} not found at ${path$1.join(process.cwd())},use unity plugin to rebuild`,
10575
- client_key: params.client_key,
10576
- },
10577
- data: null,
10578
- ctx: {
10579
- logid: '',
10580
- httpStatusCode: 400,
10581
- },
10582
- };
10583
- }
10584
- form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
10585
- filename: WASM_SYMBOL_FILE_NAME,
10586
- contentType: 'application/octet-stream',
10587
- });
10588
- // 关键:用 form.getHeaders() 获取带 boundary 的 Content-Type
10589
- const formHeaders = form.getHeaders();
10590
- return request({
10591
- url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
10592
- method: 'POST',
10593
- headers: {
10594
- ...DEV_HEADERS,
10595
- ...formHeaders, // 包含正确的 multipart/form-data; boundary=...
10596
- },
10597
- params: {
10598
- client_key: params.client_key,
10599
- with_ios: true,
10600
- },
10601
- data: form,
10602
- // 若 request 基于 axios,建议加上以下两项以支持大文件:
10603
- });
10604
- }
10605
-
10606
10263
  async function withRetry(fn, retries = 3) {
10607
10264
  let lastErr;
10608
10265
  for (let i = 0; i < retries; i++) {
@@ -10619,18 +10276,26 @@ async function withRetry(fn, retries = 3) {
10619
10276
  }
10620
10277
 
10621
10278
  function updateWasmSplitConfig(fields) {
10279
+ const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
10280
+ let config = fs.readFileSync(configFilePath, 'utf-8');
10622
10281
  for (const field in fields) {
10623
10282
  const value = fields[field];
10624
- const isString = typeof value === 'string';
10625
- const valueStr = isString ? value : String(value);
10626
- const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
10627
10283
  const placeholder = UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME[field];
10628
- const config = fs.readFileSync(configFilePath, 'utf-8');
10629
- // 将占位符替换为 true/false 字面量
10630
- // 用正则?因为 placeholder 是一个字符串,可能包含特殊字符
10631
- const updated = config.replace(placeholder, valueStr);
10632
- fs.writeFileSync(configFilePath, updated, 'utf-8');
10284
+ if (!placeholder)
10285
+ continue;
10286
+ let replacement;
10287
+ if (typeof value === 'boolean' || typeof value === 'number') {
10288
+ replacement = String(value);
10289
+ }
10290
+ else if (typeof value === 'string') {
10291
+ replacement = value;
10292
+ }
10293
+ else {
10294
+ replacement = String(value);
10295
+ }
10296
+ config = config.replace(placeholder, replacement);
10633
10297
  }
10298
+ fs.writeFileSync(configFilePath, config, 'utf-8');
10634
10299
  }
10635
10300
 
10636
10301
  async function compressWasmFile(wasmFilePath, compressedFilePath) {
@@ -10642,7 +10307,11 @@ async function compressWasmFile(wasmFilePath, compressedFilePath) {
10642
10307
  }
10643
10308
  function compressArrayBuffer(arrayBuffer) {
10644
10309
  return new Promise((resolve, reject) => {
10645
- const compressStream = zlib.createBrotliCompress();
10310
+ const compressStream = zlib.createBrotliCompress({
10311
+ params: {
10312
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 9,
10313
+ },
10314
+ });
10646
10315
  compressStream.write(Buffer.from(arrayBuffer));
10647
10316
  compressStream.end();
10648
10317
  const compressedChunks = [];
@@ -10673,164 +10342,953 @@ function keepCacheSync({ entryDir, originalWasmPath, originalSplitConfigPath, })
10673
10342
  if (!fs__namespace.existsSync(gameJsonCachePath)) {
10674
10343
  fs__namespace.copyFileSync(gameJsonPath, gameJsonCachePath);
10675
10344
  }
10345
+ /**
10346
+ * 保存 wasmcode/game.js(如果存在)
10347
+ *
10348
+ * 抖音 / 微信 mini-game 平台对 game.json.subpackages 里声明的每个子包
10349
+ * 都要求根目录有 `game.js`(哪怕是空文件)作为 `tt.loadSubpackage` 的
10350
+ * 入口锚点,否则子包加载直接失败。很多 Unity 项目把 wasmcode 当成
10351
+ * 一个内置子包(参考真实工程的 game.json),原始 wasmcode/ 里就有
10352
+ * 一个空 game.js。split 阶段虽然也会重写它,但回退时如果不把这个
10353
+ * 占位文件还原回来,mini-game 启动会因为找不到 wasmcode/game.js
10354
+ * 而崩。
10355
+ *
10356
+ * 这里只在源文件存在时才备份,避免给"原始就没有 game.js"的工程
10357
+ * 偷偷塞一个空文件污染回退状态。
10358
+ */
10359
+ const originGameJsPath = path__namespace.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, 'game.js');
10360
+ const originGameJsCachePath = path__namespace.join(cacheDir, 'wasmcode-game.js');
10361
+ if (fs__namespace.existsSync(originGameJsPath) &&
10362
+ !fs__namespace.existsSync(originGameJsCachePath)) {
10363
+ fs__namespace.copyFileSync(originGameJsPath, originGameJsCachePath);
10364
+ }
10676
10365
  return {
10677
10366
  cacheDir,
10678
10367
  };
10679
10368
  }
10680
10369
 
10681
- async function downloadPrepared(data) {
10682
- wsServer.sendUnitySplitStatus({
10683
- status: 'star_fetch_prepared_wasm_url',
10684
- });
10685
- const res = await request({
10686
- url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
10687
- method: 'POST',
10688
- headers: DEV_HEADERS,
10689
- data,
10690
- });
10691
- wsServer.sendUnitySplitStatus({
10692
- status: 'fetch_prepared_wasm_url_done',
10370
+ /**
10371
+ * Restore webgl-wasm-split.js from the cached original (with placeholders).
10372
+ * Called at the start of each prepare so that pipeline-specific values can be
10373
+ * applied deterministically, regardless of previous runs.
10374
+ * No-op if the cache does not yet exist (first run).
10375
+ */
10376
+ function restoreSplitConfigFromCache(entryDir = process.cwd()) {
10377
+ const cachedConfigPath = path.join(entryDir, WASM_SPLIT_CACHE_DIR, path.basename(WASM_SPLIT_CONFIG_FILE_NAME));
10378
+ const targetConfigPath = path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME);
10379
+ if (fs.existsSync(cachedConfigPath)) {
10380
+ fs.copyFileSync(cachedConfigPath, targetConfigPath);
10381
+ }
10382
+ }
10383
+
10384
+ /**
10385
+ * Restore the project from the backup cache:
10386
+ * - original (unmodified) wasm file back into its `wasmcode/<file>.br` location
10387
+ * - webgl-wasm-split.js back to its template (with placeholders)
10388
+ * - game.json back to its pre-split version
10389
+ * - wasmcode/game.js — restore from cache if backed up, otherwise write empty
10390
+ * (mini-game runtime requires every subpackage root to contain `game.js` as
10391
+ * a `tt.loadSubpackage` entry anchor; many Unity projects ship with
10392
+ * wasmcode declared as a subpackage in game.json, so a missing wasmcode/game.js
10393
+ * after rollback breaks subpackage loading at boot)
10394
+ * - remove ALL generated sub-package directories (see SPLIT_OUTPUT_DIRS) —
10395
+ * includes both legacy (wasmcode-android / wasmcode1-android / wasmcode-ios /
10396
+ * wasmcode1-ios) and archive-mode (wasmcode1 / wasmcode-archive) outputs
10397
+ * - clean stale split-produced .br files inside wasmcode/ (split phase writes
10398
+ * `${main_wasm_md5}.webgl...br` next to the original `${orig_md5}.webgl...br`;
10399
+ * we wipe everything in there and re-copy the cached original so the dir
10400
+ * ends up byte-identical to the pre-prepare state)
10401
+ *
10402
+ * Shared by both local and remote reset/rollback flows. Single source of
10403
+ * truth for "what does cancel actually undo" — adding a new split-output
10404
+ * dir means appending it to SPLIT_OUTPUT_DIRS, no other call site needs
10405
+ * to change.
10406
+ */
10407
+ function restoreFromCache(entryDir = process.cwd()) {
10408
+ const cacheDir = path.join(entryDir, WASM_SPLIT_CACHE_DIR);
10409
+ // 1) Wipe stale split residue inside wasmcode/ first, THEN restore the
10410
+ // original. Order matters: if we restore first then wipe, we'd delete
10411
+ // the very file we just brought back.
10412
+ const originDir = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root);
10413
+ if (fs.existsSync(originDir)) {
10414
+ for (const entry of fs.readdirSync(originDir)) {
10415
+ // Only clean files split is known to write — `.br` (main wasm) and
10416
+ // the empty `game.js` placeholder. Touching anything else risks
10417
+ // nuking developer-authored content that happens to live in
10418
+ // wasmcode/ for unrelated reasons.
10419
+ if (entry.endsWith('.br') || entry === 'game.js') {
10420
+ fs.rmSync(path.join(originDir, entry), { force: true });
10421
+ }
10422
+ }
10423
+ }
10424
+ if (fs.existsSync(cacheDir)) {
10425
+ const targetWasmBrPath = fs
10426
+ .readdirSync(cacheDir)
10427
+ .find(item => item.endsWith('.br'));
10428
+ if (targetWasmBrPath) {
10429
+ const destWasmBrPath = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
10430
+ ensureDirSync(path.dirname(destWasmBrPath));
10431
+ fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
10432
+ }
10433
+ }
10434
+ const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
10435
+ if (fs.existsSync(splitConfigCachePath)) {
10436
+ fs.copyFileSync(splitConfigCachePath, path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME));
10437
+ }
10438
+ const gameJsonCachePath = path.join(cacheDir, 'game.json');
10439
+ if (fs.existsSync(gameJsonCachePath)) {
10440
+ fs.copyFileSync(gameJsonCachePath, path.join(entryDir, 'game.json'));
10441
+ }
10442
+ // Restore wasmcode/game.js. We just deleted whatever was there in step 1,
10443
+ // so we always need to put something back when wasmcode is a subpackage.
10444
+ // Strategy:
10445
+ // - Prefer the cache (keepCacheSync stashes pre-split contents to
10446
+ // `__unity_cache__/wasmcode-game.js` when the original existed)
10447
+ // - Fall back to writing an empty file. Rationale: if we got here the
10448
+ // dir exists, the .br is in place, and the project's game.json — now
10449
+ // restored above — likely still lists wasmcode as a subpackage (true
10450
+ // for every Unity template we ship). An empty game.js is exactly what
10451
+ // downloadSplited.ts also writes; it satisfies the platform requirement
10452
+ // without changing semantics for projects that don't use wasmcode as
10453
+ // a subpackage (the file is harmless empty).
10454
+ const originGameJsCachePath = path.join(cacheDir, 'wasmcode-game.js');
10455
+ const originGameJsDestPath = path.join(originDir, 'game.js');
10456
+ if (fs.existsSync(originDir)) {
10457
+ if (fs.existsSync(originGameJsCachePath)) {
10458
+ fs.copyFileSync(originGameJsCachePath, originGameJsDestPath);
10459
+ }
10460
+ else {
10461
+ fs.writeFileSync(originGameJsDestPath, '', 'utf-8');
10462
+ }
10463
+ }
10464
+ for (const subDir of SPLIT_OUTPUT_DIRS) {
10465
+ const full = path.join(entryDir, subDir);
10466
+ if (fs.existsSync(full)) {
10467
+ fs.rmSync(full, { recursive: true, force: true });
10468
+ }
10469
+ }
10470
+ }
10471
+
10472
+ async function decompressWasmFile(inputPath, outputPath) {
10473
+ const compressed = await fs.promises.readFile(inputPath);
10474
+ const decompressed = await new Promise((resolve, reject) => {
10475
+ zlib.brotliDecompress(compressed, (err, result) => {
10476
+ if (err)
10477
+ reject(err);
10478
+ else
10479
+ resolve(result);
10480
+ });
10693
10481
  });
10482
+ await fs.promises.writeFile(outputPath, decompressed);
10483
+ }
10484
+
10485
+ function computeFileMd5Sync(filePath) {
10486
+ const content = fs.readFileSync(filePath);
10487
+ return crypto.createHash('md5').update(content).digest('hex');
10488
+ }
10489
+
10490
+ let cached = null;
10491
+ function getGameJson() {
10492
+ if (cached)
10493
+ return cached;
10494
+ const filePath = path$1.join(process.cwd(), 'game.json');
10495
+ if (fs$1.existsSync(filePath)) {
10496
+ try {
10497
+ cached = JSON.parse(fs$1.readFileSync(filePath, 'utf-8'));
10498
+ }
10499
+ catch {
10500
+ cached = {};
10501
+ }
10502
+ }
10503
+ else {
10504
+ cached = {};
10505
+ }
10506
+ return cached;
10507
+ }
10508
+
10509
+ function metaFilePath(entryDir = process.cwd()) {
10510
+ return path__namespace$1.join(entryDir, TTMG_TEMP_DIR, 'prepared-meta.json');
10511
+ }
10512
+ function writePreparedMeta(meta, entryDir = process.cwd()) {
10513
+ const target = metaFilePath(entryDir);
10514
+ fs__namespace$1.mkdirSync(path__namespace$1.dirname(target), { recursive: true });
10515
+ const payload = {
10516
+ ...meta,
10517
+ preparedAt: new Date().toISOString(),
10518
+ };
10519
+ fs__namespace$1.writeFileSync(target, JSON.stringify(payload, null, 2), 'utf-8');
10520
+ }
10521
+ function readPreparedMeta(entryDir = process.cwd()) {
10522
+ const target = metaFilePath(entryDir);
10523
+ if (!fs__namespace$1.existsSync(target))
10524
+ return null;
10694
10525
  try {
10695
- const downloadUrl = res?.data?.result?.download_url;
10696
- const willReplaceWasmPath = path.join(process.cwd(), data.wasm_path);
10697
- if (downloadUrl) {
10698
- const { cacheDir } = keepCacheSync({
10699
- entryDir: process.cwd(),
10700
- originalWasmPath: data.wasm_path,
10701
- originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
10702
- });
10703
- if (downloadUrl.includes('.br')) {
10704
- const tempWasmPath = path.join(cacheDir, '__temp__.wasm.br');
10705
- wsServer.sendUnitySplitStatus({
10706
- status: 'start_download_prepared_wasm',
10707
- url: downloadUrl,
10708
- });
10709
- await download(downloadUrl, tempWasmPath);
10710
- /**
10711
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
10712
- */
10713
- fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
10714
- wsServer.sendUnitySplitStatus({
10715
- status: 'download_prepared_wasm_done',
10716
- url: downloadUrl,
10717
- });
10718
- }
10719
- else {
10720
- const tempWasmPath = path.join(cacheDir, '__temp__.wasm');
10721
- wsServer.sendUnitySplitStatus({
10722
- status: 'start_download_prepared_wasm',
10723
- url: downloadUrl,
10724
- });
10725
- await download(downloadUrl, tempWasmPath);
10726
- wsServer.sendUnitySplitStatus({
10727
- status: 'download_prepared_wasm_done',
10728
- url: downloadUrl,
10729
- });
10730
- /**
10731
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
10732
- */
10733
- wsServer.sendUnitySplitStatus({
10734
- status: 'start_compress_prepared_wasm',
10735
- });
10736
- await compressWasmFile(tempWasmPath, willReplaceWasmPath);
10737
- wsServer.sendUnitySplitStatus({
10738
- status: 'compress_prepared_wasm_done',
10739
- url: downloadUrl,
10740
- });
10741
- wsServer.sendUnitySplitStatus({
10742
- status: 'write_compress_prepared_wasm_done',
10743
- });
10744
- }
10745
- wsServer.sendUnitySplitStatus({
10746
- status: 'start_update_wasm_split_config',
10747
- });
10748
- /**
10749
- * 读取 webgl-wasm-split.js内容,将 enableWasmCollect 设为 true
10750
- */
10751
- updateWasmSplitConfig({
10752
- ENABLEWASMCOLLECT: true,
10753
- });
10754
- wsServer.sendUnitySplitStatus({
10755
- status: 'update_wasm_split_config_done',
10756
- });
10757
- return {
10758
- isSuccess: true,
10759
- ctx: res?.ctx,
10760
- };
10526
+ const raw = fs__namespace$1.readFileSync(target, 'utf-8');
10527
+ const parsed = JSON.parse(raw);
10528
+ if (typeof parsed?.preparedWasmMd5 === 'string' &&
10529
+ typeof parsed?.codePath === 'string' &&
10530
+ parsed.preparedWasmMd5.length === 32) {
10531
+ return parsed;
10532
+ }
10533
+ return null;
10534
+ }
10535
+ catch {
10536
+ return null;
10537
+ }
10538
+ }
10539
+ /**
10540
+ * Return the current md5 of the wasm file referenced by `prepared-meta.json`
10541
+ * or null if the file is missing / meta isn't present. Caller compares the
10542
+ * result to `meta.preparedWasmMd5` mismatch means the project's wasm
10543
+ * has drifted from the prepared output (Unity re-build etc.) and the
10544
+ * project should be walked back through the prepare step before collect
10545
+ * can produce useful data.
10546
+ */
10547
+ function computeCurrentProjectWasmMd5(entryDir = process.cwd()) {
10548
+ const meta = readPreparedMeta(entryDir);
10549
+ if (!meta)
10550
+ return null;
10551
+ const absolutePath = path__namespace$1.join(entryDir, meta.codePath);
10552
+ if (!fs__namespace$1.existsSync(absolutePath))
10553
+ return null;
10554
+ const currentMd5 = crypto$1
10555
+ .createHash('md5')
10556
+ .update(fs__namespace$1.readFileSync(absolutePath))
10557
+ .digest('hex');
10558
+ return { meta, currentMd5 };
10559
+ }
10560
+
10561
+ const state = {
10562
+ pipelineMode: 'local',
10563
+ originalWasmPath: '',
10564
+ preparedWasmPath: '',
10565
+ codePath: '',
10566
+ splitOutputDir: '',
10567
+ splitMeta: null,
10568
+ totalWasmFuncCount: 0,
10569
+ wasmSize: 0,
10570
+ isArchiveMode: true,
10571
+ };
10572
+ function getLocalState() {
10573
+ return state;
10574
+ }
10575
+ function setLocalState(partial) {
10576
+ Object.assign(state, partial);
10577
+ }
10578
+
10579
+ async function startPrepare$1(params) {
10580
+ const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
10581
+ ensureDirSync(tempDir);
10582
+ const inputPath = path$1.join(process.cwd(), params.wasm_file_path);
10583
+ let rawWasmPath = path$1.join(tempDir, 'original.wasm');
10584
+ if (inputPath.endsWith('.br')) {
10585
+ await decompressWasmFile(inputPath, rawWasmPath);
10586
+ }
10587
+ else {
10588
+ fs$1.copyFileSync(inputPath, rawWasmPath);
10589
+ }
10590
+ const preparedWasmPath = path$1.join(tempDir, 'prepared.wasm');
10591
+ try {
10592
+ const result = ttmgWasmtool.prepare(rawWasmPath, preparedWasmPath);
10593
+ console.log(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
10594
+ const gameJson = getGameJson();
10595
+ const totalWasmFuncCount = gameJson.wasmFuncCount ?? 0;
10596
+ const wasmSize = fs$1.existsSync(inputPath)
10597
+ ? fs$1.statSync(inputPath).size
10598
+ : 0;
10599
+ setLocalState({
10600
+ originalWasmPath: rawWasmPath,
10601
+ preparedWasmPath,
10602
+ codePath: params.wasm_file_path,
10603
+ totalWasmFuncCount,
10604
+ wasmSize,
10605
+ });
10606
+ keepCacheSync({
10607
+ entryDir: process.cwd(),
10608
+ originalWasmPath: params.wasm_file_path,
10609
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
10610
+ });
10611
+ // Start from cached (placeholder) config so pipeline switching is deterministic
10612
+ restoreSplitConfigFromCache();
10613
+ const willReplaceWasmPath = path$1.join(process.cwd(), params.wasm_file_path);
10614
+ // Diagnostic: prove prepare actually produced a different binary
10615
+ // (size should grow noticeably because every function body is prefixed
10616
+ // with a scwebgl.logCall(funcIndex) call).
10617
+ const rawSize = fs$1.existsSync(rawWasmPath) ? fs$1.statSync(rawWasmPath).size : 0;
10618
+ const preparedSize = fs$1.existsSync(preparedWasmPath)
10619
+ ? fs$1.statSync(preparedWasmPath).size
10620
+ : 0;
10621
+ const rawMd5 = fs$1.existsSync(rawWasmPath)
10622
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(rawWasmPath)).digest('hex')
10623
+ : '<missing>';
10624
+ const preparedMd5 = fs$1.existsSync(preparedWasmPath)
10625
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(preparedWasmPath)).digest('hex')
10626
+ : '<missing>';
10627
+ console.log(`[wasmtool] prepare sanity: raw(size=${rawSize} md5=${rawMd5}) -> prepared(size=${preparedSize} md5=${preparedMd5}) delta=${preparedSize - rawSize}`);
10628
+ if (preparedSize <= rawSize || preparedMd5 === rawMd5) {
10629
+ console.warn('[wasmtool] WARNING: prepared wasm is not larger / md5 is unchanged vs raw wasm. Instrumentation likely did not happen.');
10630
+ }
10631
+ console.log('[wasmtool] compressing prepared wasm (quality=9)...');
10632
+ await compressWasmFile(preparedWasmPath, willReplaceWasmPath);
10633
+ console.log('[wasmtool] compressed and written to project');
10634
+ // Diagnostic: confirm the file the client actually fetches was overwritten,
10635
+ // and compare to the cached original brotli so we can prove on-disk replacement.
10636
+ const replacedSize = fs$1.existsSync(willReplaceWasmPath)
10637
+ ? fs$1.statSync(willReplaceWasmPath).size
10638
+ : 0;
10639
+ const replacedMd5 = fs$1.existsSync(willReplaceWasmPath)
10640
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(willReplaceWasmPath)).digest('hex')
10641
+ : '<missing>';
10642
+ const cachedOriginalBr = path$1.join(process.cwd(), TTMG_TEMP_DIR, 'wasmcode', path$1.basename(params.wasm_file_path));
10643
+ const cachedOriginalSize = fs$1.existsSync(cachedOriginalBr)
10644
+ ? fs$1.statSync(cachedOriginalBr).size
10645
+ : 0;
10646
+ const cachedOriginalMd5 = fs$1.existsSync(cachedOriginalBr)
10647
+ ? crypto$1
10648
+ .createHash('md5')
10649
+ .update(fs$1.readFileSync(cachedOriginalBr))
10650
+ .digest('hex')
10651
+ : '<missing>';
10652
+ console.log(`[wasmtool] on-disk replace check: project=${params.wasm_file_path} size=${replacedSize} md5=${replacedMd5} | cached-original size=${cachedOriginalSize} md5=${cachedOriginalMd5}`);
10653
+ if (replacedMd5 === cachedOriginalMd5) {
10654
+ console.warn('[wasmtool] WARNING: project wasm md5 matches cached-original md5. The file was not actually replaced with the instrumented build.');
10761
10655
  }
10762
10656
  else {
10763
- return {
10764
- isSuccess: false,
10765
- error: {
10766
- code: res.data?.code,
10767
- message: res.data?.message,
10768
- },
10769
- ctx: res?.ctx,
10770
- };
10657
+ console.log('[wasmtool] OK: project wasm differs from cached-original — instrumented wasm is on disk.');
10771
10658
  }
10659
+ // Local pipeline uses the new wasm-collect/v1/report API + archive sub-wasm.
10660
+ // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
10661
+ // reports the correct wasm_md5 during the collect phase.
10662
+ updateWasmSplitConfig({
10663
+ ENABLEWASMCOLLECT: true,
10664
+ ENABLEARCHIVEMODE: true,
10665
+ ORIGINALWASMMD5: params.wasm_md5,
10666
+ });
10667
+ console.log('[wasmtool] wasm split config updated (local pipeline: archive=true)');
10668
+ // Disk-persisted anchor for "wasm drift" detection in
10669
+ // `game-wasm-split-config` route. Stores the md5 that prepare just
10670
+ // wrote into the project alongside the project-relative path. The
10671
+ // route reads this back on every Modal open, recomputes the md5 of
10672
+ // the file on disk, and if they differ (Unity re-build, git
10673
+ // checkout, etc.) suppresses `enableWasmCollect=true` in the
10674
+ // response so the IDE goes back through prepare instead of dropping
10675
+ // the user straight into Collect with an un-instrumented wasm on
10676
+ // the device. See `preparedMeta.ts` for full rationale.
10677
+ writePreparedMeta({
10678
+ preparedWasmMd5: replacedMd5,
10679
+ codePath: params.wasm_file_path,
10680
+ });
10681
+ console.log(`[wasmtool] prepared-meta written: md5=${replacedMd5} codePath=${params.wasm_file_path}`);
10682
+ return {
10683
+ data: {
10684
+ code: 0,
10685
+ message: 'success',
10686
+ result: { md5: params.wasm_md5 },
10687
+ },
10688
+ error: null,
10689
+ ctx: { logid: 'local', httpStatusCode: 200 },
10690
+ };
10772
10691
  }
10773
- catch (error) {
10692
+ catch (err) {
10774
10693
  return {
10775
- isSuccess: false,
10694
+ data: null,
10776
10695
  error: {
10777
- code: res.data?.code,
10778
- message: error.message,
10696
+ code: 500,
10697
+ message: err instanceof Error ? err.message : String(err),
10779
10698
  },
10780
- ctx: res?.ctx,
10699
+ ctx: { logid: 'local', httpStatusCode: 500 },
10781
10700
  };
10782
10701
  }
10783
10702
  }
10784
10703
 
10785
- async function getCollectedFuncIds({ client_key, wasm_md5, }) {
10786
- return request({
10787
- url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
10704
+ /**
10705
+ * Local pipeline: startPrepareLocal already compressed/replaced the wasm and
10706
+ * updated webgl-wasm-split.js, so this step is a no-op that just emits UI
10707
+ * status events for parity with the remote flow.
10708
+ */
10709
+ async function downloadPrepared$1(_data) {
10710
+ const { preparedWasmPath } = getLocalState();
10711
+ if (!preparedWasmPath) {
10712
+ return {
10713
+ isSuccess: false,
10714
+ error: { code: 404, message: 'Prepared wasm not found. Run prepare first.' },
10715
+ };
10716
+ }
10717
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
10718
+ return { isSuccess: true, ctx: { logid: 'local' } };
10719
+ }
10720
+
10721
+ async function getCollectedFuncIds$1({ client_key, wasm_md5, }) {
10722
+ const res = await request({
10723
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
10788
10724
  method: 'GET',
10789
- headers: DEV_HEADERS,
10790
10725
  params: {
10791
- client_key,
10726
+ app_id: client_key,
10792
10727
  wasm_md5,
10793
10728
  },
10794
10729
  });
10730
+ const funcCount = res?.data?.func_count ?? 0;
10731
+ return {
10732
+ data: {
10733
+ code: res?.data?.code ?? 0,
10734
+ message: 'success',
10735
+ result: {
10736
+ collected_func_count: funcCount,
10737
+ data_size: funcCount,
10738
+ real_data_size: funcCount,
10739
+ collect_state: res?.data?.collect_state,
10740
+ },
10741
+ },
10742
+ error: res.error,
10743
+ ctx: res.ctx,
10744
+ };
10795
10745
  }
10796
10746
 
10797
- async function setCollect({ client_key, wasm_md5, }) {
10798
- return request({
10799
- url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
10747
+ /**
10748
+ * POST /start — opens a collect session (Portal-authenticated).
10749
+ *
10750
+ * Idempotent on the server: re-opening an already-open session just refreshes
10751
+ * `started_at`; only `reset: true` wipes history.
10752
+ *
10753
+ * Default `reset` is `false` to mirror the server-side default documented in
10754
+ * `wasm_api.md` §5.1 — "页面刷新 / 恢复" must NOT silently destroy data. The
10755
+ * "fresh run" semantic (e.g. user clicks "重新开始分包") is the responsibility
10756
+ * of the caller, which must explicitly pass `reset: true`. See `setCollect`
10757
+ * for the CLI-level wiring of those two paths.
10758
+ *
10759
+ * NOTE on naming: the server route is flat (`/start`, not `/session/start`).
10760
+ * Our local symbol stays `startWasmSession` because it's the "start collect
10761
+ * session" lifecycle primitive from the IDE's perspective.
10762
+ */
10763
+ async function startWasmSession({ client_key, wasm_md5, reset, }) {
10764
+ const res = await request({
10765
+ url: `${WASM_COLLECT_BASE_URL}/start`,
10800
10766
  method: 'POST',
10801
10767
  data: {
10802
- client_key,
10768
+ app_id: client_key,
10803
10769
  wasm_md5,
10770
+ reset: reset ?? false,
10804
10771
  },
10805
- headers: DEV_HEADERS,
10806
10772
  });
10773
+ return {
10774
+ data: res.data
10775
+ ? {
10776
+ code: res.data.code ?? 0,
10777
+ message: res.data.message || 'success',
10778
+ result: {
10779
+ collect_state: res.data.collect_state,
10780
+ started_at: res.data.started_at,
10781
+ },
10782
+ }
10783
+ : null,
10784
+ error: res.error,
10785
+ ctx: res.ctx,
10786
+ };
10807
10787
  }
10808
10788
 
10809
- async function getCollecttingInfo({ client_key, wasm_md5, }) {
10810
- return request({
10811
- url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
10789
+ /**
10790
+ * "开始收集" 的本地 pipeline 实现。语义上等价于老远程流程的
10791
+ * `stark_wasm/v4/post/set_collecting`:**打开 server 端的 collect 窗口**,
10792
+ * 让 plugin 之后的 `/report` 请求能落库。
10793
+ *
10794
+ * 做三件事(顺序敏感):
10795
+ * 1. `POST /start` 打开 session —— 失败必须立即返回给 IDE,
10796
+ * 否则 UI 会让用户进"正在收集"但实际 plugin 所有上报都会被 fail-close
10797
+ * 丢弃,场面非常悲伤。
10798
+ * 2. 成功后上传符号表(`/symbols`)。这一步故意不 await、错误仅 warn —
10799
+ * 符号表只是给 server 端后续调试用的 debug 信息,丢了也不影响分包主链路。
10800
+ * 3. 返回 `{code: 0}`。
10801
+ *
10802
+ * 两种调用语义(与 `wasm_api.md` §5.1 对齐):
10803
+ * - 默认(`resume` 缺省 / false)—— 用户点"开始收集 / 重新开始分包",
10804
+ * 发 `reset: true`,服务端清空历史。这是历史行为,对应 IDE 上的
10805
+ * "willCollect → startCollect" 主入口。
10806
+ * - `resume: true` —— 页面刷新 / 恢复继续,发 `reset: false`,幂等
10807
+ * 打开 session、保留已有 func_ids。需要这条路径的 caller(如 IDE
10808
+ * 重新挂载组件检测到 server `collect_state: "open"` 想接续)必须
10809
+ * 显式传,避免误清。
10810
+ *
10811
+ * Session 生命周期对前端透明——IDE 只知道"开始收集 / 完成收集"两个动作,
10812
+ * `/start` 和 `/finish` 都被封在本地 dispatcher 内。这样远程 pipeline(没有
10813
+ * session 概念)和本地 pipeline(有 session)在 IDE 层看起来是对称的。
10814
+ */
10815
+ async function setCollect$1({ client_key, wasm_md5, resume, }) {
10816
+ const startRes = await startWasmSession({
10817
+ client_key,
10818
+ wasm_md5,
10819
+ reset: !resume,
10820
+ });
10821
+ if (startRes.error || !startRes.data || startRes.data.code !== 0) {
10822
+ // /start is invoked internally by setCollect now; IDE only sees this
10823
+ // bubbled up as a generic "开始收集失败" toast, so dump a structured
10824
+ // one-liner here with logid — the single most useful field when
10825
+ // asking backend to look up what happened on their side.
10826
+ const code = startRes.error?.code ?? startRes.data?.code ?? -1;
10827
+ const message = startRes.error?.message ||
10828
+ startRes.data?.message ||
10829
+ 'Open collect session failed';
10830
+ const logid = startRes.ctx?.logid || 'n/a';
10831
+ console.error(`[wasm-collect] /start failed: code=${code} message=${message} logid=${logid}`);
10832
+ return {
10833
+ data: startRes.data ?? null,
10834
+ error: startRes.error ?? { code, message },
10835
+ ctx: startRes.ctx,
10836
+ };
10837
+ }
10838
+ let symbolPath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
10839
+ if (!fs$1.existsSync(symbolPath)) {
10840
+ symbolPath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
10841
+ }
10842
+ if (fs$1.existsSync(symbolPath)) {
10843
+ const symbols = fs$1.readFileSync(symbolPath, 'utf-8');
10844
+ request({
10845
+ url: `${WASM_COLLECT_BASE_URL}/symbols`,
10846
+ method: 'POST',
10847
+ data: {
10848
+ app_id: client_key,
10849
+ wasm_md5,
10850
+ symbols,
10851
+ },
10852
+ }).catch(err => {
10853
+ console.warn('[wasmtool] Failed to upload symbols:', err);
10854
+ });
10855
+ }
10856
+ return {
10857
+ data: { code: 0, message: 'success', result: {} },
10858
+ error: null,
10859
+ ctx: { logid: 'local', httpStatusCode: 200 },
10860
+ };
10861
+ }
10862
+
10863
+ async function getCollecttingInfo$1({ client_key, wasm_md5, }) {
10864
+ const res = await request({
10865
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
10812
10866
  method: 'GET',
10813
- headers: DEV_HEADERS,
10814
10867
  params: {
10815
- client_key,
10868
+ app_id: client_key,
10816
10869
  wasm_md5,
10817
10870
  },
10818
10871
  });
10872
+ const { totalWasmFuncCount } = getLocalState();
10873
+ // Fall back to game.json.wasmFuncCount so the total survives CLI restarts.
10874
+ const gameJsonFuncCount = Number(getGameJson()?.wasmFuncCount) || 0;
10875
+ return {
10876
+ data: {
10877
+ code: res?.data?.code ?? 0,
10878
+ message: 'success',
10879
+ result: {
10880
+ app_id: client_key,
10881
+ wasm_md5,
10882
+ collected_func_count: res?.data?.func_count ?? 0,
10883
+ total_wasm_func_count: gameJsonFuncCount || totalWasmFuncCount || 0,
10884
+ collect_state: res?.data?.collect_state,
10885
+ },
10886
+ },
10887
+ error: res.error,
10888
+ ctx: res.ctx,
10889
+ };
10819
10890
  }
10820
10891
 
10821
- // /api/stark_wasm/v4/post/split
10822
- async function startSplit({ client_key, wasm_md5, }) {
10823
- return request({
10824
- url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
10892
+ /**
10893
+ * POST /finish closes a collect session and returns the final `func_count`
10894
+ * so the IDE can surface "本次共收集 N 个函数" in the success dialog.
10895
+ * Idempotent on the server.
10896
+ *
10897
+ * NOTE on naming: the server route is flat (`/finish`, not `/session/finish`).
10898
+ * The local symbol keeps `finishWasmSession` for symmetry with `startWasmSession`.
10899
+ */
10900
+ async function finishWasmSession({ client_key, wasm_md5, }) {
10901
+ const res = await request({
10902
+ url: `${WASM_COLLECT_BASE_URL}/finish`,
10825
10903
  method: 'POST',
10826
- headers: {
10827
- ...DEV_HEADERS,
10828
- },
10829
10904
  data: {
10830
- client_key,
10905
+ app_id: client_key,
10831
10906
  wasm_md5,
10832
10907
  },
10833
10908
  });
10909
+ return {
10910
+ data: res.data
10911
+ ? {
10912
+ code: res.data.code ?? 0,
10913
+ message: res.data.message || 'success',
10914
+ result: {
10915
+ collect_state: res.data.collect_state,
10916
+ func_count: res.data.func_count ?? 0,
10917
+ finished_at: res.data.finished_at,
10918
+ },
10919
+ }
10920
+ : null,
10921
+ error: res.error,
10922
+ ctx: res.ctx,
10923
+ };
10924
+ }
10925
+
10926
+ async function startSplit$1({ client_key, wasm_md5, }) {
10927
+ const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
10928
+ const splitOutputDir = path$1.join(tempDir, 'split-output');
10929
+ if (fs$1.existsSync(splitOutputDir)) {
10930
+ fs$1.rmSync(splitOutputDir, { recursive: true, force: true });
10931
+ }
10932
+ ensureDirSync(splitOutputDir);
10933
+ const { originalWasmPath, isArchiveMode: archive } = getLocalState();
10934
+ const rawWasmPath = originalWasmPath || path$1.join(tempDir, 'original.wasm');
10935
+ if (!fs$1.existsSync(rawWasmPath)) {
10936
+ return {
10937
+ data: null,
10938
+ error: {
10939
+ code: 404,
10940
+ message: 'Original wasm not found. Run prepare first.',
10941
+ },
10942
+ ctx: { logid: 'local', httpStatusCode: 404 },
10943
+ };
10944
+ }
10945
+ const exportRes = await request({
10946
+ url: `${WASM_COLLECT_BASE_URL}/export`,
10947
+ method: 'GET',
10948
+ params: {
10949
+ app_id: client_key,
10950
+ wasm_md5,
10951
+ strategy: 'union',
10952
+ },
10953
+ });
10954
+ const funcIds = exportRes?.data?.func_ids;
10955
+ const bootFuncIds = exportRes?.data?.boot_func_ids ?? [];
10956
+ if (!funcIds?.length) {
10957
+ return {
10958
+ data: null,
10959
+ error: {
10960
+ code: 400,
10961
+ message: 'No collected func IDs found.',
10962
+ },
10963
+ ctx: { logid: 'local', httpStatusCode: 400 },
10964
+ };
10965
+ }
10966
+ console.log(`[wasmtool] splitting with ${funcIds.length} func IDs` +
10967
+ (bootFuncIds.length > 0
10968
+ ? `, ${bootFuncIds.length} boot-phase func IDs (→ alwaysInclude)`
10969
+ : ', no boot-phase info (legacy server, falling back to callClosure only)') +
10970
+ `, archive=${archive}`);
10971
+ try {
10972
+ const result = ttmgWasmtool.split({
10973
+ input: rawWasmPath,
10974
+ funcIds,
10975
+ // Boot-phase func ids → `alwaysInclude`. They are a subset of
10976
+ // `funcIds` so this doesn't grow `collect_count`, but it DOES seed
10977
+ // the direct-call closure BFS with the exact set needed for first
10978
+ // frame, and the split tool's `alwaysIncludeAdded` counter is the
10979
+ // observability signal when zero (= server didn't return boot info).
10980
+ alwaysInclude: bootFuncIds.length > 0 ? bootFuncIds : undefined,
10981
+ // Always-on direct-call closure over (collect ∪ alwaysInclude ∪
10982
+ // start_func). Folds in func ids that collect missed (untaken
10983
+ // branches, race conditions during collect) so first-screen code
10984
+ // paths don't trap on archive trampolines. See the split tool's
10985
+ // `closure_added` counter for the per-build size impact.
10986
+ callClosure: true,
10987
+ // Always-on indirect-call type-closure scoped to the boot subset.
10988
+ // Catches IL2CPP virtual / interface / delegate dispatch which is
10989
+ // the dominant source of remaining `firstFrame=BEFORE` archive
10990
+ // trampoline hits after the runtime collect + direct closure
10991
+ // passes (see `indirectClosureAdded` for the per-build size
10992
+ // impact). Defaults to `true` in the wasmtool but we set it
10993
+ // explicitly so a future tool default change can't silently turn
10994
+ // it off in our pipeline.
10995
+ callIndirectClosure: true,
10996
+ outputDir: splitOutputDir,
10997
+ archive,
10998
+ compress: true,
10999
+ quality: 9,
11000
+ });
11001
+ if (result.code !== 0) {
11002
+ return {
11003
+ data: null,
11004
+ error: { code: result.code, message: result.errMsg },
11005
+ ctx: { logid: 'local', httpStatusCode: 500 },
11006
+ };
11007
+ }
11008
+ const mainBrPath = result.mainWasmPath + '.br';
11009
+ const actualMainPath = fs$1.existsSync(mainBrPath)
11010
+ ? mainBrPath
11011
+ : result.mainWasmPath;
11012
+ const mainWasmMd5 = computeFileMd5Sync(actualMainPath);
11013
+ const subBrPath = result.subWasmPath ? result.subWasmPath + '.br' : '';
11014
+ const actualSubPath = subBrPath && fs$1.existsSync(subBrPath)
11015
+ ? subBrPath
11016
+ : result.subWasmPath;
11017
+ const subWasmMd5 = actualSubPath
11018
+ ? computeFileMd5Sync(actualSubPath)
11019
+ : '';
11020
+ let archiveMd5 = '';
11021
+ if (archive && result.archivePath) {
11022
+ const archiveBrPath = result.archivePath + '.br';
11023
+ const actualArchivePath = fs$1.existsSync(archiveBrPath)
11024
+ ? archiveBrPath
11025
+ : result.archivePath;
11026
+ console.log(`[wasmtool] archivePath=${result.archivePath}, brExists=${fs$1.existsSync(archiveBrPath)}, actualExists=${fs$1.existsSync(actualArchivePath)}`);
11027
+ if (fs$1.existsSync(actualArchivePath)) {
11028
+ archiveMd5 = computeFileMd5Sync(actualArchivePath);
11029
+ console.log(`[wasmtool] archive_md5=${archiveMd5}`);
11030
+ }
11031
+ }
11032
+ else {
11033
+ console.log(`[wasmtool] skip archive md5: archive=${archive}, archivePath=${result.archivePath}`);
11034
+ }
11035
+ const globalVarList = result.globalVarList
11036
+ .split(';')
11037
+ .filter(Boolean)
11038
+ .map((entry) => {
11039
+ const [name, type, mutable] = entry.trim().split(',');
11040
+ return { name, type, mutable: mutable === '1' };
11041
+ });
11042
+ const splitMeta = {
11043
+ original_wasm_md5: wasm_md5,
11044
+ main_wasm_md5: mainWasmMd5,
11045
+ main_wasm_h5_md5: mainWasmMd5,
11046
+ sub_wasm_md5: subWasmMd5,
11047
+ archive_md5: archiveMd5,
11048
+ table_size: result.tableSize,
11049
+ global_var_list: globalVarList,
11050
+ version: Date.now(),
11051
+ total_wasm_count: result.totalWasmCount,
11052
+ main_wasm_count: result.mainWasmCount,
11053
+ time_cost: result.timeCost,
11054
+ archive,
11055
+ local_main_wasm_path: result.mainWasmPath,
11056
+ local_sub_wasm_path: result.subWasmPath,
11057
+ local_func_meta_path: result.funcMetaPath,
11058
+ local_archive_path: result.archivePath,
11059
+ // Composition breakdown of main_funcs — the single most useful
11060
+ // piece of information when triaging "why is my main package X MB"
11061
+ // (or, conversely, "why are first-screen sub-package batches still
11062
+ // loading"). collect = runtime-observed, always_include =
11063
+ // boot_func_ids, closure = BFS direct callees, indirect_closure =
11064
+ // type-matching pass scoped to boot funcs (covers IL2CPP virtual
11065
+ // dispatch), export = wasm exports. These sum with imports to
11066
+ // main_wasm_count.
11067
+ collect_func_count: result.collectFuncCount,
11068
+ always_include_added: result.alwaysIncludeAdded,
11069
+ closure_added: result.closureAdded,
11070
+ indirect_closure_added: result.indirectClosureAdded,
11071
+ indirect_closure_types: result.indirectClosureTypes,
11072
+ export_added: result.exportAdded,
11073
+ };
11074
+ setLocalState({ splitOutputDir, splitMeta });
11075
+ console.log(`[wasmtool] split done: total=${result.totalWasmCount}, main=${result.mainWasmCount} ` +
11076
+ `(collect=${result.collectFuncCount}, +alwaysInclude=${result.alwaysIncludeAdded}, ` +
11077
+ `+closure=${result.closureAdded}, +indirectClosure=${result.indirectClosureAdded}` +
11078
+ `[types=${result.indirectClosureTypes}], +exports=${result.exportAdded}), ` +
11079
+ `time=${result.timeCost}s`);
11080
+ // Split landed — close the collect session so the plugin stops reporting.
11081
+ // Awaited (not fire-and-forget) so IDE can rely on "wasm-split returned
11082
+ // success" meaning "session definitively closed". If /finish itself
11083
+ // fails (e.g. portal cookie expired mid-run) we still return split
11084
+ // success to the IDE — the plugin already has the MD5-bound session
11085
+ // state from the earlier /report responses and will time out on TTL
11086
+ // anyway; failing split for a finalizer hiccup would be worse UX.
11087
+ let funcCount;
11088
+ try {
11089
+ const finishRes = await finishWasmSession({ client_key, wasm_md5 });
11090
+ if (finishRes.error || !finishRes.data || finishRes.data.code !== 0) {
11091
+ // Soft failure: split already succeeded from the user's POV, but
11092
+ // this is the main diagnostic breadcrumb if someone later reports
11093
+ // "plugin kept uploading after 分包完成". Always include logid so
11094
+ // backend can cross-reference without having to know our build.
11095
+ const code = finishRes.error?.code ?? finishRes.data?.code ?? -1;
11096
+ const message = finishRes.error?.message ||
11097
+ finishRes.data?.message ||
11098
+ 'finish session non-success';
11099
+ const logid = finishRes.ctx?.logid || 'n/a';
11100
+ console.error(`[wasm-split] /finish failed (split still succeeded): code=${code} message=${message} logid=${logid}`);
11101
+ }
11102
+ else {
11103
+ funcCount = finishRes.data.result?.func_count;
11104
+ }
11105
+ }
11106
+ catch (e) {
11107
+ const msg = e instanceof Error ? e.message : String(e);
11108
+ console.error(`[wasm-split] /finish threw (split still succeeded): ${msg}`);
11109
+ }
11110
+ return {
11111
+ data: { code: 0, message: 'success', func_count: funcCount },
11112
+ error: null,
11113
+ ctx: { logid: 'local', httpStatusCode: 200 },
11114
+ };
11115
+ }
11116
+ catch (err) {
11117
+ return {
11118
+ data: null,
11119
+ error: {
11120
+ code: 500,
11121
+ message: err instanceof Error ? err.message : String(err),
11122
+ },
11123
+ ctx: { logid: 'local', httpStatusCode: 500 },
11124
+ };
11125
+ }
11126
+ }
11127
+
11128
+ const ARCHIVE_SUBPACKAGE_CONFIG = [
11129
+ WASM_SPLIT_SUBPACKAGE_CONFIG.origin,
11130
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveSub,
11131
+ WASM_SPLIT_SUBPACKAGE_CONFIG.archiveCode,
11132
+ ];
11133
+ function updateSubpackageConfigSync(archive = false) {
11134
+ const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
11135
+ const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
11136
+ const gameJson = JSON.parse(raw);
11137
+ delete gameJson.wasmFuncCount;
11138
+ const fieldName = SUBPACKAGE_FIELD_NAMES.find(k => k in gameJson) ??
11139
+ SUBPACKAGE_FIELD_NAMES[0];
11140
+ if (!gameJson[fieldName])
11141
+ gameJson[fieldName] = [];
11142
+ const subpackages = gameJson[fieldName];
11143
+ const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
11144
+ if (archive) {
11145
+ ARCHIVE_SUBPACKAGE_CONFIG.forEach(pkg => filtered.push(pkg));
11146
+ }
11147
+ else {
11148
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
11149
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
11150
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
11151
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
11152
+ }
11153
+ const map = new Map(filtered.map(s => [s.name, s]));
11154
+ gameJson[fieldName] = Array.from(map.values());
11155
+ fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11156
+ }
11157
+
11158
+ async function downloadSplited$1(_context) {
11159
+ const cwd = process.cwd();
11160
+ const { splitMeta } = getLocalState();
11161
+ if (!splitMeta) {
11162
+ return {
11163
+ data: { isSuccess: false },
11164
+ error: { message: 'No local split result found. Run split first.' },
11165
+ ctx: _context,
11166
+ };
11167
+ }
11168
+ const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11169
+ ensureDirSync(splitTempDir);
11170
+ const isArchive = splitMeta.archive;
11171
+ const mainAndroidDir = path.join(splitTempDir, isArchive ? 'wasmcode' : WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
11172
+ const subAndroidDir = path.join(splitTempDir, isArchive ? 'wasmcode1' : WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
11173
+ const mainIosDir = isArchive
11174
+ ? mainAndroidDir
11175
+ : path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
11176
+ const subIosDir = path.join(splitTempDir, isArchive ? 'wasmcode-archive' : WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
11177
+ const dirs = [...new Set([mainAndroidDir, subAndroidDir, mainIosDir, subIosDir])];
11178
+ dirs.forEach(ensureDirSync);
11179
+ try {
11180
+ console.log('[wasmtool] organizing split output...');
11181
+ const mainWasmMd5 = splitMeta.main_wasm_md5;
11182
+ const subWasmMd5 = splitMeta.sub_wasm_md5;
11183
+ const mainWasmH5Md5 = splitMeta.main_wasm_h5_md5;
11184
+ const localMainPath = splitMeta.local_main_wasm_path;
11185
+ const mainBrPath = localMainPath + BR_SUFFIX;
11186
+ const actualMainPath = fs.existsSync(mainBrPath) ? mainBrPath : localMainPath;
11187
+ if (actualMainPath && fs.existsSync(actualMainPath)) {
11188
+ const isBr = actualMainPath.endsWith(BR_SUFFIX);
11189
+ const ext = isBr
11190
+ ? `${WASM_FILENAME_SUFFIX}${BR_SUFFIX}`
11191
+ : WASM_FILENAME_SUFFIX;
11192
+ const mainAndroidDest = path.join(mainAndroidDir, `${mainWasmMd5}${ext}`);
11193
+ fs.copyFileSync(actualMainPath, mainAndroidDest);
11194
+ wsServer.sendUnitySplitStatus({
11195
+ status: 'download_android_main_wasm_done',
11196
+ });
11197
+ if (mainIosDir !== mainAndroidDir) {
11198
+ const mainIosDest = path.join(mainIosDir, `${mainWasmH5Md5}${ext}`);
11199
+ fs.copyFileSync(actualMainPath, mainIosDest);
11200
+ }
11201
+ wsServer.sendUnitySplitStatus({
11202
+ status: 'download_ios_main_wasm_done',
11203
+ });
11204
+ }
11205
+ const localSubPath = splitMeta.local_sub_wasm_path;
11206
+ const subBrPath = localSubPath + BR_SUFFIX;
11207
+ const actualSubPath = fs.existsSync(subBrPath) ? subBrPath : localSubPath;
11208
+ if (actualSubPath && fs.existsSync(actualSubPath)) {
11209
+ const isBr = actualSubPath.endsWith(BR_SUFFIX);
11210
+ const ext = isBr
11211
+ ? `${WASM_FILENAME_SUFFIX}${BR_SUFFIX}`
11212
+ : WASM_FILENAME_SUFFIX;
11213
+ const subAndroidDest = path.join(subAndroidDir, `${subWasmMd5}${ext}`);
11214
+ fs.copyFileSync(actualSubPath, subAndroidDest);
11215
+ wsServer.sendUnitySplitStatus({
11216
+ status: 'download_android_sub_wasm_code_done',
11217
+ });
11218
+ }
11219
+ const localArchivePath = splitMeta.local_archive_path;
11220
+ if (isArchive && localArchivePath) {
11221
+ const archiveBrPath = localArchivePath + BR_SUFFIX;
11222
+ const actualArchivePath = fs.existsSync(archiveBrPath) ? archiveBrPath : localArchivePath;
11223
+ console.log(`[wasmtool] archive copy: archive_md5=${splitMeta.archive_md5}, localPath=${localArchivePath}, brExists=${fs.existsSync(archiveBrPath)}, actual=${actualArchivePath}`);
11224
+ if (fs.existsSync(actualArchivePath)) {
11225
+ const archiveMd5 = splitMeta.archive_md5 || '';
11226
+ const archiveBaseName = path.basename(actualArchivePath);
11227
+ const destName = archiveMd5 ? `${archiveMd5}.${archiveBaseName}` : archiveBaseName;
11228
+ const archiveDest = path.join(subIosDir, destName);
11229
+ console.log(`[wasmtool] archive dest: ${archiveDest}`);
11230
+ fs.copyFileSync(actualArchivePath, archiveDest);
11231
+ }
11232
+ }
11233
+ dirs.forEach((dir) => {
11234
+ fs.writeFileSync(path.join(dir, 'game.js'), '', { encoding: 'utf-8' });
11235
+ });
11236
+ console.log('[wasmtool] copy split output to root...');
11237
+ wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
11238
+ for (const file of fs.readdirSync(splitTempDir)) {
11239
+ const srcPath = path.join(splitTempDir, file);
11240
+ const destPath = path.join(cwd, file);
11241
+ if (fs.existsSync(destPath)) {
11242
+ await promises.rm(destPath, { recursive: true, force: true });
11243
+ }
11244
+ await promises.cp(srcPath, destPath, { recursive: true, force: true });
11245
+ }
11246
+ wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
11247
+ console.log('[wasmtool] updating subpackage config...');
11248
+ updateSubpackageConfigSync(isArchive);
11249
+ console.log('[wasmtool] updating wasm split config...');
11250
+ wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11251
+ updateWasmSplitConfig({
11252
+ ENABLEWASMCOLLECT: true,
11253
+ ORIGINALWASMMD5: `${splitMeta.original_wasm_md5}`,
11254
+ WASMTABLESIZE: splitMeta.table_size,
11255
+ GLOBALVARLIST: JSON.stringify(splitMeta.global_var_list ?? []),
11256
+ SUBJSURL: '',
11257
+ IOS_CODE_FILE_MD5: `${splitMeta.main_wasm_h5_md5}`,
11258
+ ANDROID_CODE_FILE_MD5: `${splitMeta.main_wasm_md5}`,
11259
+ ANDROID_SUB_CODE_FILE_MD5: `${splitMeta.sub_wasm_md5}`,
11260
+ ARCHIVE_CODE_FILE_MD5: `${splitMeta.archive_md5 || ''}`,
11261
+ WASMSPLITVERSION: `${splitMeta.version}`,
11262
+ USINGWASMH5: Boolean(splitMeta.main_wasm_h5_md5),
11263
+ ENABLEWASMSPLIT: true,
11264
+ ENABLEARCHIVEMODE: isArchive,
11265
+ });
11266
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11267
+ return {
11268
+ data: { isSuccess: true },
11269
+ ctx: splitMeta,
11270
+ };
11271
+ }
11272
+ catch (err) {
11273
+ wsServer.sendUnitySplitStatus({
11274
+ status: 'wasm_split_failed',
11275
+ errorMsg: err instanceof Error ? err.message : String(err),
11276
+ });
11277
+ return {
11278
+ data: { isSuccess: false },
11279
+ error: { message: err instanceof Error ? err.message : String(err) },
11280
+ ctx: splitMeta,
11281
+ };
11282
+ }
11283
+ finally {
11284
+ await promises.rm(splitTempDir, { recursive: true, force: true });
11285
+ if (!isArchive) {
11286
+ await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
11287
+ recursive: true,
11288
+ force: true,
11289
+ });
11290
+ }
11291
+ }
10834
11292
  }
10835
11293
 
10836
11294
  /*
@@ -10999,75 +11457,28 @@ function pLimit(concurrency) {
10999
11457
  return generator;
11000
11458
  }
11001
11459
 
11002
- function updateSubpackageConfigSync() {
11003
- const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
11004
- const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
11005
- const gameJson = JSON.parse(raw);
11006
- /**
11007
- * wasm 分包完整流程完成后,删除一次性校验字段,
11008
- * 避免后续调试阶段继续触发 wasmFuncCount 的提示。
11009
- */
11010
- delete gameJson.wasmFuncCount;
11011
- const fieldName = SUBPACKAGE_FIELD_NAMES.find(k => k in gameJson) ??
11012
- SUBPACKAGE_FIELD_NAMES[0];
11013
- if (!gameJson[fieldName])
11014
- gameJson[fieldName] = [];
11015
- const subpackages = gameJson[fieldName];
11016
- // 删除老的 'wasmcode'
11017
- const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
11018
- /**
11019
- * 基于 SUBPACKAGE_CONFIG_FILE_NAME 更新 subpackages
11020
- */
11021
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
11022
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
11023
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
11024
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
11025
- // 合并去重:存在则更新 root,不存在则新增
11026
- const map = new Map(filtered.map(s => [s.name, s]));
11027
- gameJson[fieldName] = Array.from(map.values());
11028
- fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11029
- }
11030
-
11031
- async function downloadAndCompress(opts) {
11032
- const { startDownloadStatus, downloadDoneStatus, startCompressStatus, compressDoneStatus, url, out, enableCompress = false, } = opts;
11460
+ async function downloadOne(opts) {
11461
+ const { startStatus, doneStatus, url, out } = opts;
11033
11462
  if (!url)
11034
11463
  return;
11035
11464
  const willDownloadedFileIsBr = url.includes(BR_SUFFIX);
11036
- const wasmBrOutName = willDownloadedFileIsBr ? out + BR_SUFFIX : out;
11037
- // 下载
11038
- wsServer.sendUnitySplitStatus({ status: startDownloadStatus });
11039
- console.log(`download url: ${url}`);
11465
+ const finalOut = willDownloadedFileIsBr && !out.endsWith(BR_SUFFIX) ? out + BR_SUFFIX : out;
11466
+ wsServer.sendUnitySplitStatus({ status: startStatus });
11467
+ console.log(`[remote-split-download] fetching -> ${finalOut}`);
11040
11468
  const t0 = Date.now();
11041
- await withRetry(() => download(url, wasmBrOutName), DOWNLOAD_RETRY);
11042
- try {
11043
- const st = await promises.stat(wasmBrOutName);
11044
- if (!st.size)
11045
- throw new Error(`Empty download: ${wasmBrOutName}`);
11046
- wsServer.sendUnitySplitStatus({ status: downloadDoneStatus, url });
11047
- console.log(`download done: ${path.basename(wasmBrOutName)} size=${st.size}B time=${Date.now() - t0}ms`);
11048
- }
11049
- catch (e) {
11050
- await promises.rm(wasmBrOutName);
11051
- throw e;
11052
- }
11053
- if (enableCompress) {
11054
- console.log(`compress start: ${path.basename(out)}${BR_SUFFIX}`);
11055
- // 压缩
11056
- wsServer.sendUnitySplitStatus({ status: startCompressStatus });
11057
- const t1 = Date.now();
11058
- await compressWasmFile(out, wasmBrOutName);
11059
- wsServer.sendUnitySplitStatus({ status: compressDoneStatus });
11060
- console.log(`compress done: ${path.basename(wasmBrOutName)} time=${Date.now() - t1}ms`);
11061
- }
11062
- /**
11063
- * 在当前文件所在目录下写入一个空的 game.js
11064
- */
11065
- fs__namespace.writeFileSync(path.join(path.dirname(out), 'game.js'), '', {
11066
- encoding: 'utf-8',
11067
- });
11068
- }
11069
-
11070
- async function downloadSplited(context) {
11469
+ await withRetry(() => download(url, finalOut), DOWNLOAD_RETRY);
11470
+ const st = await promises.stat(finalOut);
11471
+ if (!st.size) {
11472
+ await promises.rm(finalOut, { force: true });
11473
+ throw new Error(`Empty download: ${finalOut}`);
11474
+ }
11475
+ console.log(`[remote-split-download] done: ${path.basename(finalOut)} size=${st.size}B time=${Date.now() - t0}ms`);
11476
+ wsServer.sendUnitySplitStatus({ status: doneStatus, url });
11477
+ // Legacy behaviour: write an empty game.js next to each downloaded artifact
11478
+ // so the subpackage loader doesn't complain about missing js entries.
11479
+ fs.writeFileSync(path.join(path.dirname(out), 'game.js'), '', 'utf-8');
11480
+ }
11481
+ async function downloadSplitedRemote(context) {
11071
11482
  const cwd = process.cwd();
11072
11483
  const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11073
11484
  ensureDirSync(splitTempDir);
@@ -11076,248 +11487,172 @@ async function downloadSplited(context) {
11076
11487
  const mainIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
11077
11488
  const subIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
11078
11489
  [mainAndroidDir, subAndroidDir, mainIosDir, subIosDir].forEach(ensureDirSync);
11079
- const mainAndroidWasmCodeTempPath = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11080
- const subAndroidWasmCodeTempPath = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11081
- const mainIosWasmCodeTempPath = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
11490
+ const mainAndroidOut = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11491
+ const subAndroidOut = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
11492
+ const mainIosOut = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
11082
11493
  const limit = pLimit(CONCURRENCY_LIMIT);
11083
11494
  try {
11084
- console.log('downloadWasmSplit', context);
11085
- // 原有状态文案,按你之前的写法
11086
- wsServer.sendUnitySplitStatus({
11087
- status: 'start_download_android_main_wasm',
11088
- });
11089
- wsServer.sendUnitySplitStatus({
11090
- status: 'start_download_android_sub_wasm_code',
11495
+ console.log('[remote-split-download] start', {
11496
+ original_wasm_md5: context.original_wasm_md5,
11497
+ main_wasm_md5: context.main_wasm_md5,
11498
+ sub_wasm_md5: context.sub_wasm_md5,
11499
+ main_wasm_h5_md5: context.main_wasm_h5_md5,
11091
11500
  });
11092
- wsServer.sendUnitySplitStatus({ status: 'start_download_ios_main_wasm' });
11093
- /**
11094
- * 需要做个保护,只有 有 URL 时才下载
11095
- */
11096
- // 并发下载 + 压缩(带重试)
11097
11501
  await Promise.all([
11098
- limit(() => downloadAndCompress({
11099
- startDownloadStatus: 'start_download_android_main_wasm',
11100
- downloadDoneStatus: 'download_android_main_wasm_done',
11101
- startCompressStatus: 'start_compress_android_main_wasm',
11102
- compressDoneStatus: 'compress_android_main_wasm_done',
11502
+ limit(() => downloadOne({
11503
+ startStatus: 'start_download_android_main_wasm',
11504
+ doneStatus: 'download_android_main_wasm_done',
11103
11505
  url: context.main_wasm_download_url,
11104
- out: mainAndroidWasmCodeTempPath,
11506
+ out: mainAndroidOut,
11105
11507
  })),
11106
- limit(() => downloadAndCompress({
11107
- startDownloadStatus: 'start_download_android_sub_wasm_code',
11108
- downloadDoneStatus: 'download_android_sub_wasm_code_done',
11109
- startCompressStatus: 'start_compress_android_sub_wasm_code',
11110
- compressDoneStatus: 'compress_android_sub_wasm_code_done',
11508
+ limit(() => downloadOne({
11509
+ startStatus: 'start_download_android_sub_wasm_code',
11510
+ doneStatus: 'download_android_sub_wasm_code_done',
11111
11511
  url: context.sub_wasm_download_url,
11112
- out: subAndroidWasmCodeTempPath,
11512
+ out: subAndroidOut,
11113
11513
  })),
11114
- limit(() => downloadAndCompress({
11115
- startDownloadStatus: 'start_download_ios_main_wasm',
11116
- downloadDoneStatus: 'download_ios_main_wasm_done',
11117
- startCompressStatus: 'start_compress_ios_main_wasm',
11118
- compressDoneStatus: 'compress_ios_main_wasm_done',
11514
+ limit(() => downloadOne({
11515
+ startStatus: 'start_download_ios_main_wasm',
11516
+ doneStatus: 'download_ios_main_wasm_done',
11119
11517
  url: context.main_wasm_h5_download_url,
11120
- out: mainIosWasmCodeTempPath,
11518
+ out: mainIosOut,
11121
11519
  })),
11122
- // 下载 ios sub js range json
11123
- limit(() => downloadAndCompress({
11124
- startDownloadStatus: 'start_download_ios_range_json',
11125
- downloadDoneStatus: 'download_ios_range_json_done',
11520
+ limit(() => downloadOne({
11521
+ startStatus: 'start_download_ios_range_json',
11522
+ doneStatus: 'download_ios_range_json_done',
11126
11523
  url: context.sub_js_range_download_url,
11127
11524
  out: path.join(subIosDir, 'func_bytes_range.json'),
11128
11525
  })),
11129
- // 下载 ios sub js data br
11130
- limit(() => downloadAndCompress({
11131
- startDownloadStatus: 'start_download_ios_js_data_br',
11132
- downloadDoneStatus: 'download_ios_js_data_br_done',
11526
+ limit(() => downloadOne({
11527
+ startStatus: 'start_download_ios_js_data_br',
11528
+ doneStatus: 'download_ios_js_data_br_done',
11133
11529
  url: context.sub_js_data_download_url,
11134
11530
  out: path.join(subIosDir, 'subjs.data'),
11135
11531
  })),
11136
11532
  ]);
11137
- // 复制 split/* 到项目根目录(递归、覆盖)——避免 EISDIR
11138
- console.log('copy splitTempDir to root start');
11533
+ console.log('[remote-split-download] copying split output to project root...');
11139
11534
  wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
11140
11535
  for (const file of fs.readdirSync(splitTempDir)) {
11141
11536
  const srcPath = path.join(splitTempDir, file);
11142
11537
  const destPath = path.join(cwd, file);
11143
- // 如果目标路径有文件或目录,先删除
11144
11538
  if (fs.existsSync(destPath)) {
11145
11539
  await promises.rm(destPath, { recursive: true, force: true });
11146
11540
  }
11147
11541
  await promises.cp(srcPath, destPath, { recursive: true, force: true });
11148
11542
  }
11149
11543
  wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
11150
- console.log('copy splitTempDir to root end');
11151
- // 更新分包配置(幂等)
11152
- console.log('updateSubpackageConfigSync start');
11153
- updateSubpackageConfigSync();
11154
- console.log('updateSubpackageConfigSync end');
11155
- // 更新 wasm split 配置(保持原始状态文案)
11156
- console.log('updateWasmSplitConfig start');
11544
+ console.log('[remote-split-download] updating subpackage config...');
11545
+ updateSubpackageConfigSync(false);
11546
+ console.log('[remote-split-download] updating webgl-wasm-split.js...');
11157
11547
  wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11158
11548
  updateWasmSplitConfig({
11159
11549
  ENABLEWASMCOLLECT: true,
11160
- ORIGINALWASMMD5: `${context.original_wasm_md5}`,
11550
+ ENABLEWASMSPLIT: true,
11551
+ ENABLEARCHIVEMODE: false,
11552
+ ORIGINALWASMMD5: `${context.original_wasm_md5 ?? ''}`,
11161
11553
  WASMTABLESIZE: context.table_size,
11162
11554
  GLOBALVARLIST: JSON.stringify(context.global_var_list ?? []),
11163
- SUBJSURL: `${context.sub_js_download_url}`,
11164
- IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5}`,
11165
- ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5}`,
11166
- ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5}`,
11167
- WASMSPLITVERSION: `${context.version}`,
11555
+ SUBJSURL: `${context.sub_js_download_url ?? ''}`,
11556
+ IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5 ?? ''}`,
11557
+ ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5 ?? ''}`,
11558
+ ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5 ?? ''}`,
11559
+ WASMSPLITVERSION: `${context.version ?? ''}`,
11168
11560
  USINGWASMH5: Boolean(context.main_wasm_h5_md5),
11169
- ENABLEWASMSPLIT: true,
11170
- // IOS_SUB_JS_FILE_CONFIG: JSON.stringify(context.merged_js ?? {}),
11171
11561
  });
11172
11562
  wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11173
- console.log('updateWasmSplitConfig end');
11174
- return {
11175
- data: {
11176
- isSuccess: true,
11177
- },
11178
- ctx: context,
11179
- };
11563
+ console.log('[remote-split-download] all done');
11564
+ return { data: { isSuccess: true }, ctx: context };
11180
11565
  }
11181
11566
  catch (err) {
11182
- wsServer.sendUnitySplitStatus({
11183
- status: 'wasm_split_failed',
11184
- errorMsg: err instanceof Error ? err.message : String(err),
11185
- });
11567
+ const message = err instanceof Error ? err.message : String(err);
11568
+ console.log('[remote-split-download] failed:', message);
11569
+ wsServer.sendUnitySplitStatus({ status: 'wasm_split_failed', errorMsg: message });
11186
11570
  return {
11187
- data: {
11188
- isSuccess: false,
11189
- },
11190
- error: {
11191
- message: err instanceof Error ? err.message : String(err),
11192
- },
11571
+ data: { isSuccess: false },
11572
+ error: { message },
11193
11573
  ctx: context,
11194
11574
  };
11195
11575
  }
11196
11576
  finally {
11197
- // 清理临时目录与旧 wasmcode 目录
11198
- console.log('delete splitTempDir start');
11199
11577
  await promises.rm(splitTempDir, { recursive: true, force: true });
11200
- console.log('delete splitTempDir end');
11201
- console.log('delete wasmcode start');
11578
+ // Legacy flow: the server-produced `wasmcode/` placeholder at the project
11579
+ // root is no longer needed once we've laid down the 4 platform dirs.
11202
11580
  await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
11203
11581
  recursive: true,
11204
11582
  force: true,
11205
11583
  });
11206
- console.log('delete wasmcode end');
11207
11584
  }
11208
11585
  }
11209
11586
 
11210
- async function getSplitResult({ client_key, wasm_md5, wasm_path, }) {
11211
- return request({
11212
- url: `${BASE_URL}/api/stark_wasm/v4/post/download`,
11213
- method: 'POST',
11214
- headers: { ...DEV_HEADERS },
11215
- data: { client_key, wasm_md5, wasm_path },
11216
- });
11217
- }
11218
-
11219
- function cancelSplit(params) {
11220
- /**
11221
- * 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
11222
- */
11223
- const { wasmCodePath } = params;
11224
- const cacheDir = path__namespace.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
11225
- /**
11226
- * 恢复 br 文件
11227
- */
11228
- if (fs__namespace.existsSync(cacheDir)) {
11229
- /**
11230
- * 判断是否有缓存的 br 文件
11231
- */
11232
- const targetWasmBrPath = path__namespace.join(cacheDir, path__namespace.basename(wasmCodePath));
11233
- if (fs__namespace.existsSync(targetWasmBrPath)) {
11234
- const destWasmBrPath = path__namespace.join(process.cwd(), wasmCodePath);
11235
- // 规避没有文件夹的情况
11236
- ensureDirSync(path__namespace.dirname(destWasmBrPath));
11237
- fs__namespace.copyFileSync(targetWasmBrPath, destWasmBrPath);
11238
- }
11239
- }
11240
- /**
11241
- * 恢复 webgl-wasm-split.js 文件
11242
- */
11243
- const splitConfigCachePath = path__namespace.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
11244
- if (fs__namespace.existsSync(splitConfigCachePath)) {
11245
- fs__namespace.copyFileSync(splitConfigCachePath, path__namespace.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
11246
- }
11247
- /**
11248
- * 恢复 game.json 文件
11249
- */
11250
- const gameJsonCachePath = path__namespace.join(cacheDir, 'game.json');
11251
- if (fs__namespace.existsSync(gameJsonCachePath)) {
11252
- fs__namespace.copyFileSync(gameJsonCachePath, path__namespace.join(process.cwd(), 'game.json'));
11587
+ async function getSplitResult$1(_params) {
11588
+ const { splitMeta } = getLocalState();
11589
+ if (!splitMeta) {
11590
+ return {
11591
+ data: null,
11592
+ error: {
11593
+ code: 404,
11594
+ message: 'No local split result found. Run split first.',
11595
+ },
11596
+ ctx: { logid: 'local', httpStatusCode: 404 },
11597
+ };
11253
11598
  }
11254
- }
11255
-
11256
- async function resetWasmSplit(data) {
11257
- const res = await request({
11258
- url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
11259
- method: 'POST',
11260
- headers: {
11261
- ...DEV_HEADERS,
11262
- },
11599
+ return {
11263
11600
  data: {
11264
- client_key: data.clientkey,
11265
- wasm_md5: data.wasmMd5,
11601
+ code: 0,
11602
+ message: 'success',
11603
+ result: splitMeta,
11266
11604
  },
11267
- });
11268
- /**
11269
- * 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
11270
- */
11271
- const cacheDir = path.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
11272
- /**
11273
- * 恢复 br 文件
11274
- */
11275
- if (fs.existsSync(cacheDir)) {
11276
- /**
11277
- * 判断是否有缓存的 br 文件
11278
- */
11279
- /**
11280
- * 判断 cache 文件夹下有没有 .br 文件
11281
- *
11282
- */
11283
- const targetWasmBrPath = fs
11284
- .readdirSync(cacheDir)
11285
- .find(item => item.endsWith('.br'));
11286
- if (targetWasmBrPath) {
11287
- const destWasmBrPath = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
11288
- // 规避没有文件夹的情况
11289
- ensureDirSync(path.dirname(destWasmBrPath));
11290
- fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
11291
- }
11292
- }
11293
- /**
11294
- * 恢复 webgl-wasm-split.js 文件
11295
- */
11296
- const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
11297
- if (fs.existsSync(splitConfigCachePath)) {
11298
- fs.copyFileSync(splitConfigCachePath, path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
11299
- }
11300
- /**
11301
- * 恢复 game.json 文件
11302
- */
11303
- const gameJsonCachePath = path.join(cacheDir, 'game.json');
11304
- if (fs.existsSync(gameJsonCachePath)) {
11305
- fs.copyFileSync(gameJsonCachePath, path.join(process.cwd(), 'game.json'));
11306
- }
11307
- /**
11308
- * 删除历史分包产物
11309
- */
11310
- const androidSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
11311
- if (fs.existsSync(androidSubpackageDir)) {
11312
- fs.rmSync(androidSubpackageDir, { recursive: true });
11313
- }
11314
- const androidSubpackageSubDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
11315
- if (fs.existsSync(androidSubpackageSubDir)) {
11316
- fs.rmSync(androidSubpackageSubDir, { recursive: true });
11605
+ error: null,
11606
+ ctx: { logid: 'local', httpStatusCode: 200 },
11607
+ };
11608
+ }
11609
+
11610
+ /**
11611
+ * Local-only undo of a prepare run. Routes through the shared
11612
+ * `restoreFromCache` helper so this stays in sync with `resetWasmSplit`
11613
+ * and never falls behind when new split-output dirs are added.
11614
+ *
11615
+ * `wasmCodePath` is kept on the signature for backward-compat with the
11616
+ * existing /game/wasm-cancel route shape, but it's no longer needed —
11617
+ * `restoreFromCache` already resolves the wasm path from the cache
11618
+ * directory contents (whatever `keepCacheSync` recorded as the original).
11619
+ */
11620
+ function cancelSplit(_params) {
11621
+ restoreFromCache();
11622
+ // Drop the prepared-meta anchor for the same reason resetWasmSplit
11623
+ // does: cancel rolled the wasm back to its un-instrumented state, so
11624
+ // the recorded "preparedWasmMd5" is no longer accurate. Leaving it
11625
+ // would trip the split-config drift guard on the very next Modal open.
11626
+ const preparedMetaPath = path__namespace.join(process.cwd(), TTMG_TEMP_DIR, 'prepared-meta.json');
11627
+ if (fs__namespace.existsSync(preparedMetaPath)) {
11628
+ fs__namespace.rmSync(preparedMetaPath, { force: true });
11317
11629
  }
11318
- const iosSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.ios.root);
11319
- if (fs.existsSync(iosSubpackageDir)) {
11320
- fs.rmSync(iosSubpackageDir, { recursive: true });
11630
+ }
11631
+
11632
+ async function resetWasmSplit$1(data) {
11633
+ const res = await request({
11634
+ url: `${WASM_COLLECT_BASE_URL}/reset`,
11635
+ method: 'POST',
11636
+ data: {
11637
+ app_id: data.clientkey,
11638
+ wasm_md5: data.wasmMd5,
11639
+ },
11640
+ });
11641
+ // Reuse the shared restore helper so local + remote rollback cleans
11642
+ // exactly the same set of files. Previously this function had its own
11643
+ // inline copy that only removed wasmcode-android / wasmcode1-android /
11644
+ // wasmcode-ios — missing wasmcode1 / wasmcode-archive (archive mode)
11645
+ // and wasmcode1-ios (legacy iOS sub), which left stale split outputs
11646
+ // on disk and caused them to be re-uploaded on the next build.
11647
+ restoreFromCache();
11648
+ // Drop the prepared-meta anchor as well — rollback restores the
11649
+ // original wasm into the project, so any md5 we previously recorded
11650
+ // for the prepared build is no longer valid. Leaving it behind would
11651
+ // make the split-config drift guard fire on the very next Modal open
11652
+ // and force the user through a redundant prepare cycle.
11653
+ const preparedMetaPath = path.join(process.cwd(), TTMG_TEMP_DIR, 'prepared-meta.json');
11654
+ if (fs.existsSync(preparedMetaPath)) {
11655
+ fs.rmSync(preparedMetaPath, { force: true });
11321
11656
  }
11322
11657
  return res;
11323
11658
  }
@@ -11354,23 +11689,291 @@ function getSplitConfig() {
11354
11689
  }
11355
11690
  }
11356
11691
 
11357
- const getTaskStatus = (params) => {
11358
- return request({
11359
- url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
11692
+ var WasmStatus;
11693
+ (function (WasmStatus) {
11694
+ WasmStatus[WasmStatus["IdleStatus"] = 0] = "IdleStatus";
11695
+ WasmStatus[WasmStatus["WasmPreparingStatus"] = 1] = "WasmPreparingStatus";
11696
+ WasmStatus[WasmStatus["WasmPreparedStatus"] = 2] = "WasmPreparedStatus";
11697
+ WasmStatus[WasmStatus["WasmSplitStatus"] = 3] = "WasmSplitStatus";
11698
+ WasmStatus[WasmStatus["WasmSplittingStatus"] = 4] = "WasmSplittingStatus";
11699
+ WasmStatus[WasmStatus["WasmSplitDoneStatus"] = 5] = "WasmSplitDoneStatus";
11700
+ WasmStatus[WasmStatus["WasmSplitReadyToPrepareStatus"] = 6] = "WasmSplitReadyToPrepareStatus";
11701
+ WasmStatus[WasmStatus["WasmSplitPreparingStatus"] = 7] = "WasmSplitPreparingStatus";
11702
+ WasmStatus[WasmStatus["WasmSplitPreparedStatus"] = 8] = "WasmSplitPreparedStatus";
11703
+ WasmStatus[WasmStatus["WasmCollectingStatus"] = 9] = "WasmCollectingStatus";
11704
+ WasmStatus[WasmStatus["WasmUploadFailStatus"] = -1] = "WasmUploadFailStatus";
11705
+ WasmStatus[WasmStatus["WasmDownloadFailStatus"] = -2] = "WasmDownloadFailStatus";
11706
+ WasmStatus[WasmStatus["WasmFileNotExistStatus"] = -3] = "WasmFileNotExistStatus";
11707
+ WasmStatus[WasmStatus["WasmSplitFailStatus"] = -4] = "WasmSplitFailStatus";
11708
+ WasmStatus[WasmStatus["WasmSplitUpdateDBFailedStatus"] = -5] = "WasmSplitUpdateDBFailedStatus";
11709
+ WasmStatus[WasmStatus["WasmSplitPrepareFailedStatus"] = -6] = "WasmSplitPrepareFailedStatus";
11710
+ })(WasmStatus || (WasmStatus = {}));
11711
+
11712
+ const getTaskStatus$1 = async (params) => {
11713
+ const { preparedWasmPath, splitMeta } = getLocalState();
11714
+ let status = WasmStatus.IdleStatus;
11715
+ if (splitMeta) {
11716
+ status = WasmStatus.WasmSplitDoneStatus;
11717
+ }
11718
+ else if (preparedWasmPath) {
11719
+ status = WasmStatus.WasmSplitPreparedStatus;
11720
+ }
11721
+ return {
11722
+ data: {
11723
+ code: 0,
11724
+ message: 'success',
11725
+ result: {
11726
+ status,
11727
+ wasm_md5: params.wasm_md5,
11728
+ },
11729
+ },
11730
+ error: null,
11731
+ ctx: { logid: 'local', httpStatusCode: 200 },
11732
+ };
11733
+ };
11734
+
11735
+ const getTaskInfo$1 = async (params) => {
11736
+ const res = await request({
11737
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
11360
11738
  method: 'GET',
11361
- headers: DEV_HEADERS,
11362
- params,
11739
+ params: {
11740
+ app_id: params.client_key,
11741
+ wasm_md5: params.wasm_md5,
11742
+ },
11363
11743
  });
11744
+ const { totalWasmFuncCount, preparedWasmPath, wasmSize } = getLocalState();
11745
+ // Prefer game.json as the source of truth so wasm_size / total_wasm_func_count
11746
+ // survive CLI restarts. localState values are only populated during the
11747
+ // prepare step of the current session; after a restart they default to 0.
11748
+ // game.json carries wasmCodeSize/wasmFuncCount emitted at build time, so
11749
+ // re-entering the collect step still shows the correct totals.
11750
+ const gameJson = getGameJson();
11751
+ const gameJsonWasmSize = Number(gameJson?.wasmCodeSize) || 0;
11752
+ const gameJsonFuncCount = Number(gameJson?.wasmFuncCount) || 0;
11753
+ return {
11754
+ data: {
11755
+ code: res?.data?.code ?? 0,
11756
+ message: 'success',
11757
+ result: {
11758
+ app_id: params.client_key,
11759
+ wasm_md5: params.wasm_md5,
11760
+ is_prepared: Boolean(preparedWasmPath),
11761
+ collected_func_count: res?.data?.func_count ?? 0,
11762
+ total_wasm_func_count: gameJsonFuncCount || totalWasmFuncCount || 0,
11763
+ wasm_size: gameJsonWasmSize || wasmSize || 0,
11764
+ },
11765
+ },
11766
+ error: res.error,
11767
+ ctx: res.ctx,
11768
+ };
11364
11769
  };
11365
11770
 
11366
- const getTaskInfo = async (params) => {
11771
+ async function startPrepareRemote(params) {
11772
+ // Back up the original wasm + split config on the first run so cancel/rollback
11773
+ // works even if the user aborts before the server-side prepare finishes.
11774
+ keepCacheSync({
11775
+ entryDir: process.cwd(),
11776
+ originalWasmPath: params.wasm_file_path,
11777
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
11778
+ });
11779
+ const form = new FormData$1();
11780
+ form.append('desc', params.desc);
11781
+ form.append('wasm_md5', params.wasm_md5);
11782
+ form.append('with_ios', 'true');
11783
+ 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' });
11784
+ let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
11785
+ if (!fs$1.existsSync(symbolFilePath)) {
11786
+ symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
11787
+ }
11788
+ if (!fs$1.existsSync(symbolFilePath)) {
11789
+ return {
11790
+ error: { code: 400, message: `${WASM_SYMBOL_FILE_NAME} not found`, client_key: params.client_key },
11791
+ data: null,
11792
+ ctx: { logid: '', httpStatusCode: 400 },
11793
+ };
11794
+ }
11795
+ form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
11796
+ filename: WASM_SYMBOL_FILE_NAME,
11797
+ contentType: 'application/octet-stream',
11798
+ });
11799
+ const formHeaders = form.getHeaders();
11800
+ return request({
11801
+ url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
11802
+ method: 'POST',
11803
+ headers: { ...DEV_HEADERS, ...formHeaders },
11804
+ params: { client_key: params.client_key, with_ios: true },
11805
+ data: form,
11806
+ });
11807
+ }
11808
+ async function setCollectRemote({ client_key, wasm_md5 }) {
11809
+ return request({
11810
+ url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
11811
+ method: 'POST',
11812
+ data: { client_key, wasm_md5 },
11813
+ headers: DEV_HEADERS,
11814
+ });
11815
+ }
11816
+ async function getCollectedFuncIdsRemote({ client_key, wasm_md5 }) {
11817
+ return request({
11818
+ url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
11819
+ method: 'GET',
11820
+ headers: DEV_HEADERS,
11821
+ params: { client_key, wasm_md5 },
11822
+ });
11823
+ }
11824
+ async function getCollecttingInfoRemote({ client_key, wasm_md5 }) {
11825
+ return request({
11826
+ url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
11827
+ method: 'GET',
11828
+ headers: DEV_HEADERS,
11829
+ params: { client_key, wasm_md5 },
11830
+ });
11831
+ }
11832
+ async function startSplitRemote({ client_key, wasm_md5 }) {
11833
+ return request({
11834
+ url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
11835
+ method: 'POST',
11836
+ headers: { ...DEV_HEADERS },
11837
+ data: { client_key, wasm_md5 },
11838
+ });
11839
+ }
11840
+ async function getTaskInfoRemote(params) {
11367
11841
  return request({
11368
11842
  url: `${BASE_URL}/api/stark_wasm/v4/get/taskinfo`,
11369
11843
  method: 'GET',
11370
11844
  headers: DEV_HEADERS,
11371
11845
  params,
11372
11846
  });
11373
- };
11847
+ }
11848
+ async function getTaskStatusRemote(params) {
11849
+ return request({
11850
+ url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
11851
+ method: 'GET',
11852
+ headers: DEV_HEADERS,
11853
+ params,
11854
+ });
11855
+ }
11856
+ async function resetWasmSplitRemote(data) {
11857
+ const res = await request({
11858
+ url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
11859
+ method: 'POST',
11860
+ headers: { ...DEV_HEADERS },
11861
+ data: { client_key: data.clientkey, wasm_md5: data.wasmMd5 },
11862
+ });
11863
+ // Restore project files (wasm / webgl-wasm-split.js / game.json) so the
11864
+ // next prepare starts from the original placeholders.
11865
+ restoreFromCache();
11866
+ return res;
11867
+ }
11868
+ async function getSplitResultRemote({ client_key, wasm_md5, wasm_path }) {
11869
+ return request({
11870
+ url: `${BASE_URL}/api/stark_wasm/v4/post/download`,
11871
+ method: 'POST',
11872
+ headers: { ...DEV_HEADERS },
11873
+ data: { client_key, wasm_md5, wasm_path },
11874
+ });
11875
+ }
11876
+ /**
11877
+ * Remote pipeline: after the server finishes preparing (instrumenting) the wasm,
11878
+ * fetch the download URL, download the prepared wasm, replace the project file,
11879
+ * and update webgl-wasm-split.js for the LEGACY reporting flow.
11880
+ */
11881
+ async function downloadPreparedRemote(data) {
11882
+ wsServer.sendUnitySplitStatus({ status: 'star_fetch_prepared_wasm_url' });
11883
+ const res = await request({
11884
+ url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
11885
+ method: 'POST',
11886
+ headers: DEV_HEADERS,
11887
+ data,
11888
+ });
11889
+ wsServer.sendUnitySplitStatus({ status: 'fetch_prepared_wasm_url_done' });
11890
+ try {
11891
+ const downloadUrl = res?.data?.result?.download_url;
11892
+ if (!downloadUrl) {
11893
+ console.log('[remote-download-prepared] no download_url in response');
11894
+ return {
11895
+ isSuccess: false,
11896
+ error: { code: res.data?.code, message: res.data?.message || 'No download_url returned' },
11897
+ ctx: res?.ctx,
11898
+ };
11899
+ }
11900
+ const willReplaceWasmPath = path$1.join(process.cwd(), data.wasm_path);
11901
+ const { cacheDir } = keepCacheSync({
11902
+ entryDir: process.cwd(),
11903
+ originalWasmPath: data.wasm_path,
11904
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
11905
+ });
11906
+ console.log(`[remote-download-prepared] target=${willReplaceWasmPath}`);
11907
+ if (downloadUrl.includes('.br')) {
11908
+ const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm.br');
11909
+ console.log('[remote-download-prepared] downloading (br) ->', tempWasmPath);
11910
+ wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
11911
+ const startedAt = Date.now();
11912
+ await download(downloadUrl, tempWasmPath);
11913
+ console.log(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
11914
+ fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
11915
+ wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
11916
+ }
11917
+ else {
11918
+ const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm');
11919
+ console.log('[remote-download-prepared] downloading (raw) ->', tempWasmPath);
11920
+ wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
11921
+ const startedAt = Date.now();
11922
+ await download(downloadUrl, tempWasmPath);
11923
+ console.log(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
11924
+ wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
11925
+ wsServer.sendUnitySplitStatus({ status: 'start_compress_prepared_wasm' });
11926
+ await compressWasmFile(tempWasmPath, willReplaceWasmPath);
11927
+ console.log('[remote-download-prepared] compressed and written to project');
11928
+ wsServer.sendUnitySplitStatus({ status: 'compress_prepared_wasm_done', url: downloadUrl });
11929
+ wsServer.sendUnitySplitStatus({ status: 'write_compress_prepared_wasm_done' });
11930
+ }
11931
+ wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11932
+ // Remote (legacy) pipeline: enable collect but disable archive mode so the
11933
+ // plugin reports to the legacy stark_wasm/v4 collect API.
11934
+ // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
11935
+ // sends the correct wasm_md5 in every collect report.
11936
+ restoreSplitConfigFromCache();
11937
+ updateWasmSplitConfig({
11938
+ ENABLEWASMCOLLECT: true,
11939
+ ENABLEARCHIVEMODE: false,
11940
+ ORIGINALWASMMD5: res?.data?.result?.original_wasm_md5 ||
11941
+ res?.data?.result?.md5 ||
11942
+ data.wasm_md5,
11943
+ });
11944
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11945
+ console.log('[remote-download-prepared] split config updated, returning success');
11946
+ return { isSuccess: true, ctx: res?.ctx };
11947
+ }
11948
+ catch (error) {
11949
+ console.log('[remote-download-prepared] error:', error);
11950
+ return {
11951
+ isSuccess: false,
11952
+ error: { code: res.data?.code, message: error instanceof Error ? error.message : String(error) },
11953
+ ctx: res?.ctx,
11954
+ };
11955
+ }
11956
+ }
11957
+
11958
+ function isLocal() {
11959
+ return getLocalState().pipelineMode === 'local';
11960
+ }
11961
+ const startPrepare = (params) => isLocal() ? startPrepare$1(params) : startPrepareRemote(params);
11962
+ const downloadPrepared = (params) => isLocal() ? downloadPrepared$1() : downloadPreparedRemote(params);
11963
+ const setCollect = (params) => isLocal() ? setCollect$1(params) : setCollectRemote(params);
11964
+ const getCollectedFuncIds = (params) => isLocal() ? getCollectedFuncIds$1(params) : getCollectedFuncIdsRemote(params);
11965
+ const getCollecttingInfo = (params) => isLocal() ? getCollecttingInfo$1(params) : getCollecttingInfoRemote(params);
11966
+ const startSplit = (params) => isLocal() ? startSplit$1(params) : startSplitRemote(params);
11967
+ const downloadSplited = (context) => isLocal() ? downloadSplited$1(context) : downloadSplitedRemote(context);
11968
+ const getSplitResult = (params) => isLocal() ? getSplitResult$1() : getSplitResultRemote(params);
11969
+ const getTaskInfo = (params) => isLocal() ? getTaskInfo$1(params) : getTaskInfoRemote(params);
11970
+ const getTaskStatus = (params) => isLocal() ? getTaskStatus$1(params) : getTaskStatusRemote(params);
11971
+ const resetWasmSplit = (data) => isLocal() ? resetWasmSplit$1(data) : resetWasmSplitRemote(data);
11972
+ // Collect session (`/start` / `/finish`) is an implementation detail of the
11973
+ // local `wasm-collect/v1` pipeline — it's invoked inside `setCollectLocal`
11974
+ // and `startSplitLocal` respectively. The remote `stark_wasm/v4` pipeline
11975
+ // has no session concept. IDE never calls these directly, so there is no
11976
+ // dispatcher exposed here.
11374
11977
 
11375
11978
  const gameWasmCancelRoute = {
11376
11979
  method: 'post',
@@ -11378,9 +11981,7 @@ const gameWasmCancelRoute = {
11378
11981
  handler: async (req, res) => {
11379
11982
  const { codePath } = req.body;
11380
11983
  console.log('wasm-cancel', req.body);
11381
- await cancelSplit({
11382
- wasmCodePath: codePath,
11383
- });
11984
+ await cancelSplit();
11384
11985
  res.send({
11385
11986
  code: successCode,
11386
11987
  msg: 'cancel success',
@@ -11474,26 +12075,35 @@ const gameWasmPrepareResultRoute = {
11474
12075
  method: 'post',
11475
12076
  path: '/game/wasm-prepare-result',
11476
12077
  handler: async (req, res) => {
11477
- console.log('wasm-prepare-result-request', req.body);
11478
- const { clientKey, codeMd5 } = req.body;
11479
- const response = await getTaskStatus({
11480
- client_key: clientKey,
11481
- wasm_md5: codeMd5,
11482
- });
11483
- if (response.error) {
11484
- res.send({
11485
- code: errorCode,
11486
- error: response.error,
11487
- ctx: response.ctx,
11488
- });
11489
- }
11490
- else {
12078
+ const { pipelineMode } = getLocalState();
12079
+ if (pipelineMode === 'local') {
11491
12080
  res.send({
11492
12081
  code: successCode,
11493
- data: response.data?.result || {},
11494
- ctx: response.ctx,
12082
+ data: { status: WasmStatus.WasmSplitPreparedStatus },
12083
+ ctx: { logid: 'local' },
11495
12084
  });
12085
+ return;
12086
+ }
12087
+ const { codeMd5, clientKey } = req.body;
12088
+ const result = await getTaskStatus({
12089
+ client_key: clientKey,
12090
+ wasm_md5: codeMd5,
12091
+ });
12092
+ // For the remote pipeline, forward the full `result` payload so the IDE
12093
+ // gets both `status` and the accompanying `package` info, matching the
12094
+ // legacy behaviour that the UI was written against.
12095
+ if (result?.error) {
12096
+ console.log('[wasm-prepare-result] remote error', result.error);
12097
+ res.send({ code: errorCode, error: result.error, ctx: result?.ctx });
12098
+ return;
11496
12099
  }
12100
+ const data = result?.data?.result ?? { status: WasmStatus.IdleStatus };
12101
+ console.log(`[wasm-prepare-result] remote status=${data?.status}`);
12102
+ res.send({
12103
+ code: successCode,
12104
+ data,
12105
+ ctx: result?.ctx ?? { logid: 'remote' },
12106
+ });
11497
12107
  },
11498
12108
  };
11499
12109
 
@@ -11502,7 +12112,8 @@ const gameWasmPrepareRoute = {
11502
12112
  path: '/game/wasm-prepare',
11503
12113
  handler: async (req, res) => {
11504
12114
  const { codePath, desc, codeMd5, clientKey } = req.body;
11505
- console.log('wasm-prepare-start', req.body);
12115
+ const { pipelineMode } = getLocalState();
12116
+ console.log(`wasm-prepare-start [mode=${pipelineMode}]`, req.body);
11506
12117
  const result = await startPrepare({
11507
12118
  client_key: clientKey,
11508
12119
  desc,
@@ -11541,11 +12152,19 @@ const gameWasmSetCollectRoute = {
11541
12152
  method: 'post',
11542
12153
  path: '/game/wasm-set-collect',
11543
12154
  handler: async (req, res) => {
11544
- const { clientKey, codeMd5 } = req.body;
12155
+ // `resume` is optional and only meaningful for the local pipeline:
12156
+ // when the IDE detects an existing open session (e.g. user refreshed
12157
+ // the page mid-collect) and wants to "继续收集" without nuking the
12158
+ // already-uploaded func_ids, it POSTs `{ resume: true }`. Default
12159
+ // (omitted / false) keeps the historical "fresh run" behaviour on
12160
+ // /start (server gets `reset: true`). See `setCollect` jsdoc for the
12161
+ // two-path contract; remote pipeline ignores the field outright.
12162
+ const { clientKey, codeMd5, resume } = req.body;
11545
12163
  console.log('wasm-set-collect', req.body);
11546
12164
  const response = await setCollect({
11547
12165
  client_key: clientKey,
11548
12166
  wasm_md5: codeMd5,
12167
+ resume,
11549
12168
  });
11550
12169
  if (response.error) {
11551
12170
  res.send({
@@ -11568,50 +12187,42 @@ const gameWasmSplitDownloadResultRoute = {
11568
12187
  method: 'post',
11569
12188
  path: '/game/wasm-split-download-result',
11570
12189
  handler: async (req, res) => {
12190
+ const { pipelineMode, splitMeta } = getLocalState();
12191
+ if (pipelineMode === 'local') {
12192
+ if (!splitMeta) {
12193
+ res.send({
12194
+ code: errorCode,
12195
+ error: { message: 'No local split result found. Run split first.' },
12196
+ });
12197
+ return;
12198
+ }
12199
+ res.send({
12200
+ code: successCode,
12201
+ data: { result: splitMeta },
12202
+ msg: 'download success',
12203
+ ctx: { logid: 'local' },
12204
+ });
12205
+ return;
12206
+ }
11571
12207
  const { clientKey, codeMd5, codePath } = req.body;
11572
- console.log('game/wasm-split-download-result-start', req.body);
11573
- const response = await getSplitResult({
12208
+ const result = await getSplitResult({
11574
12209
  client_key: clientKey,
11575
12210
  wasm_md5: codeMd5,
11576
12211
  wasm_path: codePath,
11577
12212
  });
11578
- if (response.error) {
12213
+ if (result.error) {
11579
12214
  res.send({
11580
12215
  code: errorCode,
11581
- error: response.error,
11582
- ctx: response.ctx,
12216
+ error: result.error,
12217
+ ctx: result.ctx,
11583
12218
  });
11584
12219
  }
11585
12220
  else {
11586
- const splitResult = (response.data?.result || {});
11587
- const requiredDownloadFields = [
11588
- 'main_wasm_download_url',
11589
- 'main_wasm_h5_download_url',
11590
- // 'sub_wasm_download_url',
11591
- // 'sub_js_download_url',
11592
- // 'sub_js_data_download_url',
11593
- // 'sub_js_range_download_url',
11594
- ];
11595
- const missingFields = requiredDownloadFields.filter(field => {
11596
- const value = splitResult[field];
11597
- return typeof value !== 'string' || value.trim() === '';
11598
- });
11599
- if (missingFields.length > 0) {
11600
- res.send({
11601
- code: errorCode,
11602
- error: {
11603
- message: `Missing required wasm split fields: ${missingFields.join(', ')}`,
11604
- },
11605
- data: response.data || {},
11606
- ctx: response.ctx,
11607
- });
11608
- return;
11609
- }
11610
12221
  res.send({
11611
12222
  code: successCode,
11612
- data: response.data || {},
12223
+ data: result.data || {},
11613
12224
  msg: 'download success',
11614
- ctx: response.ctx,
12225
+ ctx: result.ctx,
11615
12226
  });
11616
12227
  }
11617
12228
  },
@@ -11700,26 +12311,32 @@ const gameWasmSplitResultRoute = {
11700
12311
  method: 'post',
11701
12312
  path: '/game/wasm-split-result',
11702
12313
  handler: async (req, res) => {
11703
- const { codeMd5, clientKey } = req.body;
11704
- console.log('wasm-split-result', req.body);
11705
- const response = await getTaskStatus({
11706
- client_key: clientKey,
11707
- wasm_md5: codeMd5,
11708
- });
11709
- if (response.error) {
11710
- res.send({
11711
- code: errorCode,
11712
- error: response.error,
11713
- ctx: response.ctx,
11714
- });
11715
- }
11716
- else {
12314
+ const { pipelineMode } = getLocalState();
12315
+ if (pipelineMode === 'local') {
11717
12316
  res.send({
11718
12317
  code: successCode,
11719
- data: response.data?.result || {},
11720
- ctx: response.ctx,
12318
+ data: { status: WasmStatus.WasmSplitDoneStatus },
12319
+ ctx: { logid: 'local' },
11721
12320
  });
12321
+ return;
11722
12322
  }
12323
+ const { clientKey, codeMd5 } = req.body;
12324
+ const result = await getTaskStatus({
12325
+ client_key: clientKey,
12326
+ wasm_md5: codeMd5,
12327
+ });
12328
+ if (result?.error) {
12329
+ console.log('[wasm-split-result] remote error', result.error);
12330
+ res.send({ code: errorCode, error: result.error, ctx: result?.ctx });
12331
+ return;
12332
+ }
12333
+ const data = result?.data?.result ?? { status: WasmStatus.IdleStatus };
12334
+ console.log(`[wasm-split-result] remote status=${data?.status}`);
12335
+ res.send({
12336
+ code: successCode,
12337
+ data,
12338
+ ctx: result?.ctx ?? { logid: 'remote' },
12339
+ });
11723
12340
  },
11724
12341
  };
11725
12342
 
@@ -11730,10 +12347,52 @@ const gameWasmSplitConfigRoute = {
11730
12347
  const config = getSplitConfig();
11731
12348
  if (!config) {
11732
12349
  res.send({ code: errorCode, data: 'Failed to parse split config' });
12350
+ return;
11733
12351
  }
11734
- else {
11735
- res.send({ code: successCode, data: config });
12352
+ // When the CLI is restarted mid-session, localState.pipelineMode resets to
12353
+ // 'local' even though the project on disk may have been prepared in remote
12354
+ // mode. Re-infer it from the persisted split config so subsequent
12355
+ // dispatches (taskinfo / collect / split download) pick the right backend.
12356
+ // Heuristic: the local pipeline always writes enableArchiveMode=true, the
12357
+ // legacy remote pipeline always writes enableArchiveMode=false.
12358
+ if (config.enableWasmCollect) {
12359
+ const inferredMode = config.enableArchiveMode === true ? 'local' : 'remote';
12360
+ const current = getLocalState().pipelineMode;
12361
+ if (current !== inferredMode) {
12362
+ setLocalState({ pipelineMode: inferredMode });
12363
+ console.log(`[pipeline] inferred mode=${inferredMode} from webgl-wasm-split.js (was ${current})`);
12364
+ }
12365
+ }
12366
+ // ── wasm drift guard ─────────────────────────────────────────────
12367
+ //
12368
+ // `webgl-wasm-split.js` is persisted state about "which stage was
12369
+ // last completed", but it can desync from reality: the user's Unity
12370
+ // build re-emits `wasmcode/<file>.br` with a fresh, un-instrumented
12371
+ // binary while the config still claims `enableWasmCollect=true`. The
12372
+ // IDE's `canCollect()` then returns true, the prepare step gets
12373
+ // skipped, and the device loads a wasm that has no `scwebgl.logCall`
12374
+ // import — the `[wasmcollect] FATAL: no scwebgl.logCall import`
12375
+ // failure.
12376
+ //
12377
+ // To guard: compare the wasm file currently on disk to the md5 that
12378
+ // startPrepare wrote into `.ttmg-temp/prepared-meta.json`. If they
12379
+ // differ, demote `enableWasmCollect` back to its placeholder string
12380
+ // in the response so `canCollect()` → false and the IDE walks the
12381
+ // user through prepare again. We never touch the real config file
12382
+ // on disk — this is a transient correction at the read boundary, so
12383
+ // the next successful prepare seamlessly re-aligns everything.
12384
+ if (config.enableWasmCollect === true) {
12385
+ const wasmMeta = computeCurrentProjectWasmMd5();
12386
+ if (wasmMeta && wasmMeta.currentMd5 !== wasmMeta.meta.preparedWasmMd5) {
12387
+ 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.`);
12388
+ // Mirror the string-placeholder shape the template uses before
12389
+ // prepare writes a real boolean — matches what `canCollect`
12390
+ // expects and is indistinguishable from "never prepared" from
12391
+ // the IDE's perspective.
12392
+ config.enableWasmCollect = '$ENABLEWASMCOLLECT';
12393
+ }
11736
12394
  }
12395
+ res.send({ code: successCode, data: config });
11737
12396
  },
11738
12397
  };
11739
12398
 
@@ -11801,6 +12460,59 @@ function getGameFallbackRoute(publicPath) {
11801
12460
  };
11802
12461
  }
11803
12462
 
12463
+ /**
12464
+ * Explicit "user clicked a lang toggle in the IDE" endpoint. Unlike
12465
+ * `/game/config-fillback` — which intentionally no-ops when the CLI
12466
+ * already has a lang configured — this route always writes the incoming
12467
+ * lang to TTMGRC so the next IDE bootstrap's `setCurrentLang(cliLang)`
12468
+ * call won't stomp the user's fresh choice.
12469
+ */
12470
+ const gameLanguageRoute = {
12471
+ method: 'post',
12472
+ path: '/game/language',
12473
+ handler: async (req, res) => {
12474
+ const incomingLang = req.body?.lang;
12475
+ const nextLang = resolveSupportedLanguage(incomingLang);
12476
+ if (!nextLang) {
12477
+ res.send({
12478
+ code: successCode,
12479
+ data: {
12480
+ lang: null,
12481
+ updated: false,
12482
+ },
12483
+ });
12484
+ return;
12485
+ }
12486
+ setTTMGRC({ lang: nextLang });
12487
+ res.send({
12488
+ code: successCode,
12489
+ data: {
12490
+ lang: nextLang,
12491
+ updated: true,
12492
+ },
12493
+ });
12494
+ },
12495
+ };
12496
+
12497
+ const gamePipelineModeRoute = {
12498
+ method: 'post',
12499
+ path: '/game/pipeline-mode',
12500
+ handler: (req, res) => {
12501
+ const { mode } = req.body;
12502
+ setLocalState({ pipelineMode: mode });
12503
+ console.log(`[pipeline] mode set to: ${mode}`);
12504
+ res.send({ code: successCode, data: { mode } });
12505
+ },
12506
+ };
12507
+ const gamePipelineModeGetRoute = {
12508
+ method: 'get',
12509
+ path: '/game/pipeline-mode',
12510
+ handler: (_req, res) => {
12511
+ const { pipelineMode } = getLocalState();
12512
+ res.send({ code: successCode, data: { mode: pipelineMode } });
12513
+ },
12514
+ };
12515
+
11804
12516
  const routes = [
11805
12517
  gameAssetPreviewUrlRoute,
11806
12518
  gameAssetsRoute,
@@ -11829,6 +12541,9 @@ const routes = [
11829
12541
  gameWasmSplitDownloadRoute,
11830
12542
  gameWasmCancelRoute,
11831
12543
  gameWasmSplitResetRoute,
12544
+ gamePipelineModeRoute,
12545
+ gamePipelineModeGetRoute,
12546
+ gameLanguageRoute,
11832
12547
  ];
11833
12548
  function registerRoutes(app, options) {
11834
12549
  const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
@@ -11852,6 +12567,14 @@ async function start() {
11852
12567
  const { url, version } = await listen(app, { maxRetries: 20 });
11853
12568
  console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(t('native.server.readyIn')), chalk.bold(`${Date.now() - startTime}ms`));
11854
12569
  showTips({ server: url });
12570
+ // 联调场景下(root `dev:debug` 脚本)我们用 vite dev server 提供 IDE,
12571
+ // 不需要 CLI 自己再开浏览器到 dist/public 里那份打包后的旧 IDE。设置
12572
+ // `TTMG_DEV_NO_OPEN=1` 抑制 openUrl,由调度脚本统一负责开 5173。
12573
+ // 普通用户场景(npm 安装 ttmg)不会设置这个 env var,行为不变。
12574
+ if (process.env.TTMG_DEV_NO_OPEN === '1') {
12575
+ 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.`));
12576
+ return;
12577
+ }
11855
12578
  openUrl(url);
11856
12579
  }
11857
12580
 
@@ -12274,7 +12997,7 @@ async function upload({ clientKey, note = '--', dir, }) {
12274
12997
  }
12275
12998
  }
12276
12999
 
12277
- var version = "0.3.8-beta.3";
13000
+ var version = "0.3.9-beta.wasm.1";
12278
13001
  var pkg = {
12279
13002
  version: version};
12280
13003