@ttmg/cli 0.3.9 → 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 (59) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/index.js +280 -61
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +1 -1
  5. package/dist/public/assets/{Card-DvUkoCA3.js → Card-BMss5cuV.js} +1 -1
  6. package/dist/public/assets/{Detail-Cbq9dDUe.js → Detail-Lyas-t6F.js} +1 -1
  7. package/dist/public/assets/Detail-Lyas-t6F.js.br +0 -0
  8. package/dist/public/assets/{MonetizationMode-0taoFj2g.js → MonetizationMode-DI4fy3U7.js} +1 -1
  9. package/dist/public/assets/{MonetizationModeSummary--L4WcmYS.js → MonetizationModeSummary-B6yEB26H.js} +1 -1
  10. package/dist/public/assets/{SectionHeader-DfBBWmpa.js → SectionHeader-3b39hv3n.js} +1 -1
  11. package/dist/public/assets/{Tag-DuP9fCS0.js → Tag-B5gYnANu.js} +1 -1
  12. package/dist/public/assets/{arrow-left-1V5G0Kw7.js → arrow-left-B3swWB64.js} +1 -1
  13. package/dist/public/assets/{baseForm-2J62W6xr.js → baseForm-Dsi8Ia0k.js} +1 -1
  14. package/dist/public/assets/baseForm-Dsi8Ia0k.js.br +0 -0
  15. package/dist/public/assets/{chevron-right-C5K8xBff.js → chevron-right-WZeuFw11.js} +1 -1
  16. package/dist/public/assets/{compass-DZKQ36UU.js → compass-CCsIev11.js} +1 -1
  17. package/dist/public/assets/{index-CZ7VVYfd.js → index-B3-gsm5-.js} +1 -1
  18. package/dist/public/assets/index-B3L1GnMP.js +1 -0
  19. package/dist/public/assets/index-B3L1GnMP.js.br +0 -0
  20. package/dist/public/assets/{index-BaZPp3HP.js → index-BJV8-tz8.js} +1 -1
  21. package/dist/public/assets/{index-cPr70sDS.js → index-BqSAtEgq.js} +1 -1
  22. package/dist/public/assets/{index-CmVUh50W.css → index-ByX5RuFA.css} +1 -1
  23. package/dist/public/assets/{index-WZnCtUAc.js → index-C2NWNiPX.js} +1 -1
  24. package/dist/public/assets/{index-BdDJLxXD.js → index-C9HXjiVu.js} +1 -1
  25. package/dist/public/assets/{index-DqoKSlOn.js → index-C9Un1hFP.js} +1 -1
  26. package/dist/public/assets/{index-Bmw61rl1.css → index-CFd8iglC.css} +1 -1
  27. package/dist/public/assets/index-CFd8iglC.css.br +0 -0
  28. package/dist/public/assets/{index-fS7DhM40.js → index-CV2u_S0E.js} +1 -1
  29. package/dist/public/assets/index-CV2u_S0E.js.br +0 -0
  30. package/dist/public/assets/index-Ci-lqt1V.js +1 -0
  31. package/dist/public/assets/index-Ci-lqt1V.js.br +0 -0
  32. package/dist/public/assets/{index-DYxQAjNc.js → index-Cigxnpav.js} +1 -1
  33. package/dist/public/assets/index-Cigxnpav.js.br +0 -0
  34. package/dist/public/assets/{index-BWWe1lsP.js → index-D0xEiy7C.js} +1 -1
  35. package/dist/public/assets/{index-DNktXE2W.js → index-D1oGAPRa.js} +1 -1
  36. package/dist/public/assets/{index-Br2fR8kJ.js → index-DeL2bgxo.js} +1 -1
  37. package/dist/public/assets/{index-B13yZ-GH.js → index-DjY3Igd6.js} +1 -1
  38. package/dist/public/assets/{index-CcxOfQrE.js → index-DpWpqPKC.js} +1 -1
  39. package/dist/public/assets/index-DpWpqPKC.js.br +0 -0
  40. package/dist/public/assets/{index-CnReCFYA.js → index-DxISN0Xc.js} +5 -5
  41. package/dist/public/assets/index-DxISN0Xc.js.br +0 -0
  42. package/dist/public/assets/{index-CgTtqphq.js → index-HYSh4-Ri.js} +1 -1
  43. package/dist/public/assets/{index-B-k2MmlV.js → index-JNUqDBWt.js} +1 -1
  44. package/dist/public/assets/{index-CwYCdibK.js → index-aqIfJUqW.js} +1 -1
  45. package/dist/public/assets/{index-BttcxiCg.js → index-uquhwGkB.js} +1 -1
  46. package/dist/public/assets/{sparkles-tr6K1Npg.js → sparkles-D2QPhLAu.js} +1 -1
  47. package/dist/public/assets/{zap-CuMRVa_B.js → zap-Cv4X8yRx.js} +1 -1
  48. package/dist/public/index.html +1 -1
  49. package/package.json +1 -1
  50. package/dist/public/assets/Detail-Cbq9dDUe.js.br +0 -0
  51. package/dist/public/assets/baseForm-2J62W6xr.js.br +0 -0
  52. package/dist/public/assets/index-Bmw61rl1.css.br +0 -0
  53. package/dist/public/assets/index-CcxOfQrE.js.br +0 -0
  54. package/dist/public/assets/index-CnReCFYA.js.br +0 -0
  55. package/dist/public/assets/index-DYxQAjNc.js.br +0 -0
  56. package/dist/public/assets/index-YkO-JjYA.js +0 -1
  57. package/dist/public/assets/index-fS7DhM40.js.br +0 -0
  58. package/dist/public/assets/index-qHht7NSU.js +0 -1
  59. package/dist/public/assets/index-qHht7NSU.js.br +0 -0
package/CHANGELOG.md CHANGED
@@ -238,4 +238,9 @@ API 预检建议文案统一收敛,避免重复提示,阅读更清晰
238
238
  6. 新版本提示由红点改为「有更新」文字标签,含义更直观。
239
239
  7. 上传游戏包按钮配色与产品主题色统一。
240
240
  8. 修复多个页面中标题与正文内容未对齐的问题,整体排版更整齐。
241
- 9. 文案细节调整:「身份与授权」更名为「登录与授权」;移除侧边栏冗余的「查看文档」入口。
241
+ 9. 文案细节调整:「身份与授权」更名为「登录与授权」;移除侧边栏冗余的「查看文档」入口。
242
+
243
+ ## 0.3.9-beta.1
244
+ 本次测试版本主要补充启动模式场景能力,并优化场景命名可理解性:
245
+
246
+ 1. 启动模式新增 `inbox` 场景,支持在调试链路中模拟消息入口启动小游戏。
package/dist/index.js CHANGED
@@ -8372,8 +8372,81 @@ async function compile(context) {
8372
8372
  }
8373
8373
 
8374
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
+ }
8375
8436
  async function watch() {
8376
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
+ };
8377
8450
  // 监听当前工作目录,排除 node_modules 和 .git
8378
8451
  const watcher = chokidar.watch(process.cwd(), {
8379
8452
  /**
@@ -8388,17 +8461,20 @@ async function watch() {
8388
8461
  });
8389
8462
  // 任意文件变化都触发
8390
8463
  watcher.on('all', (event, path) => {
8391
- // 清除之前的定时器
8392
- if (debounceTimer)
8393
- clearTimeout(debounceTimer);
8394
- // 重新设置定时器
8395
- debounceTimer = setTimeout(async () => {
8396
- (await compile({
8397
- mode: 'watch',
8398
- }),
8399
- wsServer.sendResourceChange());
8400
- debounceTimer = null;
8401
- }, 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?.();
8402
8478
  });
8403
8479
  watcher.on('error', error => {
8404
8480
  // console.error(chalk.red('[watch] 监听发生错误:'), error);
@@ -10490,42 +10566,83 @@ function restoreSplitConfigFromCache(entryDir = process.cwd()) {
10490
10566
  * to change.
10491
10567
  */
10492
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) {
10493
10582
  const cacheDir = path.join(entryDir, WASM_SPLIT_CACHE_DIR);
10494
- // 1) Wipe stale split residue inside wasmcode/ first, THEN restore the
10495
- // original. Order matters: if we restore first then wipe, we'd delete
10496
- // the very file we just brought back.
10497
10583
  const originDir = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root);
10498
- if (fs.existsSync(originDir)) {
10499
- for (const entry of fs.readdirSync(originDir)) {
10500
- // Only clean files split is known to write `.br` (main wasm) and
10501
- // the empty `game.js` placeholder. Touching anything else risks
10502
- // nuking developer-authored content that happens to live in
10503
- // wasmcode/ for unrelated reasons.
10504
- if (entry.endsWith('.br') || entry === 'game.js') {
10505
- fs.rmSync(path.join(originDir, entry), { force: true });
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
+ }
10506
10609
  }
10507
10610
  }
10611
+ const destWasmBrPath = path.join(originDir, path.basename(cachedWasmBr));
10612
+ ensureDirSync(path.dirname(destWasmBrPath));
10613
+ fs.copyFileSync(path.join(cacheDir, cachedWasmBr), destWasmBrPath);
10508
10614
  }
10509
- if (fs.existsSync(cacheDir)) {
10510
- const targetWasmBrPath = fs
10511
- .readdirSync(cacheDir)
10512
- .find(item => item.endsWith('.br'));
10513
- if (targetWasmBrPath) {
10514
- const destWasmBrPath = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
10515
- ensureDirSync(path.dirname(destWasmBrPath));
10516
- fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
10517
- }
10518
- }
10519
- const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
10520
- if (fs.existsSync(splitConfigCachePath)) {
10521
- fs.copyFileSync(splitConfigCachePath, path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME));
10522
- }
10523
- const gameJsonCachePath = path.join(cacheDir, 'game.json');
10524
- if (fs.existsSync(gameJsonCachePath)) {
10525
- fs.copyFileSync(gameJsonCachePath, path.join(entryDir, 'game.json'));
10526
- }
10527
- // Restore wasmcode/game.js. We just deleted whatever was there in step 1,
10528
- // so we always need to put something back when wasmcode is a subpackage.
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.
10529
10646
  // Strategy:
10530
10647
  // - Prefer the cache (keepCacheSync stashes pre-split contents to
10531
10648
  // `__unity_cache__/wasmcode-game.js` when the original existed)
@@ -10536,14 +10653,16 @@ function restoreFromCache(entryDir = process.cwd()) {
10536
10653
  // downloadSplited.ts also writes; it satisfies the platform requirement
10537
10654
  // without changing semantics for projects that don't use wasmcode as
10538
10655
  // a subpackage (the file is harmless empty).
10539
- const originGameJsCachePath = path.join(cacheDir, 'wasmcode-game.js');
10540
- const originGameJsDestPath = path.join(originDir, 'game.js');
10541
- if (fs.existsSync(originDir)) {
10542
- if (fs.existsSync(originGameJsCachePath)) {
10543
- fs.copyFileSync(originGameJsCachePath, originGameJsDestPath);
10544
- }
10545
- else {
10546
- 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
+ }
10547
10666
  }
10548
10667
  }
10549
10668
  for (const subDir of SPLIT_OUTPUT_DIRS) {
@@ -10661,19 +10780,51 @@ function setLocalState(partial) {
10661
10780
  Object.assign(state, partial);
10662
10781
  }
10663
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
+ */
10664
10789
  async function startPrepare$1(params) {
10790
+ return withWatchSuspended(() => startPrepareImpl(params), {
10791
+ emitOnFinish: true,
10792
+ });
10793
+ }
10794
+ async function startPrepareImpl(params) {
10665
10795
  const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
10666
10796
  ensureDirSync(tempDir);
10667
10797
  const inputPath = path$1.join(process.cwd(), params.wasm_file_path);
10668
- let rawWasmPath = path$1.join(tempDir, 'original.wasm');
10669
- if (inputPath.endsWith('.br')) {
10670
- await decompressWasmFile(inputPath, rawWasmPath);
10671
- }
10672
- else {
10673
- fs$1.copyFileSync(inputPath, rawWasmPath);
10674
- }
10798
+ const rawWasmPath = path$1.join(tempDir, 'original.wasm');
10675
10799
  const preparedWasmPath = path$1.join(tempDir, 'prepared.wasm');
10676
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
+ }
10677
10828
  const result = ttmgWasmtool.prepare(rawWasmPath, preparedWasmPath);
10678
10829
  verboseLog(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
10679
10830
  const gameJson = getGameJson();
@@ -11244,7 +11395,18 @@ function updateSubpackageConfigSync(archive = false) {
11244
11395
  fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11245
11396
  }
11246
11397
 
11247
- 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) {
11248
11410
  const cwd = process.cwd();
11249
11411
  const { splitMeta } = getLocalState();
11250
11412
  if (!splitMeta) {
@@ -11567,7 +11729,17 @@ async function downloadOne(opts) {
11567
11729
  // so the subpackage loader doesn't complain about missing js entries.
11568
11730
  fs.writeFileSync(path.join(path.dirname(out), 'game.js'), '', 'utf-8');
11569
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
+ */
11570
11737
  async function downloadSplitedRemote(context) {
11738
+ return withWatchSuspended(() => downloadSplitedRemoteImpl(context), {
11739
+ emitOnFinish: true,
11740
+ });
11741
+ }
11742
+ async function downloadSplitedRemoteImpl(context) {
11571
11743
  const cwd = process.cwd();
11572
11744
  const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11573
11745
  ensureDirSync(splitTempDir);
@@ -12687,14 +12859,61 @@ const routes = [
12687
12859
  gamePipelineModeGetRoute,
12688
12860
  gameLanguageRoute,
12689
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
+ }
12690
12908
  function registerRoutes(app, options) {
12691
12909
  const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
12692
12910
  for (const route of allRoutes) {
12911
+ const handler = withErrorBoundary(route.handler);
12693
12912
  if (route.method === 'get') {
12694
- app.get(route.path, route.handler);
12913
+ app.get(route.path, handler);
12695
12914
  }
12696
12915
  else if (route.method === 'post') {
12697
- app.post(route.path, route.handler);
12916
+ app.post(route.path, handler);
12698
12917
  }
12699
12918
  }
12700
12919
  }
@@ -13139,7 +13358,7 @@ async function upload({ clientKey, note = '--', dir, }) {
13139
13358
  }
13140
13359
  }
13141
13360
 
13142
- var version = "0.3.9";
13361
+ var version = "0.4.0";
13143
13362
  var pkg = {
13144
13363
  version: version};
13145
13364