@ttmg/cli 0.3.9-beta.wasm.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/dist/index.js +503 -142
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +1 -1
  5. package/dist/public/assets/Card-BMss5cuV.js +1 -0
  6. package/dist/public/assets/Detail-Lyas-t6F.js +1 -0
  7. package/dist/public/assets/Detail-Lyas-t6F.js.br +0 -0
  8. package/dist/public/assets/MonetizationMode-DI4fy3U7.js +1 -0
  9. package/dist/public/assets/MonetizationModeSummary-B6yEB26H.js +1 -0
  10. package/dist/public/assets/SectionHeader-3b39hv3n.js +1 -0
  11. package/dist/public/assets/Tag-B5gYnANu.js +1 -0
  12. package/dist/public/assets/arrow-left-B3swWB64.js +1 -0
  13. package/dist/public/assets/baseForm-Dl4zA6hU.css +1 -0
  14. package/dist/public/assets/baseForm-Dl4zA6hU.css.br +0 -0
  15. package/dist/public/assets/baseForm-Dsi8Ia0k.js +10 -0
  16. package/dist/public/assets/baseForm-Dsi8Ia0k.js.br +0 -0
  17. package/dist/public/assets/chevron-right-WZeuFw11.js +1 -0
  18. package/dist/public/assets/compass-CCsIev11.js +1 -0
  19. package/dist/public/assets/index-4NS2mMuQ.css +1 -0
  20. package/dist/public/assets/index-B-AeuNlL.css +1 -0
  21. package/dist/public/assets/index-B-AeuNlL.css.br +0 -0
  22. package/dist/public/assets/index-B3-gsm5-.js +1 -0
  23. package/dist/public/assets/index-B3L1GnMP.js +1 -0
  24. package/dist/public/assets/index-B3L1GnMP.js.br +0 -0
  25. package/dist/public/assets/{index-tOg_vZEc.js → index-B6NHbQwP.js} +1 -1
  26. package/dist/public/assets/index-BAud2cRu.css +1 -0
  27. package/dist/public/assets/index-BJV8-tz8.js +1 -0
  28. package/dist/public/assets/index-BOl1-Siv.css +1 -0
  29. package/dist/public/assets/index-BOl1-Siv.css.br +0 -0
  30. package/dist/public/assets/index-BqSAtEgq.js +1 -0
  31. package/dist/public/assets/index-ByX5RuFA.css +1 -0
  32. package/dist/public/assets/index-C-tTmNa4.css +1 -0
  33. package/dist/public/assets/{index-BUDAPXlB.js → index-C2NWNiPX.js} +1 -1
  34. package/dist/public/assets/index-C9HXjiVu.js +1 -0
  35. package/dist/public/assets/{index-f0PBkQd9.js → index-C9Un1hFP.js} +1 -1
  36. package/dist/public/assets/index-CFd8iglC.css +1 -0
  37. package/dist/public/assets/index-CFd8iglC.css.br +0 -0
  38. package/dist/public/assets/index-CV2u_S0E.js +1 -0
  39. package/dist/public/assets/index-CV2u_S0E.js.br +0 -0
  40. package/dist/public/assets/index-Cc1ilXmc.css +1 -0
  41. package/dist/public/assets/index-Ci-lqt1V.js +1 -0
  42. package/dist/public/assets/index-Ci-lqt1V.js.br +0 -0
  43. package/dist/public/assets/index-Cigxnpav.js +1 -0
  44. package/dist/public/assets/index-Cigxnpav.js.br +0 -0
  45. package/dist/public/assets/index-Crx61Qjc.css +1 -0
  46. package/dist/public/assets/index-D-GbEkoB.css +1 -0
  47. package/dist/public/assets/index-D0xEiy7C.js +1 -0
  48. package/dist/public/assets/index-D1oGAPRa.js +1 -0
  49. package/dist/public/assets/index-DeL2bgxo.js +1 -0
  50. package/dist/public/assets/{index-BpefkOoB.js → index-DjY3Igd6.js} +1 -1
  51. package/dist/public/assets/index-DpWpqPKC.js +1 -0
  52. package/dist/public/assets/index-DpWpqPKC.js.br +0 -0
  53. package/dist/public/assets/index-Dvg_oNs7.css +1 -0
  54. package/dist/public/assets/index-DxISN0Xc.js +14 -0
  55. package/dist/public/assets/index-DxISN0Xc.js.br +0 -0
  56. package/dist/public/assets/{index-BJQfbNPd.js → index-HYSh4-Ri.js} +1 -1
  57. package/dist/public/assets/index-JNUqDBWt.js +1 -0
  58. package/dist/public/assets/{index-CgAbOvxk.css → index-ROKxx4f7.css} +1 -1
  59. package/dist/public/assets/index-ROKxx4f7.css.br +0 -0
  60. package/dist/public/assets/index-RYY-l6Oq.css +1 -0
  61. package/dist/public/assets/index-aqIfJUqW.js +1 -0
  62. package/dist/public/assets/index-faRHENEQ.css +1 -0
  63. package/dist/public/assets/{index-Ba1XAQLw.js → index-uquhwGkB.js} +1 -1
  64. package/dist/public/assets/sparkles-D2QPhLAu.js +1 -0
  65. package/dist/public/assets/zap-Cv4X8yRx.js +1 -0
  66. package/dist/public/index.html +11 -8
  67. package/package.json +1 -1
  68. package/dist/public/assets/Detail-CqXvsvPM.js +0 -1
  69. package/dist/public/assets/Detail-CqXvsvPM.js.br +0 -0
  70. package/dist/public/assets/MonetizationMode-2bp9UZWM.js +0 -1
  71. package/dist/public/assets/MonetizationModeSummary-DBtIQq9Z.js +0 -1
  72. package/dist/public/assets/baseForm-BFkpAlns.js +0 -10
  73. package/dist/public/assets/baseForm-BFkpAlns.js.br +0 -0
  74. package/dist/public/assets/baseForm-CB6KCNqW.css +0 -1
  75. package/dist/public/assets/baseForm-CB6KCNqW.css.br +0 -0
  76. package/dist/public/assets/index-88dZ53Te.css +0 -1
  77. package/dist/public/assets/index-88dZ53Te.css.br +0 -0
  78. package/dist/public/assets/index-B6ZeUJlM.js +0 -1
  79. package/dist/public/assets/index-BFePHKNX.js +0 -1
  80. package/dist/public/assets/index-BFePHKNX.js.br +0 -0
  81. package/dist/public/assets/index-BPzYPKPA.js +0 -1
  82. package/dist/public/assets/index-BRFS2ZhY.css +0 -1
  83. package/dist/public/assets/index-BRFS2ZhY.css.br +0 -0
  84. package/dist/public/assets/index-BYgShzrj.js +0 -1
  85. package/dist/public/assets/index-Bf6aJOeV.css +0 -1
  86. package/dist/public/assets/index-Bf85t01Q.css +0 -1
  87. package/dist/public/assets/index-Bf85t01Q.css.br +0 -0
  88. package/dist/public/assets/index-BnU4EHWL.css +0 -1
  89. package/dist/public/assets/index-BnU4EHWL.css.br +0 -0
  90. package/dist/public/assets/index-BvxhyFWU.js +0 -14
  91. package/dist/public/assets/index-BvxhyFWU.js.br +0 -0
  92. package/dist/public/assets/index-BwbPFgZF.css +0 -1
  93. package/dist/public/assets/index-C06KDNuj.css +0 -1
  94. package/dist/public/assets/index-CH7igbHY.css +0 -1
  95. package/dist/public/assets/index-CL2qDQto.js +0 -1
  96. package/dist/public/assets/index-CLgcHgzd.css +0 -1
  97. package/dist/public/assets/index-CMUKwrEm.js +0 -1
  98. package/dist/public/assets/index-CRAXXzpR.js +0 -1
  99. package/dist/public/assets/index-CRAXXzpR.js.br +0 -0
  100. package/dist/public/assets/index-CgAbOvxk.css.br +0 -0
  101. package/dist/public/assets/index-DNsJSSmy.css +0 -1
  102. package/dist/public/assets/index-DY13Lo-E.js +0 -1
  103. package/dist/public/assets/index-DY13Lo-E.js.br +0 -0
  104. package/dist/public/assets/index-DqFmR7Qk.css +0 -1
  105. package/dist/public/assets/index-FFfimhRp.js +0 -1
  106. package/dist/public/assets/index-FFfimhRp.js.br +0 -0
  107. package/dist/public/assets/index-OLBa9viz.js +0 -1
  108. package/dist/public/assets/index-OsVhWD4K.js +0 -1
  109. package/dist/public/assets/index-lKrpiZfQ.js +0 -1
  110. package/dist/public/assets/index-lKrpiZfQ.js.br +0 -0
  111. package/dist/public/assets/times-C1GmugF6.js +0 -1
  112. package/dist/public/assets/times-C7C5ulLg.css +0 -1
package/dist/index.js CHANGED
@@ -6588,9 +6588,30 @@ function buildCookieHeaderFromSetCookies(setCookies) {
6588
6588
  return sanitizeCookieHeader(setCookies.join('; '));
6589
6589
  }
6590
6590
 
6591
+ /**
6592
+ * Verbose logging helpers.
6593
+ *
6594
+ * The CLI registers a global `--verbose` flag (see `src/index.ts`). Detailed
6595
+ * diagnostic logs — HTTP request/response bodies, the local wasm split
6596
+ * pipeline's `[wasmtool]` / `[wasm-split]` / `[download]` breadcrumbs — should
6597
+ * only surface when the user explicitly opts in, so the default `ttmg dev`
6598
+ * output stays readable.
6599
+ *
6600
+ * Genuine failures must keep using `console.error` directly so they are always
6601
+ * visible regardless of `--verbose`.
6602
+ */
6591
6603
  function isVerboseEnabled() {
6592
6604
  return process.argv.includes('--verbose');
6593
6605
  }
6606
+ function verboseLog(...args) {
6607
+ if (isVerboseEnabled())
6608
+ console.log(...args);
6609
+ }
6610
+ function verboseWarn(...args) {
6611
+ if (isVerboseEnabled())
6612
+ console.warn(...args);
6613
+ }
6614
+
6594
6615
  /**
6595
6616
  * PPE / 测试环境开关。
6596
6617
  *
@@ -6749,7 +6770,7 @@ function printApiResponseLog(title, result) {
6749
6770
  ['Return Value', result.data],
6750
6771
  ]);
6751
6772
  }
6752
- async function request({ url, method, data, headers, params, }) {
6773
+ async function request({ url, method, data, headers, params, logRequestBody = true, }) {
6753
6774
  const config = getTTMGRC();
6754
6775
  const cookie = sanitizeCookieHeader(config?.cookie);
6755
6776
  const proxyConfig = getAxiosProxyConfig();
@@ -6757,7 +6778,9 @@ async function request({ url, method, data, headers, params, }) {
6757
6778
  printApiRequestLog({
6758
6779
  url,
6759
6780
  method,
6760
- params: getRequestParams(params, data),
6781
+ params: logRequestBody
6782
+ ? getRequestParams(params, data)
6783
+ : '[omitted: large request body]',
6761
6784
  });
6762
6785
  }
6763
6786
  try {
@@ -6821,7 +6844,7 @@ async function download(url, filePath) {
6821
6844
  catch { }
6822
6845
  }
6823
6846
  const proxyConfig = getAxiosProxyConfig();
6824
- console.log('[download] start', { url: url.slice(0, 120), filePath, hasProxy: !!proxyConfig.httpsAgent });
6847
+ verboseLog('[download] start', { url: url.slice(0, 120), filePath, hasProxy: !!proxyConfig.httpsAgent });
6825
6848
  try {
6826
6849
  const res = await axios.get(url, {
6827
6850
  responseType: 'stream',
@@ -6857,7 +6880,7 @@ async function download(url, filePath) {
6857
6880
  };
6858
6881
  const onClose = () => {
6859
6882
  cleanup();
6860
- console.log(`[download] done: ${received} bytes in ${Date.now() - startedAt}ms`);
6883
+ verboseLog(`[download] done: ${received} bytes in ${Date.now() - startedAt}ms`);
6861
6884
  resolve();
6862
6885
  };
6863
6886
  const cleanup = () => {
@@ -6875,7 +6898,7 @@ async function download(url, filePath) {
6875
6898
  const pct = Math.floor((received / total) * 10) * 10;
6876
6899
  if (pct !== lastLoggedPct) {
6877
6900
  lastLoggedPct = pct;
6878
- console.log(`[download] ${pct}% (${received}/${total})`);
6901
+ verboseLog(`[download] ${pct}% (${received}/${total})`);
6879
6902
  }
6880
6903
  }
6881
6904
  };
@@ -6889,7 +6912,7 @@ async function download(url, filePath) {
6889
6912
  return { ok: true };
6890
6913
  }
6891
6914
  catch (err) {
6892
- console.log('[download] failed:', err?.message);
6915
+ verboseLog('[download] failed:', err?.message);
6893
6916
  if (isAxiosError(err) && err.response?.status === 403) {
6894
6917
  throw new Error('下载链接已过期,请重新进行分包后重试');
6895
6918
  }
@@ -8349,8 +8372,81 @@ async function compile(context) {
8349
8372
  }
8350
8373
 
8351
8374
  // import { uploadGame } from './uploadGame';
8375
+ /* ---------------------------------------------------------------------------
8376
+ * Watch suspension control
8377
+ *
8378
+ * The wasm-split pipeline (prepare / split download / rollback) intentionally
8379
+ * rewrites a burst of project files — the instrumented wasm, the `wasmcode*`
8380
+ * subpackage dirs, `game.json`, `webgl-wasm-split.js`. Without gating, every
8381
+ * one of those writes trips the watcher below, and each trip runs a full
8382
+ * `compile` + device `resourceChange` reload *in the middle of the pipeline*.
8383
+ * That both wastes cycles and can reload the device onto a half-written /
8384
+ * transient build. We suspend the watcher around those operations and fire
8385
+ * exactly one update after they complete ("完成之后再触发更新").
8386
+ *
8387
+ * `suspendDepth` is a refcount so overlapping / nested operations are safe.
8388
+ * `muteUntil` keeps swallowing events for a short cooldown AFTER resume:
8389
+ * chokidar's `awaitWriteFinish` delays events ~1s past the actual write, so
8390
+ * those late events from our own writes would otherwise land after we've
8391
+ * resumed and re-trigger the very reload we just suppressed.
8392
+ * ------------------------------------------------------------------------- */
8393
+ let suspendDepth = 0;
8394
+ let muteUntil = 0;
8395
+ let pendingWhileSuspended = false;
8396
+ // chokidar awaitWriteFinish stabilityThreshold (1000ms) + headroom, so the
8397
+ // delayed FS events caused by the pipeline's own writes are swallowed.
8398
+ const RESUME_COOLDOWN_MS = 2500;
8399
+ // Wired up inside `watch()` so suspend/resume can reuse the exact same
8400
+ // (debounced) compile + resourceChange action the watcher fires normally.
8401
+ let triggerUpdate = null;
8402
+ function suspendWatch() {
8403
+ suspendDepth += 1;
8404
+ }
8405
+ function resumeWatch(options = {}) {
8406
+ if (suspendDepth > 0) {
8407
+ suspendDepth -= 1;
8408
+ }
8409
+ if (suspendDepth > 0) {
8410
+ return;
8411
+ }
8412
+ // Back to depth 0 — start the cooldown that swallows our own delayed FS
8413
+ // events, then emit a single update if anything changed while suspended
8414
+ // (or the caller forces it).
8415
+ muteUntil = Date.now() + RESUME_COOLDOWN_MS;
8416
+ const shouldEmit = options.emit ?? pendingWhileSuspended;
8417
+ pendingWhileSuspended = false;
8418
+ if (shouldEmit) {
8419
+ triggerUpdate?.();
8420
+ }
8421
+ }
8422
+ /**
8423
+ * Run a project-file-mutating operation with the watcher muted, then fire a
8424
+ * single update once it finishes. Use this around the wasm-split pipeline
8425
+ * steps so their intermediate writes don't each reload the device.
8426
+ */
8427
+ async function withWatchSuspended(fn, options = {}) {
8428
+ suspendWatch();
8429
+ try {
8430
+ return await fn();
8431
+ }
8432
+ finally {
8433
+ resumeWatch({ emit: options.emitOnFinish ?? true });
8434
+ }
8435
+ }
8352
8436
  async function watch() {
8353
8437
  let debounceTimer = null;
8438
+ const runUpdate = async () => {
8439
+ await compile({ mode: 'watch' });
8440
+ wsServer.sendResourceChange();
8441
+ };
8442
+ triggerUpdate = () => {
8443
+ if (debounceTimer)
8444
+ clearTimeout(debounceTimer);
8445
+ debounceTimer = setTimeout(() => {
8446
+ runUpdate();
8447
+ debounceTimer = null;
8448
+ }, 2000);
8449
+ };
8354
8450
  // 监听当前工作目录,排除 node_modules 和 .git
8355
8451
  const watcher = chokidar.watch(process.cwd(), {
8356
8452
  /**
@@ -8365,17 +8461,20 @@ async function watch() {
8365
8461
  });
8366
8462
  // 任意文件变化都触发
8367
8463
  watcher.on('all', (event, path) => {
8368
- // 清除之前的定时器
8369
- if (debounceTimer)
8370
- clearTimeout(debounceTimer);
8371
- // 重新设置定时器
8372
- debounceTimer = setTimeout(async () => {
8373
- (await compile({
8374
- mode: 'watch',
8375
- }),
8376
- wsServer.sendResourceChange());
8377
- debounceTimer = null;
8378
- }, 2000);
8464
+ // Pipeline is actively rewriting project files. Remember that something
8465
+ // changed so the resume flush can fire one update, and skip the per-write
8466
+ // compile/reload.
8467
+ if (suspendDepth > 0) {
8468
+ pendingWhileSuspended = true;
8469
+ return;
8470
+ }
8471
+ // Cooldown window right after a pipeline finished: these are the delayed
8472
+ // `awaitWriteFinish` events from the pipeline's own writes. We already
8473
+ // emitted one update on resume, so swallow them to avoid a double reload.
8474
+ if (Date.now() < muteUntil) {
8475
+ return;
8476
+ }
8477
+ triggerUpdate?.();
8379
8478
  });
8380
8479
  watcher.on('error', error => {
8381
8480
  // console.error(chalk.red('[watch] 监听发生错误:'), error);
@@ -9332,6 +9431,68 @@ const gameCheckRoute = {
9332
9431
  };
9333
9432
 
9334
9433
  const changelog = [
9434
+ {
9435
+ title: '0.3.9',
9436
+ target: {
9437
+ iOS: '>=43.1',
9438
+ Android: '>=43.1',
9439
+ },
9440
+ changes: {
9441
+ optimize: [
9442
+ {
9443
+ desc: {
9444
+ 'zh-CN': '调试 IDE 默认使用浅色主题,暗色模式改为手动开启并记忆选择,避免跟随系统强制进入暗色导致不适应',
9445
+ 'en-US': 'The debugging IDE now defaults to the light theme. Dark mode is opt-in and your choice is remembered, so it no longer follows the system and forces an unexpected dark UI.',
9446
+ },
9447
+ module: 'new',
9448
+ },
9449
+ {
9450
+ desc: {
9451
+ 'zh-CN': '优化暗色模式下二维码显示,调整背景避免过亮刺眼,同时保证扫码识别率',
9452
+ 'en-US': 'Improve the QR code in dark mode by toning down the background so it is no longer glaring while staying easy to scan.',
9453
+ },
9454
+ module: 'scanQrcode',
9455
+ },
9456
+ {
9457
+ desc: {
9458
+ 'zh-CN': 'Wasm 分包页面及相关组件补齐多语言,修复页面文案中英文混排的问题',
9459
+ 'en-US': 'Complete localization for the Wasm Code Split page and its components, fixing the mixed Chinese/English text.',
9460
+ },
9461
+ module: 'check',
9462
+ },
9463
+ {
9464
+ desc: {
9465
+ 'zh-CN': '能力接入助手页面视觉升级:变现模式卡片新增图标与强调色,关键能力标签按「必需 / 可选 / 实验」分级展示,信息层级更清晰',
9466
+ 'en-US': 'Refresh the Capability Integration Assistant: monetization-mode cards get icons and accent colors, and capability tags are grouped as Required / Optional / Experimental for a clearer hierarchy.',
9467
+ },
9468
+ module: 'new',
9469
+ },
9470
+ {
9471
+ desc: {
9472
+ 'zh-CN': '完成扫码连接后,首页标题与说明文案会切换为「已连接」状态,状态反馈更明确',
9473
+ 'en-US': 'After scanning to connect, the home page title and subtitle switch to a "connected" state for clearer feedback.',
9474
+ },
9475
+ module: 'scanQrcode',
9476
+ },
9477
+ {
9478
+ desc: {
9479
+ 'zh-CN': '新版本提示由红点改为「有更新」文字标签,含义更直观;上传游戏包按钮配色与产品主题色统一',
9480
+ 'en-US': 'Replace the ambiguous red dot for new versions with an explicit "Update available" label, and align the upload button color with the product theme.',
9481
+ },
9482
+ module: 'new',
9483
+ },
9484
+ ],
9485
+ bugfix: [
9486
+ {
9487
+ desc: {
9488
+ 'zh-CN': '修复多个页面中区块标题与正文内容未对齐的问题,整体排版更整齐',
9489
+ 'en-US': 'Fix section titles that were misaligned with their body content across multiple pages for a tidier layout.',
9490
+ },
9491
+ module: 'new',
9492
+ },
9493
+ ],
9494
+ },
9495
+ },
9335
9496
  {
9336
9497
  title: '0.3.8',
9337
9498
  target: {
@@ -10405,42 +10566,83 @@ function restoreSplitConfigFromCache(entryDir = process.cwd()) {
10405
10566
  * to change.
10406
10567
  */
10407
10568
  function restoreFromCache(entryDir = process.cwd()) {
10569
+ // Rollback rewrites a burst of project files (restored wasm, game.json,
10570
+ // split config, removed split-output dirs). Mute the dev-server watcher for
10571
+ // the duration so it doesn't fire a compile + device reload per file, then
10572
+ // emit a single update once the project is back to its restored state.
10573
+ suspendWatch();
10574
+ try {
10575
+ restoreFromCacheInner(entryDir);
10576
+ }
10577
+ finally {
10578
+ resumeWatch({ emit: true });
10579
+ }
10580
+ }
10581
+ function restoreFromCacheInner(entryDir) {
10408
10582
  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
10583
  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 });
10584
+ // Resolve the cached original wasm BEFORE touching anything on disk. The
10585
+ // previous order (wipe `wasmcode/*.br` first, then look for a cached
10586
+ // original to restore) was destructive when the cache had no original to
10587
+ // give back e.g. the project was duplicated without `__TTMG_TEMP__`, the
10588
+ // temp dir was cleared, or split completed in a session whose cache is gone.
10589
+ // In that case it deleted the project's only wasm and restored nothing,
10590
+ // leaving `wasmcode/` empty while `game.json` still pointed at the now-missing
10591
+ // `<md5>.wasm.br`. The next prepare then crashed the dev server on an ENOENT
10592
+ // decompress. We only clean + restore when we have a confirmed replacement.
10593
+ const cachedWasmBr = fs.existsSync(cacheDir)
10594
+ ? fs.readdirSync(cacheDir).find(item => item.endsWith('.br'))
10595
+ : undefined;
10596
+ if (cachedWasmBr) {
10597
+ // Safe to wipe split residue first: we have a confirmed original to put
10598
+ // back. Order matters — restoring first then wiping would delete the file
10599
+ // we just brought back.
10600
+ if (fs.existsSync(originDir)) {
10601
+ for (const entry of fs.readdirSync(originDir)) {
10602
+ // Only clean files split is known to write — `.br` (main wasm) and
10603
+ // the empty `game.js` placeholder. Touching anything else risks
10604
+ // nuking developer-authored content that happens to live in
10605
+ // wasmcode/ for unrelated reasons.
10606
+ if (entry.endsWith('.br') || entry === 'game.js') {
10607
+ fs.rmSync(path.join(originDir, entry), { force: true });
10608
+ }
10421
10609
  }
10422
10610
  }
10611
+ const destWasmBrPath = path.join(originDir, path.basename(cachedWasmBr));
10612
+ ensureDirSync(path.dirname(destWasmBrPath));
10613
+ fs.copyFileSync(path.join(cacheDir, cachedWasmBr), destWasmBrPath);
10423
10614
  }
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.
10615
+ else {
10616
+ // No cached original. Do NOT delete whatever wasm is currently on disk —
10617
+ // it is all the project has left. Preserve it so the project can still
10618
+ // boot and a fresh prepare can re-seed the cache, rather than wiping it and
10619
+ // crashing the next prepare. The split-output dirs below are still cleaned.
10620
+ console.warn('[wasmtool] restoreFromCache: no cached original wasm found in ' +
10621
+ `${WASM_SPLIT_CACHE_DIR}; skipping wasmcode/*.br cleanup to avoid ` +
10622
+ 'leaving the project without a wasm binary.');
10623
+ }
10624
+ // Only roll back the split config + game.json when we actually restored the
10625
+ // original wasm above. Restoring game.json (which points at the original
10626
+ // `<md5>.wasm.br`) while the matching wasm is NOT on disk recreates exactly
10627
+ // the broken state this guard exists to prevent — a game.json referencing a
10628
+ // file that isn't there. keepCacheSync writes the wasm, config and game.json
10629
+ // together, so a cache that has game.json but no `.br` is partial/corrupt and
10630
+ // must not be applied.
10631
+ if (cachedWasmBr) {
10632
+ const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
10633
+ if (fs.existsSync(splitConfigCachePath)) {
10634
+ fs.copyFileSync(splitConfigCachePath, path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME));
10635
+ }
10636
+ const gameJsonCachePath = path.join(cacheDir, 'game.json');
10637
+ if (fs.existsSync(gameJsonCachePath)) {
10638
+ fs.copyFileSync(gameJsonCachePath, path.join(entryDir, 'game.json'));
10639
+ }
10640
+ }
10641
+ // Restore wasmcode/game.js. We just deleted whatever was there in the
10642
+ // cleanup loop above, so we always need to put something back when wasmcode
10643
+ // is a subpackage. Scoped to the same `cachedWasmBr` guard: in the degraded
10644
+ // (lost-cache) path we left the existing wasmcode/ untouched, so we must not
10645
+ // rewrite its game.js either.
10444
10646
  // Strategy:
10445
10647
  // - Prefer the cache (keepCacheSync stashes pre-split contents to
10446
10648
  // `__unity_cache__/wasmcode-game.js` when the original existed)
@@ -10451,14 +10653,16 @@ function restoreFromCache(entryDir = process.cwd()) {
10451
10653
  // downloadSplited.ts also writes; it satisfies the platform requirement
10452
10654
  // without changing semantics for projects that don't use wasmcode as
10453
10655
  // 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');
10656
+ if (cachedWasmBr) {
10657
+ const originGameJsCachePath = path.join(cacheDir, 'wasmcode-game.js');
10658
+ const originGameJsDestPath = path.join(originDir, 'game.js');
10659
+ if (fs.existsSync(originDir)) {
10660
+ if (fs.existsSync(originGameJsCachePath)) {
10661
+ fs.copyFileSync(originGameJsCachePath, originGameJsDestPath);
10662
+ }
10663
+ else {
10664
+ fs.writeFileSync(originGameJsDestPath, '', 'utf-8');
10665
+ }
10462
10666
  }
10463
10667
  }
10464
10668
  for (const subDir of SPLIT_OUTPUT_DIRS) {
@@ -10576,21 +10780,53 @@ function setLocalState(partial) {
10576
10780
  Object.assign(state, partial);
10577
10781
  }
10578
10782
 
10783
+ /**
10784
+ * Prepare overwrites the project wasm in place with the instrumented build.
10785
+ * Mute the dev-server file watcher for the duration so the in-place rewrite
10786
+ * doesn't fire a compile + device reload while the file is still being
10787
+ * written, then emit a single update once the instrumented wasm is on disk.
10788
+ */
10579
10789
  async function startPrepare$1(params) {
10790
+ return withWatchSuspended(() => startPrepareImpl(params), {
10791
+ emitOnFinish: true,
10792
+ });
10793
+ }
10794
+ async function startPrepareImpl(params) {
10580
10795
  const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
10581
10796
  ensureDirSync(tempDir);
10582
10797
  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
- }
10798
+ const rawWasmPath = path$1.join(tempDir, 'original.wasm');
10590
10799
  const preparedWasmPath = path$1.join(tempDir, 'prepared.wasm');
10591
10800
  try {
10801
+ // The project wasm referenced by game.json can legitimately be missing on
10802
+ // disk: a previous rollback may have wiped `wasmcode/*.br` without a cached
10803
+ // original to restore, the project may have been duplicated without
10804
+ // `__TTMG_TEMP__`, or Unity was re-exported. Detect it up front and return
10805
+ // a structured error. Previously the `decompressWasmFile` / `copyFileSync`
10806
+ // below sat OUTSIDE this try block, so a missing input threw ENOENT out of
10807
+ // the route handler as an unhandled rejection and crashed the whole
10808
+ // dev-server process instead of surfacing a recoverable error to the IDE.
10809
+ if (!fs$1.existsSync(inputPath)) {
10810
+ return {
10811
+ data: null,
10812
+ error: {
10813
+ code: 404,
10814
+ message: `WASM file not found: ${params.wasm_file_path}. ` +
10815
+ 'The original wasm is missing — a previous "放弃分包" may have removed it ' +
10816
+ 'without a cached original to restore. Re-export the Unity build (or restore ' +
10817
+ 'the original wasm) and try preparing again.',
10818
+ },
10819
+ ctx: { logid: 'local', httpStatusCode: 404 },
10820
+ };
10821
+ }
10822
+ if (inputPath.endsWith('.br')) {
10823
+ await decompressWasmFile(inputPath, rawWasmPath);
10824
+ }
10825
+ else {
10826
+ fs$1.copyFileSync(inputPath, rawWasmPath);
10827
+ }
10592
10828
  const result = ttmgWasmtool.prepare(rawWasmPath, preparedWasmPath);
10593
- console.log(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
10829
+ verboseLog(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
10594
10830
  const gameJson = getGameJson();
10595
10831
  const totalWasmFuncCount = gameJson.wasmFuncCount ?? 0;
10596
10832
  const wasmSize = fs$1.existsSync(inputPath)
@@ -10624,13 +10860,13 @@ async function startPrepare$1(params) {
10624
10860
  const preparedMd5 = fs$1.existsSync(preparedWasmPath)
10625
10861
  ? crypto$1.createHash('md5').update(fs$1.readFileSync(preparedWasmPath)).digest('hex')
10626
10862
  : '<missing>';
10627
- console.log(`[wasmtool] prepare sanity: raw(size=${rawSize} md5=${rawMd5}) -> prepared(size=${preparedSize} md5=${preparedMd5}) delta=${preparedSize - rawSize}`);
10863
+ verboseLog(`[wasmtool] prepare sanity: raw(size=${rawSize} md5=${rawMd5}) -> prepared(size=${preparedSize} md5=${preparedMd5}) delta=${preparedSize - rawSize}`);
10628
10864
  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.');
10865
+ verboseWarn('[wasmtool] WARNING: prepared wasm is not larger / md5 is unchanged vs raw wasm. Instrumentation likely did not happen.');
10630
10866
  }
10631
- console.log('[wasmtool] compressing prepared wasm (quality=9)...');
10867
+ verboseLog('[wasmtool] compressing prepared wasm (quality=9)...');
10632
10868
  await compressWasmFile(preparedWasmPath, willReplaceWasmPath);
10633
- console.log('[wasmtool] compressed and written to project');
10869
+ verboseLog('[wasmtool] compressed and written to project');
10634
10870
  // Diagnostic: confirm the file the client actually fetches was overwritten,
10635
10871
  // and compare to the cached original brotli so we can prove on-disk replacement.
10636
10872
  const replacedSize = fs$1.existsSync(willReplaceWasmPath)
@@ -10649,12 +10885,12 @@ async function startPrepare$1(params) {
10649
10885
  .update(fs$1.readFileSync(cachedOriginalBr))
10650
10886
  .digest('hex')
10651
10887
  : '<missing>';
10652
- console.log(`[wasmtool] on-disk replace check: project=${params.wasm_file_path} size=${replacedSize} md5=${replacedMd5} | cached-original size=${cachedOriginalSize} md5=${cachedOriginalMd5}`);
10888
+ verboseLog(`[wasmtool] on-disk replace check: project=${params.wasm_file_path} size=${replacedSize} md5=${replacedMd5} | cached-original size=${cachedOriginalSize} md5=${cachedOriginalMd5}`);
10653
10889
  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.');
10890
+ verboseWarn('[wasmtool] WARNING: project wasm md5 matches cached-original md5. The file was not actually replaced with the instrumented build.');
10655
10891
  }
10656
10892
  else {
10657
- console.log('[wasmtool] OK: project wasm differs from cached-original — instrumented wasm is on disk.');
10893
+ verboseLog('[wasmtool] OK: project wasm differs from cached-original — instrumented wasm is on disk.');
10658
10894
  }
10659
10895
  // Local pipeline uses the new wasm-collect/v1/report API + archive sub-wasm.
10660
10896
  // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
@@ -10664,7 +10900,7 @@ async function startPrepare$1(params) {
10664
10900
  ENABLEARCHIVEMODE: true,
10665
10901
  ORIGINALWASMMD5: params.wasm_md5,
10666
10902
  });
10667
- console.log('[wasmtool] wasm split config updated (local pipeline: archive=true)');
10903
+ verboseLog('[wasmtool] wasm split config updated (local pipeline: archive=true)');
10668
10904
  // Disk-persisted anchor for "wasm drift" detection in
10669
10905
  // `game-wasm-split-config` route. Stores the md5 that prepare just
10670
10906
  // wrote into the project alongside the project-relative path. The
@@ -10678,7 +10914,7 @@ async function startPrepare$1(params) {
10678
10914
  preparedWasmMd5: replacedMd5,
10679
10915
  codePath: params.wasm_file_path,
10680
10916
  });
10681
- console.log(`[wasmtool] prepared-meta written: md5=${replacedMd5} codePath=${params.wasm_file_path}`);
10917
+ verboseLog(`[wasmtool] prepared-meta written: md5=${replacedMd5} codePath=${params.wasm_file_path}`);
10682
10918
  return {
10683
10919
  data: {
10684
10920
  code: 0,
@@ -10787,42 +11023,30 @@ async function startWasmSession({ client_key, wasm_md5, reset, }) {
10787
11023
  }
10788
11024
 
10789
11025
  /**
10790
- * "开始收集" 的本地 pipeline 实现。语义上等价于老远程流程的
10791
- * `stark_wasm/v4/post/set_collecting`:**打开 server 端的 collect 窗口**,
10792
- * 让 plugin 之后的 `/report` 请求能落库。
11026
+ * 打开 server 端 collect session(本地 pipeline 的 `/start`)。
10793
11027
  *
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}`。
11028
+ * 这是**鉴权门**:`/start` 走 Portal 鉴权中间件,登录态失效会返回 `-401`
11029
+ * (或带登录关键字的 `-1`)。鉴权失败必须立即把错误回给 IDE 并中止——
11030
+ * 否则用户进了"正在收集",但 plugin 之后所有 `/report` 都会被 fail-close
11031
+ * 丢弃,函数数量永远 0,且很难归因。
10801
11032
  *
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
- * 显式传,避免误清。
11033
+ * IDE 通过独立路由 `/game/wasm-collect-start` 调用本函数,所以它在浏览器
11034
+ * Network 里是一条可见且**明确叫 start** 的请求——出鉴权问题时一眼能定位到
11035
+ * 是开 session 这步失败,而不是被误认为 collect 轮询接口的问题。symbols
11036
+ * 上传是另一步(`uploadCollectSymbols`),与开 session 解耦。
10810
11037
  *
10811
- * Session 生命周期对前端透明——IDE 只知道"开始收集 / 完成收集"两个动作,
10812
- * `/start` `/finish` 都被封在本地 dispatcher 内。这样远程 pipeline(没有
10813
- * session 概念)和本地 pipeline(有 session)在 IDE 层看起来是对称的。
11038
+ * 两种语义(与 `wasm_api.md` §5.1 对齐):
11039
+ * - 默认(`resume` 缺省 / false)→ `reset: true`,服务端清空历史。
11040
+ * - `resume: true` `reset: false`,幂等打开、保留已有 func_ids。
10814
11041
  */
10815
- async function setCollect$1({ client_key, wasm_md5, resume, }) {
11042
+ async function openCollectSession$1({ client_key, wasm_md5, resume, }) {
10816
11043
  const startRes = await startWasmSession({
10817
11044
  client_key,
10818
11045
  wasm_md5,
10819
11046
  reset: !resume,
10820
11047
  });
10821
11048
  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.
11049
+ // 结构化日志带 logid——找后端排查鉴权/会话问题时最有用的字段。
10826
11050
  const code = startRes.error?.code ?? startRes.data?.code ?? -1;
10827
11051
  const message = startRes.error?.message ||
10828
11052
  startRes.data?.message ||
@@ -10835,6 +11059,20 @@ async function setCollect$1({ client_key, wasm_md5, resume, }) {
10835
11059
  ctx: startRes.ctx,
10836
11060
  };
10837
11061
  }
11062
+ return {
11063
+ data: { code: 0, message: 'success', result: startRes.data.result },
11064
+ error: null,
11065
+ ctx: startRes.ctx,
11066
+ };
11067
+ }
11068
+ /**
11069
+ * 上传符号表(`/symbols`)。只在 session 已打开后调用。
11070
+ *
11071
+ * 符号表只是给 server 端后续调试用的 debug 信息,丢了不影响分包主链路,
11072
+ * 所以这里故意不 await、失败仅 warn,且总是返回 `{code: 0}`——它不应该
11073
+ * 阻塞或失败掉"开始收集"流程。
11074
+ */
11075
+ async function uploadCollectSymbols$1({ client_key, wasm_md5, }) {
10838
11076
  let symbolPath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
10839
11077
  if (!fs$1.existsSync(symbolPath)) {
10840
11078
  symbolPath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
@@ -10844,13 +11082,15 @@ async function setCollect$1({ client_key, wasm_md5, resume, }) {
10844
11082
  request({
10845
11083
  url: `${WASM_COLLECT_BASE_URL}/symbols`,
10846
11084
  method: 'POST',
11085
+ // symbols 是整张符号表,verbose 下打出来会刷屏,关掉它的入参日志。
11086
+ logRequestBody: false,
10847
11087
  data: {
10848
11088
  app_id: client_key,
10849
11089
  wasm_md5,
10850
11090
  symbols,
10851
11091
  },
10852
11092
  }).catch(err => {
10853
- console.warn('[wasmtool] Failed to upload symbols:', err);
11093
+ verboseWarn('[wasmtool] Failed to upload symbols:', err);
10854
11094
  });
10855
11095
  }
10856
11096
  return {
@@ -10963,7 +11203,7 @@ async function startSplit$1({ client_key, wasm_md5, }) {
10963
11203
  ctx: { logid: 'local', httpStatusCode: 400 },
10964
11204
  };
10965
11205
  }
10966
- console.log(`[wasmtool] splitting with ${funcIds.length} func IDs` +
11206
+ verboseLog(`[wasmtool] splitting with ${funcIds.length} func IDs` +
10967
11207
  (bootFuncIds.length > 0
10968
11208
  ? `, ${bootFuncIds.length} boot-phase func IDs (→ alwaysInclude)`
10969
11209
  : ', no boot-phase info (legacy server, falling back to callClosure only)') +
@@ -11023,14 +11263,14 @@ async function startSplit$1({ client_key, wasm_md5, }) {
11023
11263
  const actualArchivePath = fs$1.existsSync(archiveBrPath)
11024
11264
  ? archiveBrPath
11025
11265
  : result.archivePath;
11026
- console.log(`[wasmtool] archivePath=${result.archivePath}, brExists=${fs$1.existsSync(archiveBrPath)}, actualExists=${fs$1.existsSync(actualArchivePath)}`);
11266
+ verboseLog(`[wasmtool] archivePath=${result.archivePath}, brExists=${fs$1.existsSync(archiveBrPath)}, actualExists=${fs$1.existsSync(actualArchivePath)}`);
11027
11267
  if (fs$1.existsSync(actualArchivePath)) {
11028
11268
  archiveMd5 = computeFileMd5Sync(actualArchivePath);
11029
- console.log(`[wasmtool] archive_md5=${archiveMd5}`);
11269
+ verboseLog(`[wasmtool] archive_md5=${archiveMd5}`);
11030
11270
  }
11031
11271
  }
11032
11272
  else {
11033
- console.log(`[wasmtool] skip archive md5: archive=${archive}, archivePath=${result.archivePath}`);
11273
+ verboseLog(`[wasmtool] skip archive md5: archive=${archive}, archivePath=${result.archivePath}`);
11034
11274
  }
11035
11275
  const globalVarList = result.globalVarList
11036
11276
  .split(';')
@@ -11072,7 +11312,7 @@ async function startSplit$1({ client_key, wasm_md5, }) {
11072
11312
  export_added: result.exportAdded,
11073
11313
  };
11074
11314
  setLocalState({ splitOutputDir, splitMeta });
11075
- console.log(`[wasmtool] split done: total=${result.totalWasmCount}, main=${result.mainWasmCount} ` +
11315
+ verboseLog(`[wasmtool] split done: total=${result.totalWasmCount}, main=${result.mainWasmCount} ` +
11076
11316
  `(collect=${result.collectFuncCount}, +alwaysInclude=${result.alwaysIncludeAdded}, ` +
11077
11317
  `+closure=${result.closureAdded}, +indirectClosure=${result.indirectClosureAdded}` +
11078
11318
  `[types=${result.indirectClosureTypes}], +exports=${result.exportAdded}), ` +
@@ -11155,7 +11395,18 @@ function updateSubpackageConfigSync(archive = false) {
11155
11395
  fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11156
11396
  }
11157
11397
 
11158
- async function downloadSplited$1(_context) {
11398
+ /**
11399
+ * Lays down the split outputs (`wasmcode*` dirs, game.json, webgl-wasm-split.js)
11400
+ * into the project root — a burst of file writes. Mute the watcher across the
11401
+ * whole operation so it doesn't reload the device per file mid-write, then fire
11402
+ * one update once the split layout is fully on disk.
11403
+ */
11404
+ async function downloadSplited$1(context) {
11405
+ return withWatchSuspended(() => downloadSplitedImpl(context), {
11406
+ emitOnFinish: true,
11407
+ });
11408
+ }
11409
+ async function downloadSplitedImpl(_context) {
11159
11410
  const cwd = process.cwd();
11160
11411
  const { splitMeta } = getLocalState();
11161
11412
  if (!splitMeta) {
@@ -11177,7 +11428,7 @@ async function downloadSplited$1(_context) {
11177
11428
  const dirs = [...new Set([mainAndroidDir, subAndroidDir, mainIosDir, subIosDir])];
11178
11429
  dirs.forEach(ensureDirSync);
11179
11430
  try {
11180
- console.log('[wasmtool] organizing split output...');
11431
+ verboseLog('[wasmtool] organizing split output...');
11181
11432
  const mainWasmMd5 = splitMeta.main_wasm_md5;
11182
11433
  const subWasmMd5 = splitMeta.sub_wasm_md5;
11183
11434
  const mainWasmH5Md5 = splitMeta.main_wasm_h5_md5;
@@ -11220,20 +11471,20 @@ async function downloadSplited$1(_context) {
11220
11471
  if (isArchive && localArchivePath) {
11221
11472
  const archiveBrPath = localArchivePath + BR_SUFFIX;
11222
11473
  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}`);
11474
+ verboseLog(`[wasmtool] archive copy: archive_md5=${splitMeta.archive_md5}, localPath=${localArchivePath}, brExists=${fs.existsSync(archiveBrPath)}, actual=${actualArchivePath}`);
11224
11475
  if (fs.existsSync(actualArchivePath)) {
11225
11476
  const archiveMd5 = splitMeta.archive_md5 || '';
11226
11477
  const archiveBaseName = path.basename(actualArchivePath);
11227
11478
  const destName = archiveMd5 ? `${archiveMd5}.${archiveBaseName}` : archiveBaseName;
11228
11479
  const archiveDest = path.join(subIosDir, destName);
11229
- console.log(`[wasmtool] archive dest: ${archiveDest}`);
11480
+ verboseLog(`[wasmtool] archive dest: ${archiveDest}`);
11230
11481
  fs.copyFileSync(actualArchivePath, archiveDest);
11231
11482
  }
11232
11483
  }
11233
11484
  dirs.forEach((dir) => {
11234
11485
  fs.writeFileSync(path.join(dir, 'game.js'), '', { encoding: 'utf-8' });
11235
11486
  });
11236
- console.log('[wasmtool] copy split output to root...');
11487
+ verboseLog('[wasmtool] copy split output to root...');
11237
11488
  wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
11238
11489
  for (const file of fs.readdirSync(splitTempDir)) {
11239
11490
  const srcPath = path.join(splitTempDir, file);
@@ -11244,9 +11495,9 @@ async function downloadSplited$1(_context) {
11244
11495
  await promises.cp(srcPath, destPath, { recursive: true, force: true });
11245
11496
  }
11246
11497
  wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
11247
- console.log('[wasmtool] updating subpackage config...');
11498
+ verboseLog('[wasmtool] updating subpackage config...');
11248
11499
  updateSubpackageConfigSync(isArchive);
11249
- console.log('[wasmtool] updating wasm split config...');
11500
+ verboseLog('[wasmtool] updating wasm split config...');
11250
11501
  wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11251
11502
  updateWasmSplitConfig({
11252
11503
  ENABLEWASMCOLLECT: true,
@@ -11464,7 +11715,7 @@ async function downloadOne(opts) {
11464
11715
  const willDownloadedFileIsBr = url.includes(BR_SUFFIX);
11465
11716
  const finalOut = willDownloadedFileIsBr && !out.endsWith(BR_SUFFIX) ? out + BR_SUFFIX : out;
11466
11717
  wsServer.sendUnitySplitStatus({ status: startStatus });
11467
- console.log(`[remote-split-download] fetching -> ${finalOut}`);
11718
+ verboseLog(`[remote-split-download] fetching -> ${finalOut}`);
11468
11719
  const t0 = Date.now();
11469
11720
  await withRetry(() => download(url, finalOut), DOWNLOAD_RETRY);
11470
11721
  const st = await promises.stat(finalOut);
@@ -11472,13 +11723,23 @@ async function downloadOne(opts) {
11472
11723
  await promises.rm(finalOut, { force: true });
11473
11724
  throw new Error(`Empty download: ${finalOut}`);
11474
11725
  }
11475
- console.log(`[remote-split-download] done: ${path.basename(finalOut)} size=${st.size}B time=${Date.now() - t0}ms`);
11726
+ verboseLog(`[remote-split-download] done: ${path.basename(finalOut)} size=${st.size}B time=${Date.now() - t0}ms`);
11476
11727
  wsServer.sendUnitySplitStatus({ status: doneStatus, url });
11477
11728
  // Legacy behaviour: write an empty game.js next to each downloaded artifact
11478
11729
  // so the subpackage loader doesn't complain about missing js entries.
11479
11730
  fs.writeFileSync(path.join(path.dirname(out), 'game.js'), '', 'utf-8');
11480
11731
  }
11732
+ /**
11733
+ * Remote-mode counterpart of `downloadSplited`: downloads the server-built
11734
+ * split artifacts and copies them into the project root. Same watcher gating
11735
+ * rationale — mute across the burst of writes, emit one update at the end.
11736
+ */
11481
11737
  async function downloadSplitedRemote(context) {
11738
+ return withWatchSuspended(() => downloadSplitedRemoteImpl(context), {
11739
+ emitOnFinish: true,
11740
+ });
11741
+ }
11742
+ async function downloadSplitedRemoteImpl(context) {
11482
11743
  const cwd = process.cwd();
11483
11744
  const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11484
11745
  ensureDirSync(splitTempDir);
@@ -11492,7 +11753,7 @@ async function downloadSplitedRemote(context) {
11492
11753
  const mainIosOut = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
11493
11754
  const limit = pLimit(CONCURRENCY_LIMIT);
11494
11755
  try {
11495
- console.log('[remote-split-download] start', {
11756
+ verboseLog('[remote-split-download] start', {
11496
11757
  original_wasm_md5: context.original_wasm_md5,
11497
11758
  main_wasm_md5: context.main_wasm_md5,
11498
11759
  sub_wasm_md5: context.sub_wasm_md5,
@@ -11530,7 +11791,7 @@ async function downloadSplitedRemote(context) {
11530
11791
  out: path.join(subIosDir, 'subjs.data'),
11531
11792
  })),
11532
11793
  ]);
11533
- console.log('[remote-split-download] copying split output to project root...');
11794
+ verboseLog('[remote-split-download] copying split output to project root...');
11534
11795
  wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
11535
11796
  for (const file of fs.readdirSync(splitTempDir)) {
11536
11797
  const srcPath = path.join(splitTempDir, file);
@@ -11541,9 +11802,9 @@ async function downloadSplitedRemote(context) {
11541
11802
  await promises.cp(srcPath, destPath, { recursive: true, force: true });
11542
11803
  }
11543
11804
  wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
11544
- console.log('[remote-split-download] updating subpackage config...');
11805
+ verboseLog('[remote-split-download] updating subpackage config...');
11545
11806
  updateSubpackageConfigSync(false);
11546
- console.log('[remote-split-download] updating webgl-wasm-split.js...');
11807
+ verboseLog('[remote-split-download] updating webgl-wasm-split.js...');
11547
11808
  wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
11548
11809
  updateWasmSplitConfig({
11549
11810
  ENABLEWASMCOLLECT: true,
@@ -11560,12 +11821,12 @@ async function downloadSplitedRemote(context) {
11560
11821
  USINGWASMH5: Boolean(context.main_wasm_h5_md5),
11561
11822
  });
11562
11823
  wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11563
- console.log('[remote-split-download] all done');
11824
+ verboseLog('[remote-split-download] all done');
11564
11825
  return { data: { isSuccess: true }, ctx: context };
11565
11826
  }
11566
11827
  catch (err) {
11567
11828
  const message = err instanceof Error ? err.message : String(err);
11568
- console.log('[remote-split-download] failed:', message);
11829
+ verboseLog('[remote-split-download] failed:', message);
11569
11830
  wsServer.sendUnitySplitStatus({ status: 'wasm_split_failed', errorMsg: message });
11570
11831
  return {
11571
11832
  data: { isSuccess: false },
@@ -11890,7 +12151,7 @@ async function downloadPreparedRemote(data) {
11890
12151
  try {
11891
12152
  const downloadUrl = res?.data?.result?.download_url;
11892
12153
  if (!downloadUrl) {
11893
- console.log('[remote-download-prepared] no download_url in response');
12154
+ verboseLog('[remote-download-prepared] no download_url in response');
11894
12155
  return {
11895
12156
  isSuccess: false,
11896
12157
  error: { code: res.data?.code, message: res.data?.message || 'No download_url returned' },
@@ -11903,28 +12164,28 @@ async function downloadPreparedRemote(data) {
11903
12164
  originalWasmPath: data.wasm_path,
11904
12165
  originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
11905
12166
  });
11906
- console.log(`[remote-download-prepared] target=${willReplaceWasmPath}`);
12167
+ verboseLog(`[remote-download-prepared] target=${willReplaceWasmPath}`);
11907
12168
  if (downloadUrl.includes('.br')) {
11908
12169
  const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm.br');
11909
- console.log('[remote-download-prepared] downloading (br) ->', tempWasmPath);
12170
+ verboseLog('[remote-download-prepared] downloading (br) ->', tempWasmPath);
11910
12171
  wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
11911
12172
  const startedAt = Date.now();
11912
12173
  await download(downloadUrl, tempWasmPath);
11913
- console.log(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
12174
+ verboseLog(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
11914
12175
  fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
11915
12176
  wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
11916
12177
  }
11917
12178
  else {
11918
12179
  const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm');
11919
- console.log('[remote-download-prepared] downloading (raw) ->', tempWasmPath);
12180
+ verboseLog('[remote-download-prepared] downloading (raw) ->', tempWasmPath);
11920
12181
  wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
11921
12182
  const startedAt = Date.now();
11922
12183
  await download(downloadUrl, tempWasmPath);
11923
- console.log(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
12184
+ verboseLog(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
11924
12185
  wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
11925
12186
  wsServer.sendUnitySplitStatus({ status: 'start_compress_prepared_wasm' });
11926
12187
  await compressWasmFile(tempWasmPath, willReplaceWasmPath);
11927
- console.log('[remote-download-prepared] compressed and written to project');
12188
+ verboseLog('[remote-download-prepared] compressed and written to project');
11928
12189
  wsServer.sendUnitySplitStatus({ status: 'compress_prepared_wasm_done', url: downloadUrl });
11929
12190
  wsServer.sendUnitySplitStatus({ status: 'write_compress_prepared_wasm_done' });
11930
12191
  }
@@ -11942,11 +12203,11 @@ async function downloadPreparedRemote(data) {
11942
12203
  data.wasm_md5,
11943
12204
  });
11944
12205
  wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
11945
- console.log('[remote-download-prepared] split config updated, returning success');
12206
+ verboseLog('[remote-download-prepared] split config updated, returning success');
11946
12207
  return { isSuccess: true, ctx: res?.ctx };
11947
12208
  }
11948
12209
  catch (error) {
11949
- console.log('[remote-download-prepared] error:', error);
12210
+ verboseLog('[remote-download-prepared] error:', error);
11950
12211
  return {
11951
12212
  isSuccess: false,
11952
12213
  error: { code: res.data?.code, message: error instanceof Error ? error.message : String(error) },
@@ -11960,7 +12221,21 @@ function isLocal() {
11960
12221
  }
11961
12222
  const startPrepare = (params) => isLocal() ? startPrepare$1(params) : startPrepareRemote(params);
11962
12223
  const downloadPrepared = (params) => isLocal() ? downloadPrepared$1() : downloadPreparedRemote(params);
11963
- const setCollect = (params) => isLocal() ? setCollect$1(params) : setCollectRemote(params);
12224
+ // collect session(鉴权门)。IDE `/game/wasm-collect-start` 调用,
12225
+ // 所以它在浏览器 Network 里是一条独立可见、明确叫 start 的请求。
12226
+ // - 本地 → `/start`
12227
+ // - 远程 → 老的 `set_collecting`(远程没有独立 /start,set_collecting 即开窗)
12228
+ const openCollectSession = (params) => isLocal() ? openCollectSession$1(params) : setCollectRemote(params);
12229
+ // 上传符号表。本地走 `/symbols`(非阻塞、失败不致命);远程在 prepare 阶段
12230
+ // 已随表单上传过 symbol 文件,这里是 no-op,直接返回成功保持两条 pipeline
12231
+ // 在 IDE 层对称。
12232
+ const uploadCollectSymbols = (params) => isLocal()
12233
+ ? uploadCollectSymbols$1(params)
12234
+ : Promise.resolve({
12235
+ data: { code: 0, message: 'success', result: {} },
12236
+ error: null,
12237
+ ctx: { logid: 'remote-noop', httpStatusCode: 200 },
12238
+ });
11964
12239
  const getCollectedFuncIds = (params) => isLocal() ? getCollectedFuncIds$1(params) : getCollectedFuncIdsRemote(params);
11965
12240
  const getCollecttingInfo = (params) => isLocal() ? getCollecttingInfo$1(params) : getCollecttingInfoRemote(params);
11966
12241
  const startSplit = (params) => isLocal() ? startSplit$1(params) : startSplitRemote(params);
@@ -12148,20 +12423,58 @@ const gameWasmPrepareRoute = {
12148
12423
  },
12149
12424
  };
12150
12425
 
12426
+ /**
12427
+ * 上传符号表(`/symbols`)。只在 session 已通过 `/game/wasm-collect-start`
12428
+ * 打开后调用,与"开 session"解耦:
12429
+ * - 鉴权门是 `/game/wasm-collect-start`(→ `/start`),不在这条。
12430
+ * - 这步是非关键的 debug 信息上传,本地非阻塞/失败不致命,远程为 no-op。
12431
+ * 因此即便失败也只回 errorCode 让调用方静默处理,不应阻断"开始收集"。
12432
+ */
12151
12433
  const gameWasmSetCollectRoute = {
12152
12434
  method: 'post',
12153
12435
  path: '/game/wasm-set-collect',
12154
12436
  handler: async (req, res) => {
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.
12437
+ const { clientKey, codeMd5 } = req.body;
12438
+ console.log('wasm-set-collect (upload symbols)', req.body);
12439
+ const response = await uploadCollectSymbols({
12440
+ client_key: clientKey,
12441
+ wasm_md5: codeMd5,
12442
+ });
12443
+ if (response.error) {
12444
+ res.send({
12445
+ code: errorCode,
12446
+ error: response.error,
12447
+ ctx: response.ctx,
12448
+ });
12449
+ }
12450
+ else {
12451
+ res.send({
12452
+ code: successCode,
12453
+ data: response.data || {},
12454
+ ctx: response.ctx,
12455
+ });
12456
+ }
12457
+ },
12458
+ };
12459
+
12460
+ /**
12461
+ * 打开 collect session —— 对应后端 `/start`(本地)/ `set_collecting`(远程)。
12462
+ *
12463
+ * 单独成一条 IDE 可见路由(而不是埋在 set-collect 里),是为了让浏览器
12464
+ * Network 里有一条**明确叫 start** 的请求:`/start` 走 Portal 鉴权中间件,
12465
+ * 鉴权失败(`-401`)时这条请求会带着透传上来的 `error.code:-401` 返回,
12466
+ * IDE 全局拦截器据此弹"登录态失效"toast,且排查时能一眼定位到是开 session
12467
+ * 这步报的鉴权,而非 collect 轮询接口。
12468
+ *
12469
+ * `resume` 可选:仅本地 pipeline 有意义,缺省 → `reset:true`(重新开始)。
12470
+ */
12471
+ const gameWasmCollectStartRoute = {
12472
+ method: 'post',
12473
+ path: '/game/wasm-collect-start',
12474
+ handler: async (req, res) => {
12162
12475
  const { clientKey, codeMd5, resume } = req.body;
12163
- console.log('wasm-set-collect', req.body);
12164
- const response = await setCollect({
12476
+ console.log('wasm-collect-start', req.body);
12477
+ const response = await openCollectSession({
12165
12478
  client_key: clientKey,
12166
12479
  wasm_md5: codeMd5,
12167
12480
  resume,
@@ -12533,6 +12846,7 @@ const routes = [
12533
12846
  gameWasmPrepareDownloadRoute,
12534
12847
  gameWasmSplitRoute,
12535
12848
  gameWasmTaskinfoRoute,
12849
+ gameWasmCollectStartRoute,
12536
12850
  gameWasmSetCollectRoute,
12537
12851
  gameWasmCollectFuncidsRoute,
12538
12852
  gameWasmCollectInfoRoute,
@@ -12545,14 +12859,61 @@ const routes = [
12545
12859
  gamePipelineModeGetRoute,
12546
12860
  gameLanguageRoute,
12547
12861
  ];
12862
+ /**
12863
+ * Express 4 does not catch rejections from async route handlers — an
12864
+ * unhandled rejection in any handler propagates to the process and, under
12865
+ * Node's default `unhandledRejection` behavior, kills the whole dev server.
12866
+ * That's how a single missing-file ENOENT in `wasm-prepare` could take the
12867
+ * CLI down. Wrap every handler so a failing request returns a 500 instead of
12868
+ * crashing the process. Individual routes can still return structured errors;
12869
+ * this is only the last-resort net for unexpected throws.
12870
+ */
12871
+ function withErrorBoundary(handler) {
12872
+ return (req, res, next) => {
12873
+ try {
12874
+ const result = handler(req, res, next);
12875
+ if (result && typeof result.then === 'function') {
12876
+ result.catch((err) => {
12877
+ console.error(`[dev-server] unhandled error in ${req.method} ${req.path}:`, err);
12878
+ if (!res.headersSent) {
12879
+ res
12880
+ .status(500)
12881
+ .send({
12882
+ code: -1,
12883
+ error: {
12884
+ code: 500,
12885
+ message: err instanceof Error ? err.message : String(err),
12886
+ },
12887
+ });
12888
+ }
12889
+ });
12890
+ }
12891
+ }
12892
+ catch (err) {
12893
+ console.error(`[dev-server] unhandled error in ${req.method} ${req.path}:`, err);
12894
+ if (!res.headersSent) {
12895
+ res
12896
+ .status(500)
12897
+ .send({
12898
+ code: -1,
12899
+ error: {
12900
+ code: 500,
12901
+ message: err instanceof Error ? err.message : String(err),
12902
+ },
12903
+ });
12904
+ }
12905
+ }
12906
+ };
12907
+ }
12548
12908
  function registerRoutes(app, options) {
12549
12909
  const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
12550
12910
  for (const route of allRoutes) {
12911
+ const handler = withErrorBoundary(route.handler);
12551
12912
  if (route.method === 'get') {
12552
- app.get(route.path, route.handler);
12913
+ app.get(route.path, handler);
12553
12914
  }
12554
12915
  else if (route.method === 'post') {
12555
- app.post(route.path, route.handler);
12916
+ app.post(route.path, handler);
12556
12917
  }
12557
12918
  }
12558
12919
  }
@@ -12997,7 +13358,7 @@ async function upload({ clientKey, note = '--', dir, }) {
12997
13358
  }
12998
13359
  }
12999
13360
 
13000
- var version = "0.3.9-beta.wasm.1";
13361
+ var version = "0.4.0";
13001
13362
  var pkg = {
13002
13363
  version: version};
13003
13364