@ttmg/cli 0.4.0 → 0.4.1-beta.wasm1

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 (84) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/index.js +501 -39
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +1 -1
  5. package/dist/public/assets/{Card-BMss5cuV.js → Card-ASGGlJ8S.js} +1 -1
  6. package/dist/public/assets/Detail-DY__Zw0r.js +1 -0
  7. package/dist/public/assets/Detail-DY__Zw0r.js.br +0 -0
  8. package/dist/public/assets/MonetizationMode-sHN_4ybQ.js +1 -0
  9. package/dist/public/assets/MonetizationModeSummary-DLvyriLM.js +1 -0
  10. package/dist/public/assets/{SectionHeader-3b39hv3n.js → SectionHeader-B7dueBCf.js} +1 -1
  11. package/dist/public/assets/{Tag-B5gYnANu.js → Tag-BsX0d6UL.js} +1 -1
  12. package/dist/public/assets/{arrow-left-B3swWB64.js → arrow-left-uixm7Sd1.js} +1 -1
  13. package/dist/public/assets/{baseForm-Dsi8Ia0k.js → baseForm-C8XfTFyG.js} +1 -1
  14. package/dist/public/assets/baseForm-C8XfTFyG.js.br +0 -0
  15. package/dist/public/assets/boxes-C7RoIbId.js +1 -0
  16. package/dist/public/assets/{chevron-right-WZeuFw11.js → chevron-right-B1WBZrrD.js} +1 -1
  17. package/dist/public/assets/circle-check-wUxXeINd.js +1 -0
  18. package/dist/public/assets/{compass-CCsIev11.js → compass-VJNhHwqW.js} +1 -1
  19. package/dist/public/assets/index-BBR3CPkN.js +1 -0
  20. package/dist/public/assets/index-BBR3CPkN.js.br +0 -0
  21. package/dist/public/assets/{index-uquhwGkB.js → index-BH099q_U.js} +1 -1
  22. package/dist/public/assets/{index-ROKxx4f7.css → index-BJZ4gOGZ.css} +1 -1
  23. package/dist/public/assets/index-BJZ4gOGZ.css.br +0 -0
  24. package/dist/public/assets/{index-D0xEiy7C.js → index-BNc8JtET.js} +1 -1
  25. package/dist/public/assets/{index-JNUqDBWt.js → index-BO5MBsdm.js} +1 -1
  26. package/dist/public/assets/index-Bd-f8ERX.js +1 -0
  27. package/dist/public/assets/{index-DeL2bgxo.js → index-C1C34gtn.js} +1 -1
  28. package/dist/public/assets/index-C7Tmy9g3.js +1 -0
  29. package/dist/public/assets/index-CCKb9Y-j.js +14 -0
  30. package/dist/public/assets/index-CCKb9Y-j.js.br +0 -0
  31. package/dist/public/assets/index-CKhfKyA3.js +1 -0
  32. package/dist/public/assets/index-CKhfKyA3.js.br +0 -0
  33. package/dist/public/assets/{index-C9Un1hFP.js → index-C_npSvtQ.js} +1 -1
  34. package/dist/public/assets/{index-BOl1-Siv.css → index-DHXlsTU6.css} +1 -1
  35. package/dist/public/assets/index-DHXlsTU6.css.br +0 -0
  36. package/dist/public/assets/index-DMsjcIN8.js +1 -0
  37. package/dist/public/assets/index-DNd8ZTok.js +1 -0
  38. package/dist/public/assets/index-DNd8ZTok.js.br +0 -0
  39. package/dist/public/assets/{index-DjY3Igd6.js → index-DbxY65Om.js} +1 -1
  40. package/dist/public/assets/index-Dn5HHf-0.js +1 -0
  41. package/dist/public/assets/index-DpkktYot.css +1 -0
  42. package/dist/public/assets/index-DtoDrber.js +1 -0
  43. package/dist/public/assets/index-DtoDrber.js.br +0 -0
  44. package/dist/public/assets/index-Dx5VGEQm.css +1 -0
  45. package/dist/public/assets/{index-Cigxnpav.js → index-ZHPtAdJM.js} +1 -1
  46. package/dist/public/assets/index-ZHPtAdJM.js.br +0 -0
  47. package/dist/public/assets/{index-C9HXjiVu.js → index-ZtlCPCV9.js} +1 -1
  48. package/dist/public/assets/{index-B-AeuNlL.css → index-_6n0s04V.css} +1 -1
  49. package/dist/public/assets/index-_6n0s04V.css.br +0 -0
  50. package/dist/public/assets/{index-C2NWNiPX.js → index-a-rMUS2V.js} +1 -1
  51. package/dist/public/assets/index-bQUQ2Wmz.js +1 -0
  52. package/dist/public/assets/{index-HYSh4-Ri.js → index-qqnHwFjO.js} +1 -1
  53. package/dist/public/assets/{index-aqIfJUqW.js → index-r3aSeNL1.js} +1 -1
  54. package/dist/public/assets/{index-D1oGAPRa.js → index-s_30KWiA.js} +1 -1
  55. package/dist/public/assets/index-t4JeqZ4a.js +1 -0
  56. package/dist/public/assets/layers-Biv61qtE.js +1 -0
  57. package/dist/public/assets/{sparkles-D2QPhLAu.js → sparkles-Bv2GxrFr.js} +1 -1
  58. package/dist/public/assets/{zap-Cv4X8yRx.js → zap-CjkC_qWZ.js} +1 -1
  59. package/dist/public/index.html +2 -2
  60. package/package.json +1 -1
  61. package/dist/public/assets/Detail-Lyas-t6F.js +0 -1
  62. package/dist/public/assets/Detail-Lyas-t6F.js.br +0 -0
  63. package/dist/public/assets/MonetizationMode-DI4fy3U7.js +0 -1
  64. package/dist/public/assets/MonetizationModeSummary-B6yEB26H.js +0 -1
  65. package/dist/public/assets/baseForm-Dsi8Ia0k.js.br +0 -0
  66. package/dist/public/assets/index-B-AeuNlL.css.br +0 -0
  67. package/dist/public/assets/index-B3-gsm5-.js +0 -1
  68. package/dist/public/assets/index-B3L1GnMP.js +0 -1
  69. package/dist/public/assets/index-B3L1GnMP.js.br +0 -0
  70. package/dist/public/assets/index-B6NHbQwP.js +0 -1
  71. package/dist/public/assets/index-BJV8-tz8.js +0 -1
  72. package/dist/public/assets/index-BOl1-Siv.css.br +0 -0
  73. package/dist/public/assets/index-BqSAtEgq.js +0 -1
  74. package/dist/public/assets/index-CV2u_S0E.js +0 -1
  75. package/dist/public/assets/index-CV2u_S0E.js.br +0 -0
  76. package/dist/public/assets/index-Ci-lqt1V.js +0 -1
  77. package/dist/public/assets/index-Ci-lqt1V.js.br +0 -0
  78. package/dist/public/assets/index-Cigxnpav.js.br +0 -0
  79. package/dist/public/assets/index-DpWpqPKC.js +0 -1
  80. package/dist/public/assets/index-DpWpqPKC.js.br +0 -0
  81. package/dist/public/assets/index-Dvg_oNs7.css +0 -1
  82. package/dist/public/assets/index-DxISN0Xc.js +0 -14
  83. package/dist/public/assets/index-DxISN0Xc.js.br +0 -0
  84. package/dist/public/assets/index-ROKxx4f7.css.br +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 {
@@ -9912,7 +9921,7 @@ const gameDirectFeedCardAssetPreviewRoute = {
9912
9921
  },
9913
9922
  };
9914
9923
 
9915
- async function resolveAppIdentity$2(body) {
9924
+ async function resolveAppIdentity$3(body) {
9916
9925
  const clientKey = body?.clientKey?.trim() ||
9917
9926
  body?.client_key?.trim() ||
9918
9927
  getClientKey().clientKey?.trim();
@@ -9979,7 +9988,7 @@ const gameDirectFeedCardCreateRoute = {
9979
9988
  method: 'post',
9980
9989
  path: '/game/direct-feed-card/create',
9981
9990
  handler: async (req, res) => {
9982
- const identity = await resolveAppIdentity$2(req.body);
9991
+ const identity = await resolveAppIdentity$3(req.body);
9983
9992
  if (identity.error) {
9984
9993
  res.send({
9985
9994
  code: errorCode,
@@ -10035,7 +10044,7 @@ function isUsableDirectFeedCard(card) {
10035
10044
  }
10036
10045
  return true;
10037
10046
  }
10038
- async function resolveAppIdentity$1(body) {
10047
+ async function resolveAppIdentity$2(body) {
10039
10048
  const clientKey = body?.clientKey?.trim() ||
10040
10049
  body?.client_key?.trim() ||
10041
10050
  getClientKey().clientKey?.trim();
@@ -10080,7 +10089,7 @@ const gameDirectFeedCardListRoute = {
10080
10089
  method: 'post',
10081
10090
  path: '/game/direct-feed-card/list',
10082
10091
  handler: async (req, res) => {
10083
- const identity = await resolveAppIdentity$1(req.body);
10092
+ const identity = await resolveAppIdentity$2(req.body);
10084
10093
  if (identity.error) {
10085
10094
  res.send({
10086
10095
  code: errorCode,
@@ -10185,7 +10194,7 @@ function extractScenarioOptions(data) {
10185
10194
  }
10186
10195
  return [];
10187
10196
  }
10188
- async function resolveAppIdentity(body) {
10197
+ async function resolveAppIdentity$1(body) {
10189
10198
  const clientKey = body?.clientKey?.trim() ||
10190
10199
  body?.client_key?.trim() ||
10191
10200
  getClientKey().clientKey?.trim();
@@ -10230,7 +10239,7 @@ const gameDirectFeedCardScenariosRoute = {
10230
10239
  method: 'post',
10231
10240
  path: '/game/direct-feed-card/scenarios',
10232
10241
  handler: async (req, res) => {
10233
- const identity = await resolveAppIdentity(req.body);
10242
+ const identity = await resolveAppIdentity$1(req.body);
10234
10243
  if (identity.error) {
10235
10244
  res.send({
10236
10245
  code: errorCode,
@@ -10270,6 +10279,112 @@ const gameDirectFeedCardScenariosRoute = {
10270
10279
  },
10271
10280
  };
10272
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
+
10273
10388
  const gameDetailRoute = {
10274
10389
  method: 'get',
10275
10390
  path: '/game/detail',
@@ -11203,36 +11318,79 @@ async function startSplit$1({ client_key, wasm_md5, }) {
11203
11318
  ctx: { logid: 'local', httpStatusCode: 400 },
11204
11319
  };
11205
11320
  }
11321
+ // ── Local boot-miss feedback (no server change) ──────────────────────
11322
+ // The runtime logs every sub-package function it had to pull in BEFORE the
11323
+ // first frame as `[wasmcode2] ... firstFrame=BEFORE` — each one is a
11324
+ // first-screen hitch. Drop those func ids into
11325
+ // `__TTMG_TEMP__/boot_extra_func_ids.txt` (one per line) and they get
11326
+ // merged into `alwaysInclude` so the next split pins them into the main
11327
+ // package, closing the gap WITHOUT re-enabling the indirect-closure flood.
11328
+ // No-op when the file is absent.
11329
+ const bootExtraPath = path$1.join(tempDir, 'boot_extra_func_ids.txt');
11330
+ let bootExtra = [];
11331
+ if (fs$1.existsSync(bootExtraPath)) {
11332
+ bootExtra = fs$1
11333
+ .readFileSync(bootExtraPath, 'utf-8')
11334
+ .split('\n')
11335
+ .map(l => parseInt(l.trim(), 10))
11336
+ .filter(n => Number.isInteger(n) && n > 0);
11337
+ }
11338
+ const alwaysIncludeIds = Array.from(new Set([...bootFuncIds, ...bootExtra]));
11339
+ // ── Collect coverage signal ──────────────────────────────────────────
11340
+ // With the indirect-call closure off, the main package ≈ the collected
11341
+ // set, so collect coverage now directly determines first-screen safety.
11342
+ // Surface it, and warn (never block) when it looks too thin to cover boot.
11343
+ const totalFuncs = Number(getGameJson()?.wasmFuncCount) || getLocalState().totalWasmFuncCount || 0;
11344
+ const coveragePct = totalFuncs ? (funcIds.length / totalFuncs) * 100 : 0;
11206
11345
  verboseLog(`[wasmtool] splitting with ${funcIds.length} func IDs` +
11346
+ ` (coverage ${coveragePct.toFixed(1)}% of ${totalFuncs})` +
11207
11347
  (bootFuncIds.length > 0
11208
- ? `, ${bootFuncIds.length} boot-phase func IDs (→ alwaysInclude)`
11209
- : ', no boot-phase info (legacy server, falling back to callClosure only)') +
11210
- `, archive=${archive}`);
11348
+ ? `, ${bootFuncIds.length} boot-phase func IDs`
11349
+ : ', no boot-phase info (legacy server, relying on collect + callClosure)') +
11350
+ (bootExtra.length > 0 ? `, +${bootExtra.length} boot-extra(local)` : '') +
11351
+ `, ${alwaysIncludeIds.length} → alwaysInclude, archive=${archive}`);
11352
+ // THIN_COLLECT_FLOOR is a heuristic, not a hard rule — tune from real
11353
+ // first-screen sizes. Warn-only: a thin collect with no boot hints means
11354
+ // first-screen functions may be served from the sub package before the
11355
+ // first frame (a visible hitch).
11356
+ const THIN_COLLECT_FLOOR = 8000;
11357
+ if (alwaysIncludeIds.length === 0 && funcIds.length < THIN_COLLECT_FLOOR) {
11358
+ verboseWarn(`[wasmtool] collect looks thin (${funcIds.length} funcs, no boot hints). ` +
11359
+ `First package may miss first-screen functions → pre-first-frame sub-package loads. ` +
11360
+ `Collect more scenarios (resume) before splitting, or add ids to ${bootExtraPath}.`);
11361
+ }
11211
11362
  try {
11212
11363
  const result = ttmgWasmtool.split({
11213
11364
  input: rawWasmPath,
11214
11365
  funcIds,
11215
- // Boot-phase func ids `alwaysInclude`. They are a subset of
11216
- // `funcIds` so this doesn't grow `collect_count`, but it DOES seed
11217
- // the direct-call closure BFS with the exact set needed for first
11218
- // frame, and the split tool's `alwaysIncludeAdded` counter is the
11219
- // observability signal when zero (= server didn't return boot info).
11220
- alwaysInclude: bootFuncIds.length > 0 ? bootFuncIds : undefined,
11366
+ // Boot-phase func ids (server) local boot-extra feedback
11367
+ // `alwaysInclude`. These seed the direct-call closure BFS with the
11368
+ // exact set needed for the first frame and force first-screen
11369
+ // functions into main; the split tool's `alwaysIncludeAdded` counter
11370
+ // is the observability signal (0 = no boot hints from any source).
11371
+ alwaysInclude: alwaysIncludeIds.length > 0 ? alwaysIncludeIds : undefined,
11221
11372
  // Always-on direct-call closure over (collect ∪ alwaysInclude ∪
11222
11373
  // start_func). Folds in func ids that collect missed (untaken
11223
11374
  // branches, race conditions during collect) so first-screen code
11224
11375
  // paths don't trap on archive trampolines. See the split tool's
11225
11376
  // `closure_added` counter for the per-build size impact.
11226
11377
  callClosure: true,
11227
- // Always-on indirect-call type-closure scoped to the boot subset.
11228
- // Catches IL2CPP virtual / interface / delegate dispatch which is
11229
- // the dominant source of remaining `firstFrame=BEFORE` archive
11230
- // trampoline hits after the runtime collect + direct closure
11231
- // passes (see `indirectClosureAdded` for the per-build size
11232
- // impact). Defaults to `true` in the wasmtool but we set it
11233
- // explicitly so a future tool default change can't silently turn
11234
- // it off in our pipeline.
11235
- callIndirectClosure: true,
11378
+ // OFF: the indirect-call type-closure is a static over-approximation
11379
+ // that pulls EVERY table function whose signature matches one used by
11380
+ // a boot root. In IL2CPP builds ~90% of functions live in the indirect
11381
+ // table and share only a few hundred signatures, so any non-trivial
11382
+ // boot set touches them all and this broadcasts almost the entire
11383
+ // module into main measured ~135k/136k funcs (~50MB) main, defeating
11384
+ // the split. First-screen indirect-call targets are instead covered
11385
+ // precisely by the runtime collect (logCall is injected into every
11386
+ // function body, so any function that actually runs before first frame
11387
+ // — including call_indirect targets — is already in `funcIds`) plus the
11388
+ // direct-call closure and `alwaysInclude`. Any residual first-frame
11389
+ // indirect hit is served on-demand by the archive loader at runtime;
11390
+ // keep those rare by improving collect-session coverage, not by
11391
+ // re-enabling this flood. Set explicitly so the wasmtool default
11392
+ // (currently `true`) can't silently turn it back on.
11393
+ callIndirectClosure: false,
11236
11394
  outputDir: splitOutputDir,
11237
11395
  archive,
11238
11396
  compress: true,
@@ -12826,6 +12984,306 @@ const gamePipelineModeGetRoute = {
12826
12984
  },
12827
12985
  };
12828
12986
 
12987
+ /**
12988
+ * 启动链路子包收集 —— DevTool CLI 侧契约类型。
12989
+ *
12990
+ * 协议字段与设计方案保持一致(见
12991
+ * topics/performance/startup-subpackage-collection-technical-design.md)。
12992
+ * `entry` 在协议层用单字段 name 表示:主包入口为内部约定 `__GAME__`,
12993
+ * 独立分包入口为该独立分包在源码 `subpackages[]` 中的 name。
12994
+ */
12995
+ /** 平台内部主包标识,只活在 DevTool / 客户端 / 编译服务等平台组件之间。 */
12996
+ const GAME_ENTRY = '__GAME__';
12997
+ /** 启动子包预下载配置字段名(源码侧 + 编译产物侧同名)。 */
12998
+ const PRELOAD_FIELD_NAME = 'parallelPreloadSubpackages';
12999
+
13000
+ /**
13001
+ * 从磁盘**新鲜读取**源码 game.json(不走 getGameJson 缓存,因为本流程要写)。
13002
+ * game.json 不存在或解析失败时抛错,由上层转成 failed 状态。
13003
+ */
13004
+ function readRawGameJson() {
13005
+ const filePath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
13006
+ if (!fs__namespace.existsSync(filePath)) {
13007
+ throw new Error('game.json 不存在');
13008
+ }
13009
+ const raw = fs__namespace.readFileSync(filePath, 'utf-8');
13010
+ let json;
13011
+ try {
13012
+ json = JSON.parse(raw);
13013
+ }
13014
+ catch {
13015
+ throw new Error('game.json 解析失败');
13016
+ }
13017
+ const subpackagesField = SUBPACKAGE_FIELD_NAMES.find(k => Array.isArray(json[k])) ??
13018
+ SUBPACKAGE_FIELD_NAMES[0];
13019
+ return { filePath, raw, json, subpackagesField };
13020
+ }
13021
+ /** 只读读取一份 game.json;不存在 / 解析失败时返回 null,不抛错。 */
13022
+ function tryReadGameJson() {
13023
+ try {
13024
+ return readRawGameJson();
13025
+ }
13026
+ catch {
13027
+ return null;
13028
+ }
13029
+ }
13030
+ /** 取 subpackages 数组(保证返回数组)。 */
13031
+ function getSubpackages(game) {
13032
+ const list = game.json[game.subpackagesField];
13033
+ return Array.isArray(list) ? list : [];
13034
+ }
13035
+ /** 按 name 在 subpackages[] 中定位 entry。 */
13036
+ function findSubpackage(game, name) {
13037
+ return getSubpackages(game).find(s => s?.name === name);
13038
+ }
13039
+ /**
13040
+ * 把对象就地写回磁盘,沿用仓库 JSON 缩进 / 换行约定。
13041
+ */
13042
+ function writeGameJson(game) {
13043
+ fs__namespace.writeFileSync(game.filePath, JSON.stringify(game.json, null, JSON_INDENT) + JSON_EOL);
13044
+ }
13045
+ /** 失败回滚:把原始文本原样写回。 */
13046
+ function restoreGameJson(game) {
13047
+ fs__namespace.writeFileSync(game.filePath, game.raw);
13048
+ }
13049
+ /** entry 是否为主包入口。 */
13050
+ function isGameEntry(entry) {
13051
+ return entry === GAME_ENTRY;
13052
+ }
13053
+
13054
+ /**
13055
+ * 写入前校验:通用规则 + 独立分包入口增量规则。
13056
+ * 任意一条不满足直接抛 Error(上层转 failed,不写 game.json)。
13057
+ * 返回过滤掉防御性条目后的待写入列表(含 root)。
13058
+ */
13059
+ function validateCollect(game, entry, reported) {
13060
+ const subpackages = getSubpackages(game);
13061
+ if (!Array.isArray(game.json[game.subpackagesField])) {
13062
+ throw new Error('game.json.subpackages 缺失或不是数组');
13063
+ }
13064
+ // 独立分包入口增量校验
13065
+ if (!isGameEntry(entry)) {
13066
+ const entryPkg = findSubpackage(game, entry);
13067
+ if (!entryPkg) {
13068
+ throw new Error(`独立分包入口 ${entry} 不存在于 game.json.subpackages`);
13069
+ }
13070
+ if (entryPkg.independent !== true) {
13071
+ throw new Error(`入口 ${entry} 不是独立分包(independent !== true),不可作为启动入口采集`);
13072
+ }
13073
+ }
13074
+ const nameToPkg = new Map(subpackages.map(s => [s?.name, s]));
13075
+ const result = [];
13076
+ const seen = new Set();
13077
+ for (const item of reported) {
13078
+ const subPkgName = item?.subPkgName;
13079
+ // 客户端不应上报 __GAME__ 作为子包名:防御性忽略,不入库、不阻塞
13080
+ if (!subPkgName || subPkgName === GAME_ENTRY) {
13081
+ continue;
13082
+ }
13083
+ // 独立分包不预下载自己
13084
+ if (!isGameEntry(entry) && subPkgName === entry) {
13085
+ throw new Error(`独立分包入口 ${entry} 不能预下载自身`);
13086
+ }
13087
+ if (seen.has(subPkgName)) {
13088
+ continue;
13089
+ }
13090
+ seen.add(subPkgName);
13091
+ const pkg = nameToPkg.get(subPkgName);
13092
+ if (!pkg) {
13093
+ throw new Error(`子包 ${subPkgName} 未定义在 game.json.subpackages`);
13094
+ }
13095
+ if (!pkg.root || typeof pkg.root !== 'string') {
13096
+ throw new Error(`子包 ${subPkgName} 缺少 root 字段`);
13097
+ }
13098
+ const absDir = path__namespace.isAbsolute(pkg.root)
13099
+ ? pkg.root
13100
+ : path__namespace.join(process.cwd(), pkg.root);
13101
+ if (!fs__namespace.existsSync(absDir) || !fs__namespace.statSync(absDir).isDirectory()) {
13102
+ throw new Error(`子包 ${subPkgName} 的目录不存在:${pkg.root}`);
13103
+ }
13104
+ result.push({
13105
+ subPkgName,
13106
+ isStartup: !!item.isStartup,
13107
+ root: pkg.root,
13108
+ });
13109
+ }
13110
+ return result;
13111
+ }
13112
+
13113
+ /** 读出某个 entry 当前的 parallelPreloadSubpackages(保证返回数组)。 */
13114
+ function readEntryConfig(game, entry) {
13115
+ const container = isGameEntry(entry)
13116
+ ? game.json
13117
+ : findSubpackage(game, entry);
13118
+ const list = container?.[PRELOAD_FIELD_NAME];
13119
+ return Array.isArray(list) ? list : [];
13120
+ }
13121
+ /**
13122
+ * 按 entry 只读读取源码 game.json 中已有的 parallelPreloadSubpackages,
13123
+ * 用于 UI 初始化配置预览。game.json 不存在 / 字段缺失时返回空数组,不阻塞。
13124
+ */
13125
+ function readPreloadSubpackagesConfig(entry) {
13126
+ const game = tryReadGameJson();
13127
+ if (!game) {
13128
+ return { entry, parallelPreloadSubpackages: [] };
13129
+ }
13130
+ return { entry, parallelPreloadSubpackages: readEntryConfig(game, entry) };
13131
+ }
13132
+ /**
13133
+ * 列出某个 entry 下可被采集(可预下载)的候选子包,供 UI 基于真实可用包选择。
13134
+ *
13135
+ * 候选 = game.json.subpackages 中 root 目录真实存在、且 name 不等于当前 entry
13136
+ * 的子包(独立分包入口不可预下载自身)。仅供 mock 上报基于真实可用包选择,
13137
+ * 不含 size —— size 在编译期由 ttmg-compile 按压缩加密后体积回填。
13138
+ * game.json 不存在 / 字段缺失时返回空数组,不阻塞。
13139
+ */
13140
+ function listPreloadSubpackageCandidates(entry) {
13141
+ const game = tryReadGameJson();
13142
+ if (!game) {
13143
+ return { entry, candidates: [], totalSubpackages: 0 };
13144
+ }
13145
+ const subpackages = getSubpackages(game);
13146
+ const candidates = [];
13147
+ for (const sub of subpackages) {
13148
+ const subPkgName = sub?.name;
13149
+ const root = sub?.root;
13150
+ if (typeof subPkgName !== 'string' || !subPkgName)
13151
+ continue;
13152
+ if (subPkgName === entry)
13153
+ continue;
13154
+ if (typeof root !== 'string' || !root)
13155
+ continue;
13156
+ const absDir = path__namespace.isAbsolute(root)
13157
+ ? root
13158
+ : path__namespace.join(process.cwd(), root);
13159
+ if (!fs__namespace.existsSync(absDir) || !fs__namespace.statSync(absDir).isDirectory())
13160
+ continue;
13161
+ candidates.push({
13162
+ subPkgName,
13163
+ root,
13164
+ independent: sub.independent === true,
13165
+ });
13166
+ }
13167
+ return { entry, candidates, totalSubpackages: subpackages.length };
13168
+ }
13169
+ /**
13170
+ * 收集快照写入:校验 → 按 entry 覆盖写入对应位置 → 失败回滚。
13171
+ *
13172
+ * 只写入 `subPkgName` + `isStartup`,**不写 size**:size 是压缩+加密后的真实
13173
+ * 下发体积,由编译服务(ttmg-compile)在产出 STTPKG 时回填到编译产物,收集期
13174
+ * 拿不到也不写(见技术方案 3.4 / 3.5)。
13175
+ *
13176
+ * - 写入前校验失败:抛错,不修改 game.json。
13177
+ * - 进入写入阶段后异常:回滚原始 game.json 再抛错。
13178
+ * 返回本次 entry 下最终写入的 parallelPreloadSubpackages。
13179
+ */
13180
+ function collectPreloadSubpackages(entry, reported) {
13181
+ // readRawGameJson 抛错即「game.json 不存在 / 解析失败」,写入前阶段,不动文件
13182
+ const game = readRawGameJson();
13183
+ const validated = validateCollect(game, entry, reported);
13184
+ const parallelPreloadSubpackages = validated.map(item => ({
13185
+ subPkgName: item.subPkgName,
13186
+ isStartup: item.isStartup,
13187
+ }));
13188
+ let started = false;
13189
+ try {
13190
+ const container = isGameEntry(entry)
13191
+ ? game.json
13192
+ : findSubpackage(game, entry);
13193
+ if (!container) {
13194
+ // 独立分包入口已在 validate 阶段确认存在,这里是防御
13195
+ throw new Error(`入口 ${entry} 不存在`);
13196
+ }
13197
+ started = true;
13198
+ container[PRELOAD_FIELD_NAME] = parallelPreloadSubpackages;
13199
+ writeGameJson(game);
13200
+ }
13201
+ catch (err) {
13202
+ if (started) {
13203
+ restoreGameJson(game);
13204
+ }
13205
+ throw err;
13206
+ }
13207
+ return { entry, parallelPreloadSubpackages };
13208
+ }
13209
+
13210
+ /**
13211
+ * 读取某个启动入口已有的 parallelPreloadSubpackages(只读),用于 UI 初始化
13212
+ * 配置预览。对应设计方案的 `getPreloadSubpackagesConfig`。
13213
+ *
13214
+ * query: `entry`(缺省按主包 `__GAME__` 处理)。
13215
+ */
13216
+ const gamePreloadSubpackagesConfigRoute = {
13217
+ method: 'get',
13218
+ path: '/game/preload-subpackages-config',
13219
+ handler: async (req, res) => {
13220
+ const entry = req.query.entry || GAME_ENTRY;
13221
+ try {
13222
+ const data = readPreloadSubpackagesConfig(entry);
13223
+ res.send({ code: successCode, data });
13224
+ }
13225
+ catch (err) {
13226
+ res.send({
13227
+ code: errorCode,
13228
+ error: { message: err.message },
13229
+ });
13230
+ }
13231
+ },
13232
+ };
13233
+
13234
+ /**
13235
+ * 列出某个启动入口下可被采集(可预下载)的候选子包,供 UI mock 上报时
13236
+ * 基于真实可用包选择,而不是手填名字。候选直接来自 game.json.subpackages
13237
+ * 中 root 真实存在、name 不等于当前 entry 的子包,并附带压缩前体积。
13238
+ *
13239
+ * query: `entry`(缺省按主包 `__GAME__` 处理)。
13240
+ */
13241
+ const gamePreloadSubpackageCandidatesRoute = {
13242
+ method: 'get',
13243
+ path: '/game/preload-subpackage-candidates',
13244
+ handler: async (req, res) => {
13245
+ const entry = req.query.entry || GAME_ENTRY;
13246
+ try {
13247
+ const data = listPreloadSubpackageCandidates(entry);
13248
+ res.send({ code: successCode, data });
13249
+ }
13250
+ catch (err) {
13251
+ res.send({
13252
+ code: errorCode,
13253
+ error: { message: err.message },
13254
+ });
13255
+ }
13256
+ },
13257
+ };
13258
+
13259
+ /**
13260
+ * 收集快照写入:按 entry 校验 + 补体积 + 覆盖写入源码 game.json 对应位置,
13261
+ * 失败回滚。对应设计方案的 `collectPreloadSubpackages` /
13262
+ * `updatePreloadSubpackagesConfig`(HTTP 请求-响应实现,start 态由 UI 自身的
13263
+ * 处理中状态承载,CLI 只回 success / failed)。
13264
+ *
13265
+ * body: `{ entry, subpackages: [{ subPkgName, isStartup }] }`。
13266
+ */
13267
+ const gameCollectPreloadSubpackagesRoute = {
13268
+ method: 'post',
13269
+ path: '/game/collect-preload-subpackages',
13270
+ handler: async (req, res) => {
13271
+ const entry = req.body?.entry || GAME_ENTRY;
13272
+ const subpackages = req.body?.subpackages || [];
13273
+ console.log('collect-preload-subpackages', { entry, count: subpackages.length });
13274
+ try {
13275
+ const data = collectPreloadSubpackages(entry, subpackages);
13276
+ res.send({ code: successCode, data });
13277
+ }
13278
+ catch (err) {
13279
+ res.send({
13280
+ code: errorCode,
13281
+ error: { message: err.message },
13282
+ });
13283
+ }
13284
+ },
13285
+ };
13286
+
12829
13287
  const routes = [
12830
13288
  gameAssetPreviewUrlRoute,
12831
13289
  gameAssetsRoute,
@@ -12838,6 +13296,7 @@ const routes = [
12838
13296
  gameDirectFeedCardCreateRoute,
12839
13297
  gameDirectFeedCardListRoute,
12840
13298
  gameDirectFeedCardScenariosRoute,
13299
+ gameDirectFeedCardStatusRoute,
12841
13300
  gameUploadRoute,
12842
13301
  gameWasmSplitConfigRoute,
12843
13302
  gameWasmSplitOptionsRoute,
@@ -12858,6 +13317,9 @@ const routes = [
12858
13317
  gamePipelineModeRoute,
12859
13318
  gamePipelineModeGetRoute,
12860
13319
  gameLanguageRoute,
13320
+ gamePreloadSubpackagesConfigRoute,
13321
+ gamePreloadSubpackageCandidatesRoute,
13322
+ gameCollectPreloadSubpackagesRoute,
12861
13323
  ];
12862
13324
  /**
12863
13325
  * Express 4 does not catch rejections from async route handlers — an
@@ -13358,7 +13820,7 @@ async function upload({ clientKey, note = '--', dir, }) {
13358
13820
  }
13359
13821
  }
13360
13822
 
13361
- var version = "0.4.0";
13823
+ var version = "0.4.1-beta.wasm1";
13362
13824
  var pkg = {
13363
13825
  version: version};
13364
13826