@ttmg/cli 0.3.9 → 0.4.0-beta.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 (67) hide show
  1. package/dist/index.js +416 -81
  2. package/dist/index.js.map +1 -1
  3. package/dist/package.json +1 -1
  4. package/dist/public/assets/{Card-DvUkoCA3.js → Card-C35olIke.js} +1 -1
  5. package/dist/public/assets/Detail-Bw_dl9hw.js +1 -0
  6. package/dist/public/assets/Detail-Bw_dl9hw.js.br +0 -0
  7. package/dist/public/assets/MonetizationMode-C7Wyv9Mg.js +1 -0
  8. package/dist/public/assets/{MonetizationModeSummary--L4WcmYS.js → MonetizationModeSummary-D-zuX05a.js} +1 -1
  9. package/dist/public/assets/{SectionHeader-DfBBWmpa.js → SectionHeader-DRXnax7L.js} +1 -1
  10. package/dist/public/assets/{Tag-DuP9fCS0.js → Tag-CgPHrk93.js} +1 -1
  11. package/dist/public/assets/{arrow-left-1V5G0Kw7.js → arrow-left-CNdHGsDz.js} +1 -1
  12. package/dist/public/assets/{baseForm-2J62W6xr.js → baseForm-C2Ms-wqt.js} +1 -1
  13. package/dist/public/assets/baseForm-C2Ms-wqt.js.br +0 -0
  14. package/dist/public/assets/{chevron-right-C5K8xBff.js → chevron-right-CX-Bo3I2.js} +1 -1
  15. package/dist/public/assets/{compass-DZKQ36UU.js → compass-BUqxZo39.js} +1 -1
  16. package/dist/public/assets/{index-B13yZ-GH.js → index-8z6tNstf.js} +1 -1
  17. package/dist/public/assets/index-AGEFeR2m.js +1 -0
  18. package/dist/public/assets/{index-B-k2MmlV.js → index-B8dmfh3A.js} +1 -1
  19. package/dist/public/assets/{index-Br2fR8kJ.js → index-B9C7FZes.js} +1 -1
  20. package/dist/public/assets/{index-DYxQAjNc.js → index-BULJJdeE.js} +1 -1
  21. package/dist/public/assets/index-BULJJdeE.js.br +0 -0
  22. package/dist/public/assets/index-B_iiHvzl.js +1 -0
  23. package/dist/public/assets/index-B_iiHvzl.js.br +0 -0
  24. package/dist/public/assets/{index-fS7DhM40.js → index-BmmpJdOy.js} +1 -1
  25. package/dist/public/assets/index-BmmpJdOy.js.br +0 -0
  26. package/dist/public/assets/{index-DqoKSlOn.js → index-BuGJ38RF.js} +1 -1
  27. package/dist/public/assets/{index-CmVUh50W.css → index-ByX5RuFA.css} +1 -1
  28. package/dist/public/assets/{index-CgTtqphq.js → index-ByqGLZ4G.js} +1 -1
  29. package/dist/public/assets/{index-Bmw61rl1.css → index-CFd8iglC.css} +1 -1
  30. package/dist/public/assets/index-CFd8iglC.css.br +0 -0
  31. package/dist/public/assets/{index-CwYCdibK.js → index-CIIptoBi.js} +1 -1
  32. package/dist/public/assets/{index-BWWe1lsP.js → index-CLXkCzG6.js} +1 -1
  33. package/dist/public/assets/{index-WZnCtUAc.js → index-CUV27PJL.js} +1 -1
  34. package/dist/public/assets/{index-BttcxiCg.js → index-CxeatYOZ.js} +1 -1
  35. package/dist/public/assets/{index-BdDJLxXD.js → index-D0DgrJ_K.js} +1 -1
  36. package/dist/public/assets/{index-BaZPp3HP.js → index-D28D1GWM.js} +1 -1
  37. package/dist/public/assets/index-DCZsvwKh.js +1 -0
  38. package/dist/public/assets/index-Dd2rc2_1.js +1 -0
  39. package/dist/public/assets/index-Dd2rc2_1.js.br +0 -0
  40. package/dist/public/assets/index-Dk9_4hQ4.js +1 -0
  41. package/dist/public/assets/{index-DNktXE2W.js → index-EorOvXWq.js} +1 -1
  42. package/dist/public/assets/{index-B-AeuNlL.css → index-_6n0s04V.css} +1 -1
  43. package/dist/public/assets/index-_6n0s04V.css.br +0 -0
  44. package/dist/public/assets/{index-CnReCFYA.js → index-cC0QApVl.js} +8 -8
  45. package/dist/public/assets/index-cC0QApVl.js.br +0 -0
  46. package/dist/public/assets/{index-CZ7VVYfd.js → index-l16xoaSk.js} +1 -1
  47. package/dist/public/assets/{sparkles-tr6K1Npg.js → sparkles-C54nzN2M.js} +1 -1
  48. package/dist/public/assets/{zap-CuMRVa_B.js → zap-g-acVHwy.js} +1 -1
  49. package/dist/public/index.html +1 -1
  50. package/package.json +1 -1
  51. package/CHANGELOG.md +0 -241
  52. package/dist/public/assets/Detail-Cbq9dDUe.js +0 -1
  53. package/dist/public/assets/Detail-Cbq9dDUe.js.br +0 -0
  54. package/dist/public/assets/MonetizationMode-0taoFj2g.js +0 -1
  55. package/dist/public/assets/baseForm-2J62W6xr.js.br +0 -0
  56. package/dist/public/assets/index-B-AeuNlL.css.br +0 -0
  57. package/dist/public/assets/index-Bmw61rl1.css.br +0 -0
  58. package/dist/public/assets/index-CcxOfQrE.js +0 -1
  59. package/dist/public/assets/index-CcxOfQrE.js.br +0 -0
  60. package/dist/public/assets/index-CnReCFYA.js.br +0 -0
  61. package/dist/public/assets/index-DYxQAjNc.js.br +0 -0
  62. package/dist/public/assets/index-YkO-JjYA.js +0 -1
  63. package/dist/public/assets/index-cPr70sDS.js +0 -1
  64. package/dist/public/assets/index-fS7DhM40.js.br +0 -0
  65. package/dist/public/assets/index-qHht7NSU.js +0 -1
  66. package/dist/public/assets/index-qHht7NSU.js.br +0 -0
  67. /package/dist/public/assets/{index-B6NHbQwP.js → index-CjInIkcE.js} +0 -0
package/dist/index.js CHANGED
@@ -6924,10 +6924,8 @@ function isAxiosError(e) {
6924
6924
  }
6925
6925
 
6926
6926
  const CREATE_DIRECT_FEED_CARD_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/create_direct_feed_card';
6927
- const DIRECT_FEED_CARD_HEADERS$2 = {
6927
+ const DIRECT_FEED_CARD_HEADERS$3 = {
6928
6928
  'Content-Type': 'application/json',
6929
- 'x-use-ppe': '1',
6930
- 'x-tt-env': 'ppe_feed_play',
6931
6929
  };
6932
6930
  async function createDirectFeedCard({ appId, clientKey, directFeedCard, }) {
6933
6931
  const payload = {
@@ -6938,22 +6936,20 @@ async function createDirectFeedCard({ appId, clientKey, directFeedCard, }) {
6938
6936
  return request({
6939
6937
  url: CREATE_DIRECT_FEED_CARD_URL,
6940
6938
  method: 'POST',
6941
- headers: DIRECT_FEED_CARD_HEADERS$2,
6939
+ headers: DIRECT_FEED_CARD_HEADERS$3,
6942
6940
  data: payload,
6943
6941
  });
6944
6942
  }
6945
6943
 
6946
6944
  const GET_DIRECT_FEED_SCENARIOS_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/get_direct_feed_scenario';
6947
- const DIRECT_FEED_CARD_HEADERS$1 = {
6945
+ const DIRECT_FEED_CARD_HEADERS$2 = {
6948
6946
  'Content-Type': 'application/json',
6949
- 'x-use-ppe': '1',
6950
- 'x-tt-env': 'ppe_feed_play',
6951
6947
  };
6952
6948
  async function fetchDirectFeedScenarios({ appId, clientKey, }) {
6953
6949
  return request({
6954
6950
  url: GET_DIRECT_FEED_SCENARIOS_URL,
6955
6951
  method: 'POST',
6956
- headers: DIRECT_FEED_CARD_HEADERS$1,
6952
+ headers: DIRECT_FEED_CARD_HEADERS$2,
6957
6953
  data: {
6958
6954
  app_id: appId,
6959
6955
  client_key: clientKey,
@@ -6973,16 +6969,14 @@ async function fetchGameAssetPreviewUrl({ assetId, }) {
6973
6969
  }
6974
6970
 
6975
6971
  const GET_DIRECT_FEED_CARD_ASSET_PREVIEW_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/get_direct_feed_card_asset_preview_url';
6976
- const DIRECT_FEED_CARD_HEADERS = {
6972
+ const DIRECT_FEED_CARD_HEADERS$1 = {
6977
6973
  'Content-Type': 'application/json',
6978
- 'x-use-ppe': '1',
6979
- 'x-tt-env': 'ppe_feed_play',
6980
6974
  };
6981
6975
  async function fetchDirectFeedCardAssetPreviewUrl({ assetId, contentId, }) {
6982
6976
  return request({
6983
6977
  url: GET_DIRECT_FEED_CARD_ASSET_PREVIEW_URL,
6984
6978
  method: 'POST',
6985
- headers: DIRECT_FEED_CARD_HEADERS,
6979
+ headers: DIRECT_FEED_CARD_HEADERS$1,
6986
6980
  data: {
6987
6981
  asset_id: assetId,
6988
6982
  content_id: contentId,
@@ -7040,8 +7034,6 @@ async function fetchGameInfo(clientKey) {
7040
7034
  const LIST_DIRECT_FEED_CARD_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/list_direct_feed_card';
7041
7035
  const LIST_DIRECT_FEED_CARD_HEADERS = {
7042
7036
  'Content-Type': 'application/json',
7043
- 'x-use-ppe': '1',
7044
- 'x-tt-env': 'ppe_feed_play',
7045
7037
  };
7046
7038
  async function listDirectFeedCards({ appId, clientKey, }) {
7047
7039
  return request({
@@ -7055,6 +7047,23 @@ async function listDirectFeedCards({ appId, clientKey, }) {
7055
7047
  });
7056
7048
  }
7057
7049
 
7050
+ const CHANGE_DIRECT_FEED_CARD_STATUS_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/change_direct_feed_card_status';
7051
+ const DIRECT_FEED_CARD_HEADERS = {
7052
+ 'Content-Type': 'application/json',
7053
+ };
7054
+ async function changeDirectFeedCardStatus({ appId, clientKey, contentStatus, }) {
7055
+ return request({
7056
+ url: CHANGE_DIRECT_FEED_CARD_STATUS_URL,
7057
+ method: 'POST',
7058
+ headers: DIRECT_FEED_CARD_HEADERS,
7059
+ data: {
7060
+ app_id: appId,
7061
+ client_key: clientKey,
7062
+ content_status: contentStatus,
7063
+ },
7064
+ });
7065
+ }
7066
+
7058
7067
  async function uploadGameToPlatform({ data, name, clientKey, note = '--', appId, }) {
7059
7068
  if (!appId) {
7060
7069
  return {
@@ -8372,8 +8381,81 @@ async function compile(context) {
8372
8381
  }
8373
8382
 
8374
8383
  // import { uploadGame } from './uploadGame';
8384
+ /* ---------------------------------------------------------------------------
8385
+ * Watch suspension control
8386
+ *
8387
+ * The wasm-split pipeline (prepare / split download / rollback) intentionally
8388
+ * rewrites a burst of project files — the instrumented wasm, the `wasmcode*`
8389
+ * subpackage dirs, `game.json`, `webgl-wasm-split.js`. Without gating, every
8390
+ * one of those writes trips the watcher below, and each trip runs a full
8391
+ * `compile` + device `resourceChange` reload *in the middle of the pipeline*.
8392
+ * That both wastes cycles and can reload the device onto a half-written /
8393
+ * transient build. We suspend the watcher around those operations and fire
8394
+ * exactly one update after they complete ("完成之后再触发更新").
8395
+ *
8396
+ * `suspendDepth` is a refcount so overlapping / nested operations are safe.
8397
+ * `muteUntil` keeps swallowing events for a short cooldown AFTER resume:
8398
+ * chokidar's `awaitWriteFinish` delays events ~1s past the actual write, so
8399
+ * those late events from our own writes would otherwise land after we've
8400
+ * resumed and re-trigger the very reload we just suppressed.
8401
+ * ------------------------------------------------------------------------- */
8402
+ let suspendDepth = 0;
8403
+ let muteUntil = 0;
8404
+ let pendingWhileSuspended = false;
8405
+ // chokidar awaitWriteFinish stabilityThreshold (1000ms) + headroom, so the
8406
+ // delayed FS events caused by the pipeline's own writes are swallowed.
8407
+ const RESUME_COOLDOWN_MS = 2500;
8408
+ // Wired up inside `watch()` so suspend/resume can reuse the exact same
8409
+ // (debounced) compile + resourceChange action the watcher fires normally.
8410
+ let triggerUpdate = null;
8411
+ function suspendWatch() {
8412
+ suspendDepth += 1;
8413
+ }
8414
+ function resumeWatch(options = {}) {
8415
+ if (suspendDepth > 0) {
8416
+ suspendDepth -= 1;
8417
+ }
8418
+ if (suspendDepth > 0) {
8419
+ return;
8420
+ }
8421
+ // Back to depth 0 — start the cooldown that swallows our own delayed FS
8422
+ // events, then emit a single update if anything changed while suspended
8423
+ // (or the caller forces it).
8424
+ muteUntil = Date.now() + RESUME_COOLDOWN_MS;
8425
+ const shouldEmit = options.emit ?? pendingWhileSuspended;
8426
+ pendingWhileSuspended = false;
8427
+ if (shouldEmit) {
8428
+ triggerUpdate?.();
8429
+ }
8430
+ }
8431
+ /**
8432
+ * Run a project-file-mutating operation with the watcher muted, then fire a
8433
+ * single update once it finishes. Use this around the wasm-split pipeline
8434
+ * steps so their intermediate writes don't each reload the device.
8435
+ */
8436
+ async function withWatchSuspended(fn, options = {}) {
8437
+ suspendWatch();
8438
+ try {
8439
+ return await fn();
8440
+ }
8441
+ finally {
8442
+ resumeWatch({ emit: options.emitOnFinish ?? true });
8443
+ }
8444
+ }
8375
8445
  async function watch() {
8376
8446
  let debounceTimer = null;
8447
+ const runUpdate = async () => {
8448
+ await compile({ mode: 'watch' });
8449
+ wsServer.sendResourceChange();
8450
+ };
8451
+ triggerUpdate = () => {
8452
+ if (debounceTimer)
8453
+ clearTimeout(debounceTimer);
8454
+ debounceTimer = setTimeout(() => {
8455
+ runUpdate();
8456
+ debounceTimer = null;
8457
+ }, 2000);
8458
+ };
8377
8459
  // 监听当前工作目录,排除 node_modules 和 .git
8378
8460
  const watcher = chokidar.watch(process.cwd(), {
8379
8461
  /**
@@ -8388,17 +8470,20 @@ async function watch() {
8388
8470
  });
8389
8471
  // 任意文件变化都触发
8390
8472
  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);
8473
+ // Pipeline is actively rewriting project files. Remember that something
8474
+ // changed so the resume flush can fire one update, and skip the per-write
8475
+ // compile/reload.
8476
+ if (suspendDepth > 0) {
8477
+ pendingWhileSuspended = true;
8478
+ return;
8479
+ }
8480
+ // Cooldown window right after a pipeline finished: these are the delayed
8481
+ // `awaitWriteFinish` events from the pipeline's own writes. We already
8482
+ // emitted one update on resume, so swallow them to avoid a double reload.
8483
+ if (Date.now() < muteUntil) {
8484
+ return;
8485
+ }
8486
+ triggerUpdate?.();
8402
8487
  });
8403
8488
  watcher.on('error', error => {
8404
8489
  // console.error(chalk.red('[watch] 监听发生错误:'), error);
@@ -9836,7 +9921,7 @@ const gameDirectFeedCardAssetPreviewRoute = {
9836
9921
  },
9837
9922
  };
9838
9923
 
9839
- async function resolveAppIdentity$2(body) {
9924
+ async function resolveAppIdentity$3(body) {
9840
9925
  const clientKey = body?.clientKey?.trim() ||
9841
9926
  body?.client_key?.trim() ||
9842
9927
  getClientKey().clientKey?.trim();
@@ -9903,7 +9988,7 @@ const gameDirectFeedCardCreateRoute = {
9903
9988
  method: 'post',
9904
9989
  path: '/game/direct-feed-card/create',
9905
9990
  handler: async (req, res) => {
9906
- const identity = await resolveAppIdentity$2(req.body);
9991
+ const identity = await resolveAppIdentity$3(req.body);
9907
9992
  if (identity.error) {
9908
9993
  res.send({
9909
9994
  code: errorCode,
@@ -9959,7 +10044,7 @@ function isUsableDirectFeedCard(card) {
9959
10044
  }
9960
10045
  return true;
9961
10046
  }
9962
- async function resolveAppIdentity$1(body) {
10047
+ async function resolveAppIdentity$2(body) {
9963
10048
  const clientKey = body?.clientKey?.trim() ||
9964
10049
  body?.client_key?.trim() ||
9965
10050
  getClientKey().clientKey?.trim();
@@ -10004,7 +10089,7 @@ const gameDirectFeedCardListRoute = {
10004
10089
  method: 'post',
10005
10090
  path: '/game/direct-feed-card/list',
10006
10091
  handler: async (req, res) => {
10007
- const identity = await resolveAppIdentity$1(req.body);
10092
+ const identity = await resolveAppIdentity$2(req.body);
10008
10093
  if (identity.error) {
10009
10094
  res.send({
10010
10095
  code: errorCode,
@@ -10109,7 +10194,7 @@ function extractScenarioOptions(data) {
10109
10194
  }
10110
10195
  return [];
10111
10196
  }
10112
- async function resolveAppIdentity(body) {
10197
+ async function resolveAppIdentity$1(body) {
10113
10198
  const clientKey = body?.clientKey?.trim() ||
10114
10199
  body?.client_key?.trim() ||
10115
10200
  getClientKey().clientKey?.trim();
@@ -10154,7 +10239,7 @@ const gameDirectFeedCardScenariosRoute = {
10154
10239
  method: 'post',
10155
10240
  path: '/game/direct-feed-card/scenarios',
10156
10241
  handler: async (req, res) => {
10157
- const identity = await resolveAppIdentity(req.body);
10242
+ const identity = await resolveAppIdentity$1(req.body);
10158
10243
  if (identity.error) {
10159
10244
  res.send({
10160
10245
  code: errorCode,
@@ -10194,6 +10279,112 @@ const gameDirectFeedCardScenariosRoute = {
10194
10279
  },
10195
10280
  };
10196
10281
 
10282
+ async function resolveAppIdentity(body) {
10283
+ const clientKey = body?.clientKey?.trim() ||
10284
+ body?.client_key?.trim() ||
10285
+ getClientKey().clientKey?.trim();
10286
+ let appId = body?.appId?.trim() ||
10287
+ body?.app_id?.trim() ||
10288
+ store.getState().appId?.trim();
10289
+ if (!clientKey) {
10290
+ return {
10291
+ clientKey: '',
10292
+ appId,
10293
+ error: 'Missing client key. Please run `ttmg init` or pass `clientKey` from IDE.',
10294
+ };
10295
+ }
10296
+ if (!appId) {
10297
+ const gameInfoResponse = await fetchGameInfo(clientKey);
10298
+ if (gameInfoResponse.error) {
10299
+ return {
10300
+ clientKey,
10301
+ appId: '',
10302
+ error: gameInfoResponse.error,
10303
+ };
10304
+ }
10305
+ appId = gameInfoResponse.data?.app_id?.trim() || '';
10306
+ if (appId) {
10307
+ store.setState({ appId });
10308
+ }
10309
+ }
10310
+ if (!appId) {
10311
+ return {
10312
+ clientKey,
10313
+ appId: '',
10314
+ error: 'Missing app id. Please open project detail first or pass `appId` from IDE.',
10315
+ };
10316
+ }
10317
+ return {
10318
+ clientKey,
10319
+ appId,
10320
+ error: null,
10321
+ };
10322
+ }
10323
+ function normalizeContentStatus(body) {
10324
+ const source = body?.contentStatus || body?.content_status || {};
10325
+ const contentStatus = Object.entries(source).reduce((result, [contentId, status]) => {
10326
+ const normalizedContentId = String(contentId || '').trim();
10327
+ const normalizedStatus = Number(status);
10328
+ if (normalizedContentId && Number.isFinite(normalizedStatus)) {
10329
+ result[normalizedContentId] = normalizedStatus;
10330
+ }
10331
+ return result;
10332
+ }, {});
10333
+ if (Object.keys(contentStatus).length === 0) {
10334
+ return {
10335
+ contentStatus,
10336
+ error: 'Missing direct-play card status. Please pass a non-empty `contentStatus` from IDE.',
10337
+ };
10338
+ }
10339
+ return {
10340
+ contentStatus,
10341
+ error: null,
10342
+ };
10343
+ }
10344
+ const gameDirectFeedCardStatusRoute = {
10345
+ method: 'post',
10346
+ path: '/game/direct-feed-card/status',
10347
+ handler: async (req, res) => {
10348
+ const body = req.body;
10349
+ const identity = await resolveAppIdentity(body);
10350
+ if (identity.error) {
10351
+ res.send({
10352
+ code: errorCode,
10353
+ error: identity.error,
10354
+ data: null,
10355
+ });
10356
+ return;
10357
+ }
10358
+ const normalized = normalizeContentStatus(body);
10359
+ if (normalized.error) {
10360
+ res.send({
10361
+ code: errorCode,
10362
+ error: normalized.error,
10363
+ data: null,
10364
+ });
10365
+ return;
10366
+ }
10367
+ const response = await changeDirectFeedCardStatus({
10368
+ appId: identity.appId,
10369
+ clientKey: identity.clientKey,
10370
+ contentStatus: normalized.contentStatus,
10371
+ });
10372
+ if (response.error) {
10373
+ res.send({
10374
+ code: errorCode,
10375
+ error: response.error,
10376
+ ctx: response.ctx,
10377
+ });
10378
+ return;
10379
+ }
10380
+ res.send({
10381
+ code: successCode,
10382
+ data: response.data,
10383
+ ctx: response.ctx,
10384
+ });
10385
+ },
10386
+ };
10387
+
10197
10388
  const gameDetailRoute = {
10198
10389
  method: 'get',
10199
10390
  path: '/game/detail',
@@ -10490,42 +10681,83 @@ function restoreSplitConfigFromCache(entryDir = process.cwd()) {
10490
10681
  * to change.
10491
10682
  */
10492
10683
  function restoreFromCache(entryDir = process.cwd()) {
10684
+ // Rollback rewrites a burst of project files (restored wasm, game.json,
10685
+ // split config, removed split-output dirs). Mute the dev-server watcher for
10686
+ // the duration so it doesn't fire a compile + device reload per file, then
10687
+ // emit a single update once the project is back to its restored state.
10688
+ suspendWatch();
10689
+ try {
10690
+ restoreFromCacheInner(entryDir);
10691
+ }
10692
+ finally {
10693
+ resumeWatch({ emit: true });
10694
+ }
10695
+ }
10696
+ function restoreFromCacheInner(entryDir) {
10493
10697
  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
10698
  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 });
10699
+ // Resolve the cached original wasm BEFORE touching anything on disk. The
10700
+ // previous order (wipe `wasmcode/*.br` first, then look for a cached
10701
+ // original to restore) was destructive when the cache had no original to
10702
+ // give back e.g. the project was duplicated without `__TTMG_TEMP__`, the
10703
+ // temp dir was cleared, or split completed in a session whose cache is gone.
10704
+ // In that case it deleted the project's only wasm and restored nothing,
10705
+ // leaving `wasmcode/` empty while `game.json` still pointed at the now-missing
10706
+ // `<md5>.wasm.br`. The next prepare then crashed the dev server on an ENOENT
10707
+ // decompress. We only clean + restore when we have a confirmed replacement.
10708
+ const cachedWasmBr = fs.existsSync(cacheDir)
10709
+ ? fs.readdirSync(cacheDir).find(item => item.endsWith('.br'))
10710
+ : undefined;
10711
+ if (cachedWasmBr) {
10712
+ // Safe to wipe split residue first: we have a confirmed original to put
10713
+ // back. Order matters — restoring first then wiping would delete the file
10714
+ // we just brought back.
10715
+ if (fs.existsSync(originDir)) {
10716
+ for (const entry of fs.readdirSync(originDir)) {
10717
+ // Only clean files split is known to write — `.br` (main wasm) and
10718
+ // the empty `game.js` placeholder. Touching anything else risks
10719
+ // nuking developer-authored content that happens to live in
10720
+ // wasmcode/ for unrelated reasons.
10721
+ if (entry.endsWith('.br') || entry === 'game.js') {
10722
+ fs.rmSync(path.join(originDir, entry), { force: true });
10723
+ }
10506
10724
  }
10507
10725
  }
10726
+ const destWasmBrPath = path.join(originDir, path.basename(cachedWasmBr));
10727
+ ensureDirSync(path.dirname(destWasmBrPath));
10728
+ fs.copyFileSync(path.join(cacheDir, cachedWasmBr), destWasmBrPath);
10508
10729
  }
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.
10730
+ else {
10731
+ // No cached original. Do NOT delete whatever wasm is currently on disk —
10732
+ // it is all the project has left. Preserve it so the project can still
10733
+ // boot and a fresh prepare can re-seed the cache, rather than wiping it and
10734
+ // crashing the next prepare. The split-output dirs below are still cleaned.
10735
+ console.warn('[wasmtool] restoreFromCache: no cached original wasm found in ' +
10736
+ `${WASM_SPLIT_CACHE_DIR}; skipping wasmcode/*.br cleanup to avoid ` +
10737
+ 'leaving the project without a wasm binary.');
10738
+ }
10739
+ // Only roll back the split config + game.json when we actually restored the
10740
+ // original wasm above. Restoring game.json (which points at the original
10741
+ // `<md5>.wasm.br`) while the matching wasm is NOT on disk recreates exactly
10742
+ // the broken state this guard exists to prevent — a game.json referencing a
10743
+ // file that isn't there. keepCacheSync writes the wasm, config and game.json
10744
+ // together, so a cache that has game.json but no `.br` is partial/corrupt and
10745
+ // must not be applied.
10746
+ if (cachedWasmBr) {
10747
+ const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
10748
+ if (fs.existsSync(splitConfigCachePath)) {
10749
+ fs.copyFileSync(splitConfigCachePath, path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME));
10750
+ }
10751
+ const gameJsonCachePath = path.join(cacheDir, 'game.json');
10752
+ if (fs.existsSync(gameJsonCachePath)) {
10753
+ fs.copyFileSync(gameJsonCachePath, path.join(entryDir, 'game.json'));
10754
+ }
10755
+ }
10756
+ // Restore wasmcode/game.js. We just deleted whatever was there in the
10757
+ // cleanup loop above, so we always need to put something back when wasmcode
10758
+ // is a subpackage. Scoped to the same `cachedWasmBr` guard: in the degraded
10759
+ // (lost-cache) path we left the existing wasmcode/ untouched, so we must not
10760
+ // rewrite its game.js either.
10529
10761
  // Strategy:
10530
10762
  // - Prefer the cache (keepCacheSync stashes pre-split contents to
10531
10763
  // `__unity_cache__/wasmcode-game.js` when the original existed)
@@ -10536,14 +10768,16 @@ function restoreFromCache(entryDir = process.cwd()) {
10536
10768
  // downloadSplited.ts also writes; it satisfies the platform requirement
10537
10769
  // without changing semantics for projects that don't use wasmcode as
10538
10770
  // 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');
10771
+ if (cachedWasmBr) {
10772
+ const originGameJsCachePath = path.join(cacheDir, 'wasmcode-game.js');
10773
+ const originGameJsDestPath = path.join(originDir, 'game.js');
10774
+ if (fs.existsSync(originDir)) {
10775
+ if (fs.existsSync(originGameJsCachePath)) {
10776
+ fs.copyFileSync(originGameJsCachePath, originGameJsDestPath);
10777
+ }
10778
+ else {
10779
+ fs.writeFileSync(originGameJsDestPath, '', 'utf-8');
10780
+ }
10547
10781
  }
10548
10782
  }
10549
10783
  for (const subDir of SPLIT_OUTPUT_DIRS) {
@@ -10661,19 +10895,51 @@ function setLocalState(partial) {
10661
10895
  Object.assign(state, partial);
10662
10896
  }
10663
10897
 
10898
+ /**
10899
+ * Prepare overwrites the project wasm in place with the instrumented build.
10900
+ * Mute the dev-server file watcher for the duration so the in-place rewrite
10901
+ * doesn't fire a compile + device reload while the file is still being
10902
+ * written, then emit a single update once the instrumented wasm is on disk.
10903
+ */
10664
10904
  async function startPrepare$1(params) {
10905
+ return withWatchSuspended(() => startPrepareImpl(params), {
10906
+ emitOnFinish: true,
10907
+ });
10908
+ }
10909
+ async function startPrepareImpl(params) {
10665
10910
  const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
10666
10911
  ensureDirSync(tempDir);
10667
10912
  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
- }
10913
+ const rawWasmPath = path$1.join(tempDir, 'original.wasm');
10675
10914
  const preparedWasmPath = path$1.join(tempDir, 'prepared.wasm');
10676
10915
  try {
10916
+ // The project wasm referenced by game.json can legitimately be missing on
10917
+ // disk: a previous rollback may have wiped `wasmcode/*.br` without a cached
10918
+ // original to restore, the project may have been duplicated without
10919
+ // `__TTMG_TEMP__`, or Unity was re-exported. Detect it up front and return
10920
+ // a structured error. Previously the `decompressWasmFile` / `copyFileSync`
10921
+ // below sat OUTSIDE this try block, so a missing input threw ENOENT out of
10922
+ // the route handler as an unhandled rejection and crashed the whole
10923
+ // dev-server process instead of surfacing a recoverable error to the IDE.
10924
+ if (!fs$1.existsSync(inputPath)) {
10925
+ return {
10926
+ data: null,
10927
+ error: {
10928
+ code: 404,
10929
+ message: `WASM file not found: ${params.wasm_file_path}. ` +
10930
+ 'The original wasm is missing — a previous "放弃分包" may have removed it ' +
10931
+ 'without a cached original to restore. Re-export the Unity build (or restore ' +
10932
+ 'the original wasm) and try preparing again.',
10933
+ },
10934
+ ctx: { logid: 'local', httpStatusCode: 404 },
10935
+ };
10936
+ }
10937
+ if (inputPath.endsWith('.br')) {
10938
+ await decompressWasmFile(inputPath, rawWasmPath);
10939
+ }
10940
+ else {
10941
+ fs$1.copyFileSync(inputPath, rawWasmPath);
10942
+ }
10677
10943
  const result = ttmgWasmtool.prepare(rawWasmPath, preparedWasmPath);
10678
10944
  verboseLog(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
10679
10945
  const gameJson = getGameJson();
@@ -11244,7 +11510,18 @@ function updateSubpackageConfigSync(archive = false) {
11244
11510
  fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
11245
11511
  }
11246
11512
 
11247
- async function downloadSplited$1(_context) {
11513
+ /**
11514
+ * Lays down the split outputs (`wasmcode*` dirs, game.json, webgl-wasm-split.js)
11515
+ * into the project root — a burst of file writes. Mute the watcher across the
11516
+ * whole operation so it doesn't reload the device per file mid-write, then fire
11517
+ * one update once the split layout is fully on disk.
11518
+ */
11519
+ async function downloadSplited$1(context) {
11520
+ return withWatchSuspended(() => downloadSplitedImpl(context), {
11521
+ emitOnFinish: true,
11522
+ });
11523
+ }
11524
+ async function downloadSplitedImpl(_context) {
11248
11525
  const cwd = process.cwd();
11249
11526
  const { splitMeta } = getLocalState();
11250
11527
  if (!splitMeta) {
@@ -11567,7 +11844,17 @@ async function downloadOne(opts) {
11567
11844
  // so the subpackage loader doesn't complain about missing js entries.
11568
11845
  fs.writeFileSync(path.join(path.dirname(out), 'game.js'), '', 'utf-8');
11569
11846
  }
11847
+ /**
11848
+ * Remote-mode counterpart of `downloadSplited`: downloads the server-built
11849
+ * split artifacts and copies them into the project root. Same watcher gating
11850
+ * rationale — mute across the burst of writes, emit one update at the end.
11851
+ */
11570
11852
  async function downloadSplitedRemote(context) {
11853
+ return withWatchSuspended(() => downloadSplitedRemoteImpl(context), {
11854
+ emitOnFinish: true,
11855
+ });
11856
+ }
11857
+ async function downloadSplitedRemoteImpl(context) {
11571
11858
  const cwd = process.cwd();
11572
11859
  const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
11573
11860
  ensureDirSync(splitTempDir);
@@ -12666,6 +12953,7 @@ const routes = [
12666
12953
  gameDirectFeedCardCreateRoute,
12667
12954
  gameDirectFeedCardListRoute,
12668
12955
  gameDirectFeedCardScenariosRoute,
12956
+ gameDirectFeedCardStatusRoute,
12669
12957
  gameUploadRoute,
12670
12958
  gameWasmSplitConfigRoute,
12671
12959
  gameWasmSplitOptionsRoute,
@@ -12687,14 +12975,61 @@ const routes = [
12687
12975
  gamePipelineModeGetRoute,
12688
12976
  gameLanguageRoute,
12689
12977
  ];
12978
+ /**
12979
+ * Express 4 does not catch rejections from async route handlers — an
12980
+ * unhandled rejection in any handler propagates to the process and, under
12981
+ * Node's default `unhandledRejection` behavior, kills the whole dev server.
12982
+ * That's how a single missing-file ENOENT in `wasm-prepare` could take the
12983
+ * CLI down. Wrap every handler so a failing request returns a 500 instead of
12984
+ * crashing the process. Individual routes can still return structured errors;
12985
+ * this is only the last-resort net for unexpected throws.
12986
+ */
12987
+ function withErrorBoundary(handler) {
12988
+ return (req, res, next) => {
12989
+ try {
12990
+ const result = handler(req, res, next);
12991
+ if (result && typeof result.then === 'function') {
12992
+ result.catch((err) => {
12993
+ console.error(`[dev-server] unhandled error in ${req.method} ${req.path}:`, err);
12994
+ if (!res.headersSent) {
12995
+ res
12996
+ .status(500)
12997
+ .send({
12998
+ code: -1,
12999
+ error: {
13000
+ code: 500,
13001
+ message: err instanceof Error ? err.message : String(err),
13002
+ },
13003
+ });
13004
+ }
13005
+ });
13006
+ }
13007
+ }
13008
+ catch (err) {
13009
+ console.error(`[dev-server] unhandled error in ${req.method} ${req.path}:`, err);
13010
+ if (!res.headersSent) {
13011
+ res
13012
+ .status(500)
13013
+ .send({
13014
+ code: -1,
13015
+ error: {
13016
+ code: 500,
13017
+ message: err instanceof Error ? err.message : String(err),
13018
+ },
13019
+ });
13020
+ }
13021
+ }
13022
+ };
13023
+ }
12690
13024
  function registerRoutes(app, options) {
12691
13025
  const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
12692
13026
  for (const route of allRoutes) {
13027
+ const handler = withErrorBoundary(route.handler);
12693
13028
  if (route.method === 'get') {
12694
- app.get(route.path, route.handler);
13029
+ app.get(route.path, handler);
12695
13030
  }
12696
13031
  else if (route.method === 'post') {
12697
- app.post(route.path, route.handler);
13032
+ app.post(route.path, handler);
12698
13033
  }
12699
13034
  }
12700
13035
  }
@@ -13139,7 +13474,7 @@ async function upload({ clientKey, note = '--', dir, }) {
13139
13474
  }
13140
13475
  }
13141
13476
 
13142
- var version = "0.3.9";
13477
+ var version = "0.4.0-beta.1";
13143
13478
  var pkg = {
13144
13479
  version: version};
13145
13480