@ttmg/cli 0.1.5 → 0.1.6

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 (72) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/index.js +564 -272
  3. package/dist/index.js.map +1 -1
  4. package/dist/public/assets/index-3dSJcSdI.js +67 -0
  5. package/dist/public/assets/index-3dSJcSdI.js.br +0 -0
  6. package/dist/public/assets/index-B9XS0oB-.css +1 -0
  7. package/dist/public/assets/index-B9XS0oB-.css.br +0 -0
  8. package/dist/public/assets/index-BBms5OYm.css +1 -0
  9. package/dist/public/assets/index-BBms5OYm.css.br +0 -0
  10. package/dist/public/assets/index-BLeuZX1W.css +1 -0
  11. package/dist/public/assets/index-BLeuZX1W.css.br +0 -0
  12. package/dist/public/assets/index-BQeetFTy.css +1 -0
  13. package/dist/public/assets/index-BQeetFTy.css.br +0 -0
  14. package/dist/public/assets/index-B_Qh1IFA.js +67 -0
  15. package/dist/public/assets/index-B_Qh1IFA.js.br +0 -0
  16. package/dist/public/assets/index-BcvpKJpv.js +12 -0
  17. package/dist/public/assets/index-BcvpKJpv.js.br +0 -0
  18. package/dist/public/assets/index-BjfplLoJ.js +67 -0
  19. package/dist/public/assets/index-BjfplLoJ.js.br +0 -0
  20. package/dist/public/assets/index-Bw6Q4Jbr.js +67 -0
  21. package/dist/public/assets/index-Bw6Q4Jbr.js.br +0 -0
  22. package/dist/public/assets/index-BzM7ZRWT.js +67 -0
  23. package/dist/public/assets/index-CEfsFr9q.js +67 -0
  24. package/dist/public/assets/index-CEfsFr9q.js.br +0 -0
  25. package/dist/public/assets/index-CNGNW4V0.css +1 -0
  26. package/dist/public/assets/index-CNGNW4V0.css.br +0 -0
  27. package/dist/public/assets/index-CPKWZ0lk.js +67 -0
  28. package/dist/public/assets/index-CPKWZ0lk.js.br +0 -0
  29. package/dist/public/assets/index-CYjeGeW3.js +67 -0
  30. package/dist/public/assets/index-CYjeGeW3.js.br +0 -0
  31. package/dist/public/assets/index-CieLbLV3.js +67 -0
  32. package/dist/public/assets/index-CieLbLV3.js.br +0 -0
  33. package/dist/public/assets/index-CkfGcO9X.js +67 -0
  34. package/dist/public/assets/index-CkfGcO9X.js.br +0 -0
  35. package/dist/public/assets/index-CvRRmiYm.js +67 -0
  36. package/dist/public/assets/index-CvRRmiYm.js.br +0 -0
  37. package/dist/public/assets/index-D92wLRBC.css +1 -0
  38. package/dist/public/assets/index-D92wLRBC.css.br +0 -0
  39. package/dist/public/assets/index-DF19Ik_I.js +67 -0
  40. package/dist/public/assets/index-DF19Ik_I.js.br +0 -0
  41. package/dist/public/assets/index-DL6K0uau.css +1 -0
  42. package/dist/public/assets/index-DL6K0uau.css.br +0 -0
  43. package/dist/public/assets/index-DOZ9HWK0.js +67 -0
  44. package/dist/public/assets/index-DOZ9HWK0.js.br +0 -0
  45. package/dist/public/assets/index-DPSI2SAJ.js +67 -0
  46. package/dist/public/assets/index-DPSI2SAJ.js.br +0 -0
  47. package/dist/public/assets/index-DTOwOuUY.js +67 -0
  48. package/dist/public/assets/index-DTOwOuUY.js.br +0 -0
  49. package/dist/public/assets/index-DewGL4pl.js +12 -0
  50. package/dist/public/assets/index-DiaqC69h.js +67 -0
  51. package/dist/public/assets/index-DiaqC69h.js.br +0 -0
  52. package/dist/public/assets/index-Dsnf7n5I.css +1 -0
  53. package/dist/public/assets/index-DxEOIplv.js +67 -0
  54. package/dist/public/assets/index-DxEOIplv.js.br +0 -0
  55. package/dist/public/assets/index-VKaSrbK1.js +67 -0
  56. package/dist/public/assets/index-Wp-ZSjKf.css +1 -0
  57. package/dist/public/assets/index-a4bDRD38.js +67 -0
  58. package/dist/public/assets/index-a4bDRD38.js.br +0 -0
  59. package/dist/public/assets/index-nhTk4P_h.js +67 -0
  60. package/dist/public/assets/index-wkPXet9W.js +67 -0
  61. package/dist/public/assets/index-wkPXet9W.js.br +0 -0
  62. package/dist/public/assets/react-dom-B7KT1F-k.js +32 -0
  63. package/dist/public/assets/react-l0sNRNKZ.js +1 -0
  64. package/dist/public/assets/semi-ui-3SIY-ze1.js +25 -0
  65. package/dist/public/assets/semi-ui-BgfPE7Mt.css +1 -0
  66. package/dist/public/assets/vendor-BgfPE7Mt.css +1 -0
  67. package/dist/public/assets/vendor-BgfPE7Mt.css.br +0 -0
  68. package/dist/public/assets/vendor-CP79f9j0.js +56 -0
  69. package/dist/public/assets/vendor-CP79f9j0.js.br +0 -0
  70. package/dist/public/index.html +2 -2
  71. package/dist/template/open_context.html.hbs +0 -1
  72. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -15,14 +15,17 @@ var os = require('os');
15
15
  var child_process = require('child_process');
16
16
  var https = require('https');
17
17
  var semver = require('semver');
18
+ var axios = require('axios');
18
19
  var handlebars = require('handlebars');
19
20
  var esbuild = require('esbuild');
20
21
  var archiver = require('archiver');
22
+ var expressStaticGzip = require('express-static-gzip');
23
+ var chokidar = require('chokidar');
21
24
  var WebSocket = require('ws');
25
+ var ttmgPack = require('ttmg-pack');
22
26
  var glob = require('glob');
23
27
  var got = require('got');
24
28
  var FormData = require('form-data');
25
- var ttmgPack = require('ttmg-pack');
26
29
 
27
30
  function _interopNamespaceDefault(e) {
28
31
  var n = Object.create(null);
@@ -41,6 +44,8 @@ function _interopNamespaceDefault(e) {
41
44
  return Object.freeze(n);
42
45
  }
43
46
 
47
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
48
+ var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
44
49
  var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
45
50
 
46
51
  const CONFIG_FILE_NAME = 'minigame.config.json';
@@ -192,6 +197,7 @@ function init() {
192
197
 
193
198
  async function openUrl(url) {
194
199
  try {
200
+ const userDataDir = path__namespace.join(os__namespace.homedir(), '.my-chrome-dev-profile');
195
201
  await chromeLauncher.launch({
196
202
  startingUrl: url,
197
203
  chromeFlags: [
@@ -200,11 +206,11 @@ async function openUrl(url) {
200
206
  '--allow-insecure-localhost',
201
207
  '--allow-running-insecure-content',
202
208
  '--remote-allow-origins=*',
203
- '--user-data-dir=/tmp/chrome-debug-profile',
204
- '--disable-popup-blocking'
209
+ `--user-data-dir=${userDataDir}`,
205
210
  ],
206
211
  });
207
212
  await new Promise(() => { });
213
+ return true;
208
214
  }
209
215
  catch (e) {
210
216
  // const open = await import('open');
@@ -291,48 +297,143 @@ To update, run: ${chalk.magenta(`npm i -g ${pkgName}`)}
291
297
  }
292
298
  }
293
299
 
300
+ let config$1 = null;
301
+ const CONFIG_PATH = path.join(os.homedir(), '.ttmgrc');
302
+ const getTTmgrcConfig = () => {
303
+ if (config$1) {
304
+ return config$1;
305
+ }
306
+ else {
307
+ // only check one time
308
+ if (!fs.existsSync(CONFIG_PATH)) {
309
+ fs.writeFileSync(CONFIG_PATH, '{}');
310
+ return {};
311
+ }
312
+ else {
313
+ // safe parse
314
+ let res = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
315
+ config$1 = res;
316
+ return res;
317
+ }
318
+ }
319
+ };
320
+ const setTTmgrcConfig = (config) => {
321
+ // updata cache config
322
+ config = config;
323
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config));
324
+ };
325
+
294
326
  /*
295
327
  *
296
328
  * 基于开发者输入的邮箱账号和密码进行登录,登录成功后存储账号和返回的 cookie 至本地,类似 npm
297
329
  */
330
+ const LOGIN_TT4D = 'https://developers.tiktok.com/passport/web/email/login';
331
+ const params = {
332
+ aid: '2471',
333
+ account_sdk_source: 'web',
334
+ sdk_version: '2.1.1',
335
+ };
298
336
  const prompt = inquirer.createPromptModule();
299
- const CONFIG_PATH = path.join(os.homedir(), '.ttmgrc');
300
337
  async function login() {
301
- // 1. 获取用户输入
338
+ // 增加对邮箱密码的校验
302
339
  const { email, password } = await prompt([
303
- { type: 'input', name: 'email', message: '请输入邮箱账号:' },
304
- { type: 'password', name: 'password', message: '请输入密码:', mask: '*' },
340
+ {
341
+ type: 'input',
342
+ name: 'email',
343
+ message: '请输入邮箱账号:',
344
+ validate: input => {
345
+ if (!input) {
346
+ return 'email is required, please input email';
347
+ }
348
+ else {
349
+ /**
350
+ * 邮箱格式校验
351
+ */
352
+ if (!/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/.test(input)) {
353
+ return 'email format is invalid';
354
+ }
355
+ }
356
+ return true;
357
+ },
358
+ },
359
+ {
360
+ type: 'password',
361
+ name: 'password',
362
+ message: '请输入密码:',
363
+ mask: '*',
364
+ validate: input => {
365
+ if (!input) {
366
+ return 'password is required, please input password';
367
+ }
368
+ return true;
369
+ },
370
+ },
305
371
  ]);
306
- try {
307
- // 2. 请求登录接口
308
- // const response = await axios.post(
309
- // 'https://developers.tiktok.com/passport/web/email/login/?aid=2471&account_sdk_source=web&sdk_version=2.1.6-tiktok&verifyFp=verify_mfavz9g2_ycOtXIar_WIqx_42JG_8SOW_4D6YysTKunpb',
310
- // {
311
- // email,
312
- // password,
313
- // },
314
- // {
315
- // withCredentials: true,
316
- // },
317
- // );
318
- // // 3. 获取 cookie
319
- // const setCookie = response.headers['set-cookie'];
320
- // if (!setCookie) {
321
- // console.log('登录失败,未返回 cookie');
322
- // return;
323
- // }
324
- // 4. 存储账号和 cookie 到本地 ~/.ttmgrc
372
+ // const email = 'zhanghongyang.mocha@bytedance.com';
373
+ // const password = 'BOKEboke1314?';
374
+ const url = LOGIN_TT4D + '?' + new URLSearchParams(params);
375
+ const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
376
+ console.log(chalk.yellow('⏳ Please wait, logging in...'));
377
+ const response = await axios.post(url, {
378
+ email,
379
+ password,
380
+ }, { headers });
381
+ if (response?.data?.data?.error_code) {
382
+ console.error(chalk.red(`❌ login failed: ${response.data?.data?.description || response.data?.data?.error_code}`));
383
+ process.exit(1);
384
+ }
385
+ else {
325
386
  const data = {
326
387
  email,
327
- cookie: '_ttp=2wqKnK3Yb65BuVqLqeeMUjWKD7W; tt_chain_token=vxgO9iPSWB+6xYu3aO3RGw==; d_ticket=1444f6a26052f4919429f0e16689ee67c77af; ttwid=1%7CFeMoDZ8N9sTnKbwZ33DcTGk46ZFgw4IadcCzTxXFEjs%7C1756899035%7C7d27405e2db5ac486eb6ee03cfcf13a9d86625deb672466f9e3c1651c068c487; csrf_session_id=caf26d4c0ecc54e2a25a025c92fb6c4f; s_v_web_id=verify_mfavz9g2_ycOtXIar_WIqx_42JG_8SOW_4D6YysTKunpb; passport_csrf_token=acd3ee68954a195c9137cc816b9fb6d7; passport_csrf_token_default=acd3ee68954a195c9137cc816b9fb6d7; store-country-sign=MEIEDMvOxGLthOIn0j3M3gQg75hoNvqLled8LLm0cCUn4sCyqAWE8B-3Ot1PLYucx1IEEPLYaz6ok73in9EPqjHcGho; sid_guard_tt_open=1b76acc062070251b8917063b59dc729%7C1757321726%7C21600%7CMon%2C+08-Sep-2025+14%3A55%3A26+GMT; uid_tt_tt_open=f12e9238d436936d63198a211362a2ec; uid_tt_ss_tt_open=f12e9238d436936d63198a211362a2ec; sid_tt_tt_open=1b76acc062070251b8917063b59dc729; sessionid_tt_open=1b76acc062070251b8917063b59dc729; sessionid_ss_tt_open=1b76acc062070251b8917063b59dc729; sid_ucp_v1_tt_open=1.0.0-KDJkNmI5ZjkzODA4ZDRiZDBkYzEyMTBiYjg1ZTc3YjAxM2U2ZmFhYTEKCRD-s_rFBhinExADGgZtYWxpdmEiIDFiNzZhY2MwNjIwNzAyNTFiODkxNzA2M2I1OWRjNzI5; ssid_ucp_v1_tt_open=1.0.0-KDJkNmI5ZjkzODA4ZDRiZDBkYzEyMTBiYjg1ZTc3YjAxM2U2ZmFhYTEKCRD-s_rFBhinExADGgZtYWxpdmEiIDFiNzZhY2MwNjIwNzAyNTFiODkxNzA2M2I1OWRjNzI5;****',
388
+ user_id: response?.data?.data?.user_id,
389
+ cookie: response?.headers['set-cookie'].join('; '),
328
390
  };
329
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2));
330
- console.log(chalk.green.bold('Login success, your account info has saved in:'), CONFIG_PATH);
391
+ setTTmgrcConfig(data);
392
+ console.log(chalk.bold.green(' login successfully!'));
331
393
  process.exit(0);
332
394
  }
395
+ }
396
+
397
+ /**
398
+ * 获取配置文件中的 cookie
399
+ */
400
+ const config = getTTmgrcConfig();
401
+ const cookie = config.cookie;
402
+ axios.defaults.headers.common['Cookie'] = cookie;
403
+ // ppe_dev_tool
404
+ async function request({ url, method, data, headers, params, }) {
405
+ try {
406
+ const res = await axios({
407
+ url,
408
+ method,
409
+ data,
410
+ params,
411
+ headers,
412
+ });
413
+ return {
414
+ data: res.data,
415
+ error: null,
416
+ };
417
+ }
333
418
  catch (err) {
334
- console.error(chalk.red('login failed:'), err.response ? err.response.data : err.message);
335
- process.exit(1);
419
+ console.error('request failed', err.response?.data);
420
+ return {
421
+ data: null,
422
+ error: err.response?.data,
423
+ };
424
+ }
425
+ }
426
+
427
+ function getCurrentUser() {
428
+ try {
429
+ const config = getTTmgrcConfig();
430
+ return {
431
+ email: config?.email || '',
432
+ user_id: config?.user_id || '',
433
+ };
434
+ }
435
+ catch (err) {
436
+ return {};
336
437
  }
337
438
  }
338
439
 
@@ -655,15 +756,53 @@ function getOutputDir() {
655
756
 
656
757
  const DEV_PORT = 9528;
657
758
  const DEV_WS_PORT = 9529;
658
- const OUTPUT_DIR = path.join(os.homedir(), '__TTMG__');
759
+ path.join(os.homedir(), '__TTMG__');
760
+
761
+ function openDevTool() {
762
+ try {
763
+ const qrCodeUrl = `http://localhost:${DEV_PORT}`;
764
+ console.log('');
765
+ console.log(chalk.green.bold('💡 Tips for Local Debugging'));
766
+ console.log(chalk.gray('─────────────────────────────────────────────'));
767
+ console.log(chalk.yellow.bold('1.') +
768
+ ' The QR code page will be opened automatically in Chrome.');
769
+ console.log(' If it fails, please open the following link manually:');
770
+ console.log(' ' + chalk.cyan.underline('http://localhost:9528'));
771
+ console.log('');
772
+ console.log(chalk.yellow.bold('2.') +
773
+ ' The debugging service will automatically compile your game assets.');
774
+ console.log(' Any changes in your game directory will trigger recompilation.');
775
+ console.log(' You can debug the updated content when upload is completed.');
776
+ console.log('');
777
+ console.log(chalk.yellow.bold('3.') +
778
+ ' After scanning the QR code with your phone for Test User authentication,');
779
+ console.log(' the compiled code package will be uploaded to the client automatically.');
780
+ console.log(' Game debugging will start right away.');
781
+ console.log(chalk.gray('─────────────────────────────────────────────'));
782
+ console.log('');
783
+ /**
784
+ * 自动打开浏览器
785
+ */
786
+ openUrl(qrCodeUrl);
787
+ }
788
+ catch (err) {
789
+ console.error(chalk.red('Failed to generate QR code image.'), err);
790
+ }
791
+ }
659
792
 
793
+ const successCode = 0;
794
+ const publicPath = path.join(__dirname, 'public');
795
+ // 自定义静态文件服务中间件
660
796
  let server;
661
- async function createServer() {
797
+ async function start() {
662
798
  if (server) {
663
799
  closeServer();
664
800
  }
665
- const publicPath = path.join(__dirname, 'public');
666
801
  const app = express();
802
+ app.use(expressStaticGzip(publicPath, {
803
+ enableBrotli: true,
804
+ orderPreference: ['br'],
805
+ }));
667
806
  /**
668
807
  * 支持跨域请求
669
808
  */
@@ -681,20 +820,45 @@ async function createServer() {
681
820
  * 支持文件访问
682
821
  */
683
822
  const outputDir = getOutputDir();
684
- /**
685
- *
686
- */
687
- app.use(express.static(publicPath));
688
823
  /**
689
824
  * 支持静态资源
690
825
  */
691
826
  app.use('/game/files', express.static(outputDir));
692
827
  app.get('/game/config', async (req, res) => {
693
- res.send({ nodeWsPort: DEV_WS_PORT });
828
+ const { clientKey } = getClientKey();
829
+ const { email, user_id } = getCurrentUser();
830
+ res.send({
831
+ email,
832
+ user_id,
833
+ code: successCode,
834
+ nodeWsPort: DEV_WS_PORT,
835
+ clientKey: clientKey,
836
+ schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${DEV_WS_PORT}`,
837
+ });
838
+ });
839
+ app.get('/game/info', async (req, res) => {
840
+ const { clientKey } = getClientKey();
841
+ const data = await request({
842
+ url: 'https://developers.tiktok.com/tiktok/v3/devportal/minigame/info',
843
+ method: 'GET',
844
+ params: {
845
+ client_key: 'mg7oas80sthix6xy',
846
+ version_type: 1,
847
+ },
848
+ headers: {
849
+ 'x-tt-env': 'ppe_dev_tool',
850
+ 'x-use-ppe': '1',
851
+ },
852
+ });
853
+ res.send({
854
+ code: successCode,
855
+ data,
856
+ });
694
857
  });
695
858
  app.get('/game/schema', async (req, res) => {
696
859
  const { clientKey } = getClientKey();
697
860
  res.send({
861
+ code: successCode,
698
862
  schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${DEV_WS_PORT}`,
699
863
  nodeWsPort: DEV_WS_PORT,
700
864
  clientKey: clientKey,
@@ -705,7 +869,8 @@ async function createServer() {
705
869
  });
706
870
  // 启动服务
707
871
  server = app.listen(port, () => {
708
- console.log(chalk.cyan.bold(`Node devServer is running on port ${port}\n`));
872
+ console.log(chalk.green.bold(`✅ Local dev server started successfully! \n`));
873
+ openDevTool();
709
874
  });
710
875
  return {
711
876
  port,
@@ -720,6 +885,81 @@ function closeServer() {
720
885
  });
721
886
  }
722
887
 
888
+ /**
889
+ * 一个类型安全的、通用的事件发射器类。
890
+ * 它允许你注册、注销和触发具有严格类型检查的事件。
891
+ */
892
+ class TypedEventEmitter {
893
+ constructor() {
894
+ // 使用 Map 存储事件监听器。
895
+ // Key 是事件名,Value 是一个 Set,其中包含该事件的所有监听器函数。
896
+ // 使用 Set 可以自动防止同一个监听器被重复注册。
897
+ this.listeners = new Map();
898
+ }
899
+ /**
900
+ * 注册一个事件监听器。
901
+ * @param eventName 要监听的事件名称。
902
+ * @param listener 事件触发时执行的回调函数。
903
+ * @returns 返回一个函数,调用该函数即可注销此监听器,方便使用。
904
+ */
905
+ on(eventName, listener) {
906
+ if (!this.listeners.has(eventName)) {
907
+ this.listeners.set(eventName, new Set());
908
+ }
909
+ this.listeners.get(eventName).add(listener);
910
+ // 返回一个便捷的取消订阅函数
911
+ return () => this.off(eventName, listener);
912
+ }
913
+ /**
914
+ * 注销一个事件监听器。
915
+ * @param eventName 要注销的事件名称。
916
+ * @param listener 之前通过 on() 方法注册的回调函数实例。
917
+ */
918
+ off(eventName, listener) {
919
+ const eventListeners = this.listeners.get(eventName);
920
+ if (eventListeners) {
921
+ eventListeners.delete(listener);
922
+ if (eventListeners.size === 0) {
923
+ this.listeners.delete(eventName);
924
+ }
925
+ }
926
+ }
927
+ /**
928
+ * 触发一个事件,并同步调用所有相关的监听器。
929
+ * @param eventName 要触发的事件名称。
930
+ * @param payload 传递给所有监听器的数据。类型必须与事件定义匹配。
931
+ */
932
+ emit(eventName, payload) {
933
+ const eventListeners = this.listeners.get(eventName);
934
+ if (eventListeners) {
935
+ // 遍历 Set 并执行每一个监听器
936
+ eventListeners.forEach(listener => {
937
+ try {
938
+ // 在独立的 try...catch 中调用,防止一个监听器的错误影响其他监听器
939
+ listener(payload);
940
+ }
941
+ catch (error) {
942
+ console.error(`Error in listener for event "${String(eventName)}":`, error);
943
+ }
944
+ });
945
+ }
946
+ }
947
+ /**
948
+ * 注销指定事件的所有监听器。
949
+ * @param eventName 要清除监听器的事件名称。
950
+ */
951
+ removeAllListeners(eventName) {
952
+ this.listeners.delete(eventName);
953
+ }
954
+ /**
955
+ * 清空所有事件的所有监听器。
956
+ */
957
+ clearAll() {
958
+ this.listeners.clear();
959
+ }
960
+ }
961
+ const eventEmitter = new TypedEventEmitter();
962
+
723
963
  class Store {
724
964
  constructor(initialState) {
725
965
  this.listeners = [];
@@ -761,149 +1001,18 @@ const store = Store.getInstance({
761
1001
  clientWsHost: '',
762
1002
  clientKey: '',
763
1003
  packages: {},
1004
+ isUnderCompiling: false,
1005
+ isWaitingForUpload: false,
1006
+ projectInfo: {
1007
+ projectSize: 0,
1008
+ mainPkg: {
1009
+ pkgSize: 0,
1010
+ isMatchSizeLimit: false,
1011
+ },
1012
+ independentPkgs: {},
1013
+ },
764
1014
  });
765
1015
 
766
- async function uploadGame(callback) {
767
- const outputDir = getOutputDir();
768
- callback({
769
- status: 'start',
770
- percent: 0,
771
- });
772
- console.log(chalk.yellow.bold('Start compress game resource'));
773
- const zipPath = path.join(os.homedir(), '__TTMG__', 'upload.zip');
774
- await zipDirectory(outputDir, zipPath);
775
- console.log(chalk.green.bold('Compress game package resource success \n'));
776
- await uploadZip(zipPath, callback);
777
- }
778
- /**
779
- * 复制源目录内容到临时目录,然后根据 glob 模式过滤并压缩文件。
780
- * 原始目录保持不变。
781
- *
782
- * @param sourceDir - 要压缩的源文件夹路径。
783
- * @param outPath - 输出的 zip 文件路径。
784
- */
785
- async function zipDirectory(sourceDir, outPath) {
786
- // 1. 创建一个唯一的临时目录
787
- // fsp 现在是 fs.promises 的别名
788
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'zip-temp-'));
789
- try {
790
- // 2. 将源目录的所有内容复制到临时目录
791
- await fs.promises.cp(sourceDir, tempDir, { recursive: true });
792
- // 3. 对临时目录进行压缩
793
- const output = fs.createWriteStream(outPath);
794
- const archive = archiver('zip', { zlib: { level: 9 } });
795
- // 使用 Promise 包装流操作
796
- const archivePromise = new Promise((resolve, reject) => {
797
- output.on('close', () => {
798
- resolve();
799
- });
800
- archive.on('warning', err => console.warn('Archiver warning:', err));
801
- output.on('error', err => reject(err));
802
- archive.on('error', err => reject(err));
803
- });
804
- archive.pipe(output);
805
- // 4. 使用 glob 在临时目录中查找文件,并应用过滤规则
806
- const files = await glob__namespace.glob('**/*', {
807
- cwd: tempDir,
808
- nodir: true,
809
- ignore: '**/*.js.map', // 过滤规则
810
- });
811
- // 5. 将过滤后的文件逐个添加到压缩包
812
- for (const file of files) {
813
- const filePath = path.join(tempDir, file);
814
- archive.file(filePath, { name: file });
815
- }
816
- // 6. 完成压缩
817
- await archive.finalize();
818
- // 等待文件流关闭
819
- await archivePromise;
820
- }
821
- catch (err) {
822
- console.error('压缩过程中发生错误:', err);
823
- throw err;
824
- }
825
- finally {
826
- // 7. 无论成功还是失败,都清理临时目录
827
- await fs.promises.rm(tempDir, { recursive: true, force: true });
828
- }
829
- }
830
- async function uploadZip(zipPath, callback) {
831
- const form = new FormData();
832
- form.append('file', fs.createReadStream(zipPath), {
833
- filename: 'upload.zip',
834
- contentType: 'application/zip',
835
- });
836
- // 帮我计算下文件大小,变成 MB 为单位
837
- const fileSize = fs.statSync(zipPath).size / 1024 / 1024;
838
- console.log(chalk.yellow.bold(`Start upload resource to client, size: ${fileSize.toFixed(2)} MB`));
839
- const { clientHttpPort, clientHost } = store.getState();
840
- const url = `http://${clientHost}:${clientHttpPort}/game/upload`;
841
- try {
842
- // 1. 创建请求流
843
- const stream = got.stream.post(url, {
844
- body: form,
845
- });
846
- // 2. 监听上传进度 (这个回调是并行的,不影响封装)
847
- stream.on('uploadProgress', progress => {
848
- const percent = progress.percent;
849
- // const transferred = progress.transferred;
850
- // const total = progress.total;
851
- process.stdout.write(`\r${chalk.cyan('Uploading progress: ')}${chalk.green((percent * 100).toFixed(0) + '%')}`);
852
- callback({
853
- status: 'process',
854
- percent,
855
- });
856
- });
857
- // 3. 【核心封装】将流的处理过程包装在 Promise 中
858
- const response = await new Promise((resolve, reject) => {
859
- // 用于拼接响应数据
860
- const chunks = [];
861
- // 当流传输数据时,收集数据块
862
- stream.on('data', chunk => {
863
- chunks.push(chunk);
864
- });
865
- // 当流成功结束时
866
- stream.on('end', () => {
867
- // 拼接所有数据块,并转换为字符串
868
- // const responseBody = Buffer.concat(chunks).toString('utf-8');
869
- // stream.response 在流结束后才可用
870
- // 将完整的响应对象 resolve 出去
871
- callback({
872
- status: 'success',
873
- percent: 1,
874
- });
875
- resolve({
876
- statusCode: 200,
877
- });
878
- });
879
- // 当流发生错误时
880
- stream.on('error', err => {
881
- // 将错误 reject 出去,这样外层的 try...catch 就能捕获到
882
- reject(err);
883
- callback({
884
- status: 'error',
885
- percent: 0,
886
- msg: err.message,
887
- });
888
- });
889
- });
890
- // 4. 当 await 完成后,说明流已成功结束,可以安全地执行后续操作
891
- process.stdout.write('\n'); // 换行,保持终端整洁
892
- console.log(chalk.green.bold('✔ Upload completed successfully!'));
893
- return response;
894
- }
895
- catch (err) {
896
- callback({
897
- status: 'error',
898
- percent: 0,
899
- msg: err?.message,
900
- });
901
- process.stdout.write('\n');
902
- console.log('\n');
903
- console.error(chalk.red.bold('✖ Upload failed with server error, please scan qrcode to reupload'));
904
- }
905
- }
906
-
907
1016
  class WsServer {
908
1017
  constructor() {
909
1018
  this.ws = new WebSocket.Server({ port: DEV_WS_PORT });
@@ -928,30 +1037,42 @@ class WsServer {
928
1037
  const method = clientMessage.method;
929
1038
  switch (method) {
930
1039
  case 'startUpload':
931
- this.sendUploadStatus('start');
932
- uploadGame(({ status, percent, msg }) => {
933
- if (status === 'process') {
934
- this.sendUploadStatus('process', {
935
- status: 'process',
936
- progress: percent,
937
- });
938
- }
939
- else if (status === 'error') {
940
- this.sendUploadStatus('error', {
941
- status: 'error',
942
- errMsg: msg,
943
- isSuccess: false,
944
- });
945
- }
946
- else if (status === 'success') {
947
- this.sendUploadStatus('success', {
948
- status: 'success',
949
- packages: store.getState().packages,
950
- clientKey: getClientKey().clientKey,
951
- isSuccess: true,
952
- });
953
- }
954
- });
1040
+ if (store.getState().isUnderCompiling) {
1041
+ store.setState({
1042
+ isWaitingForUpload: true,
1043
+ });
1044
+ this.sendResourceCompile();
1045
+ }
1046
+ else {
1047
+ eventEmitter.emit('compileSuccess', {});
1048
+ }
1049
+ // /**
1050
+ // * 如果是编译中,需要等待编译好之后再上传,关键如何实现呢?
1051
+ // * 1. 编译完成后,需要通知客户端上传
1052
+ // * 2. 客户端上传完成后,需要通知服务端上传完成
1053
+ // */
1054
+ // this.sendUploadStatus('start');
1055
+ // uploadGame(({ status, percent, msg }) => {
1056
+ // if (status === 'process') {
1057
+ // this.sendUploadStatus('process', {
1058
+ // status: 'process',
1059
+ // progress: percent,
1060
+ // });
1061
+ // } else if (status === 'error') {
1062
+ // this.sendUploadStatus('error', {
1063
+ // status: 'error',
1064
+ // errMsg: msg,
1065
+ // isSuccess: false,
1066
+ // });
1067
+ // } else if (status === 'success') {
1068
+ // this.sendUploadStatus('success', {
1069
+ // status: 'success',
1070
+ // packages: store.getState().packages,
1071
+ // clientKey: getClientKey().clientKey,
1072
+ // isSuccess: true,
1073
+ // });
1074
+ // }
1075
+ // });
955
1076
  break;
956
1077
  case 'closeLocalDebug':
957
1078
  console.log('closeLocalDebug');
@@ -1048,6 +1169,11 @@ class WsServer {
1048
1169
  method: 'resourceChange',
1049
1170
  });
1050
1171
  }
1172
+ sendResourceCompile() {
1173
+ this.send({
1174
+ method: 'resourceCompile',
1175
+ });
1176
+ }
1051
1177
  close() {
1052
1178
  this.ws.close();
1053
1179
  }
@@ -1061,7 +1187,7 @@ class WsServer {
1061
1187
  }
1062
1188
  const wsServer = new WsServer();
1063
1189
 
1064
- async function prepareResource(context) {
1190
+ async function compile(context) {
1065
1191
  const entryDir = process.cwd();
1066
1192
  const outputDir = getOutputDir();
1067
1193
  const { clientKey, msg } = getClientKey();
@@ -1075,7 +1201,12 @@ async function prepareResource(context) {
1075
1201
  if (!fs.existsSync(outputDir)) {
1076
1202
  fs.mkdirSync(outputDir, { recursive: true });
1077
1203
  }
1078
- const tip = context?.mode === 'watch' ? chalk.yellow('game resource change, restart to upload') : chalk.yellow.bold('Start compile game for debug');
1204
+ store.setState({
1205
+ isUnderCompiling: true,
1206
+ });
1207
+ const tip = context?.mode === 'watch'
1208
+ ? chalk.yellow('⏳ Local game assets updated. Starting build process...\n')
1209
+ : chalk.yellow.bold('⏳ Start compile game assets for local debugging...\n');
1079
1210
  console.log(tip);
1080
1211
  const { isSuccess, errorMsg, packages } = await ttmgPack.debugPkgs({
1081
1212
  entry: entryDir,
@@ -1094,15 +1225,25 @@ async function prepareResource(context) {
1094
1225
  },
1095
1226
  });
1096
1227
  if (!isSuccess) {
1228
+ store.setState({
1229
+ isUnderCompiling: false,
1230
+ });
1097
1231
  console.log(chalk.redBright('Build game package failed, Please check the error message below:'));
1098
1232
  console.log(chalk.redBright(errorMsg));
1099
1233
  process.exit(1);
1100
1234
  }
1101
1235
  else {
1102
1236
  store.setState({
1237
+ isUnderCompiling: false,
1103
1238
  packages,
1104
1239
  });
1105
- console.log(chalk.green.bold('Compile game package success \n'));
1240
+ if (store.getState().isWaitingForUpload) {
1241
+ eventEmitter.emit('compileSuccess', {});
1242
+ }
1243
+ console.log(chalk.green.bold('✅ Compile game assets success! \n'));
1244
+ /**
1245
+ * 开始上传客户端
1246
+ */
1106
1247
  return {
1107
1248
  isSuccess,
1108
1249
  errorMsg,
@@ -1112,94 +1253,240 @@ async function prepareResource(context) {
1112
1253
  }
1113
1254
 
1114
1255
  // import { uploadGame } from './uploadGame';
1115
- async function watchChange() {
1256
+ async function watch() {
1116
1257
  let debounceTimer = null;
1117
- fs.watch(process.cwd(), (eventType, filename) => {
1258
+ // 监听当前工作目录,排除 node_modules .git
1259
+ const watcher = chokidar.watch(process.cwd(), {
1260
+ // ignored: /(^|[\/\\])(\.git|node_modules)/, // 忽略 .git 和 node_modules
1261
+ ignoreInitial: true, // 忽略初始添加事件
1262
+ persistent: true,
1263
+ awaitWriteFinish: {
1264
+ stabilityThreshold: 1000,
1265
+ },
1266
+ });
1267
+ // 任意文件变化都触发
1268
+ watcher.on('all', (event, path) => {
1118
1269
  // 清除之前的定时器
1119
1270
  if (debounceTimer)
1120
1271
  clearTimeout(debounceTimer);
1121
1272
  // 重新设置定时器
1122
1273
  debounceTimer = setTimeout(async () => {
1123
- await prepareResource({
1274
+ await compile({
1124
1275
  mode: 'watch',
1125
1276
  });
1126
1277
  wsServer.sendResourceChange();
1127
- // TODO:只做文件预准备,但不主动上传
1128
- // uploadGame()
1129
- // .then((res) => {
1130
- // if (res.isSuccess) {
1131
- // wsServer.sendUploadStatus('success');
1132
- // } else {
1133
- // wsServer.sendUploadStatus('error', {
1134
- // errMsg: res.errorMsg,
1135
- // });
1136
- // }
1137
- // })
1138
- // .catch(() => {
1139
- // wsServer.sendUploadStatus('error');
1140
- // });
1141
1278
  debounceTimer = null;
1142
- }, 500); // 500ms内只执行一次
1279
+ }, 3000); // 1秒内只执行一次
1280
+ });
1281
+ watcher.on('error', error => {
1282
+ // console.error(chalk.red('[watch] 监听发生错误:'), error);
1143
1283
  });
1144
1284
  }
1145
1285
 
1146
- // 你的静态服务器运行的端口
1147
- // --- 修改结束 ---
1148
- async function showSchema() {
1149
- if (!fs.existsSync(OUTPUT_DIR)) {
1150
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
1286
+ async function uploadGame(callback) {
1287
+ const outputDir = getOutputDir();
1288
+ callback({
1289
+ status: 'start',
1290
+ percent: 0,
1291
+ });
1292
+ console.log(chalk.yellow.bold('Start compress game resource'));
1293
+ const zipPath = path.join(os.homedir(), '__TTMG__', 'upload.zip');
1294
+ await zipDirectory(outputDir, zipPath);
1295
+ console.log(chalk.green.bold('Compress game package resource success \n'));
1296
+ await uploadZip(zipPath, callback);
1297
+ }
1298
+ /**
1299
+ * 复制源目录内容到临时目录,然后根据 glob 模式过滤并压缩文件。
1300
+ * 原始目录保持不变。
1301
+ *
1302
+ * @param sourceDir - 要压缩的源文件夹路径。
1303
+ * @param outPath - 输出的 zip 文件路径。
1304
+ */
1305
+ async function zipDirectory(sourceDir, outPath) {
1306
+ // 1. 创建一个唯一的临时目录
1307
+ // fsp 现在是 fs.promises 的别名
1308
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'zip-temp-'));
1309
+ try {
1310
+ // 2. 将源目录的所有内容复制到临时目录
1311
+ await fs.promises.cp(sourceDir, tempDir, { recursive: true });
1312
+ // 3. 对临时目录进行压缩
1313
+ const output = fs.createWriteStream(outPath);
1314
+ const archive = archiver('zip', { zlib: { level: 9 } });
1315
+ // 使用 Promise 包装流操作
1316
+ const archivePromise = new Promise((resolve, reject) => {
1317
+ output.on('close', () => {
1318
+ resolve();
1319
+ });
1320
+ archive.on('warning', err => console.warn('Archiver warning:', err));
1321
+ output.on('error', err => reject(err));
1322
+ archive.on('error', err => reject(err));
1323
+ });
1324
+ archive.pipe(output);
1325
+ // 4. 使用 glob 在临时目录中查找文件,并应用过滤规则
1326
+ const files = await glob__namespace.glob('**/*', {
1327
+ cwd: tempDir,
1328
+ nodir: true,
1329
+ ignore: '**/*.js.map', // 过滤规则
1330
+ });
1331
+ // 5. 将过滤后的文件逐个添加到压缩包
1332
+ for (const file of files) {
1333
+ const filePath = path.join(tempDir, file);
1334
+ archive.file(filePath, { name: file });
1335
+ }
1336
+ // 6. 完成压缩
1337
+ await archive.finalize();
1338
+ // 等待文件流关闭
1339
+ await archivePromise;
1151
1340
  }
1152
- const { clientKey } = getClientKey();
1153
- // 2. 定义二维码图片的文件名和完整路径
1341
+ catch (err) {
1342
+ console.error('压缩过程中发生错误:', err);
1343
+ throw err;
1344
+ }
1345
+ finally {
1346
+ // 7. 无论成功还是失败,都清理临时目录
1347
+ await fs.promises.rm(tempDir, { recursive: true, force: true });
1348
+ }
1349
+ }
1350
+ async function uploadZip(zipPath, callback) {
1351
+ const form = new FormData();
1352
+ form.append('file', fs.createReadStream(zipPath), {
1353
+ filename: 'upload.zip',
1354
+ contentType: 'application/zip',
1355
+ });
1356
+ // 帮我计算下文件大小,变成 MB 为单位
1357
+ const fileSize = fs.statSync(zipPath).size / 1024 / 1024;
1358
+ console.log(chalk.yellow.bold(`Start upload resource to client, size: ${fileSize.toFixed(2)} MB`));
1359
+ const { clientHttpPort, clientHost } = store.getState();
1360
+ const url = `http://${clientHost}:${clientHttpPort}/game/upload`;
1154
1361
  try {
1155
- const qrCodeUrl = `http://localhost:${DEV_PORT}`;
1156
- // 5. 打印更新后的提示信息
1157
- console.log(chalk.green.bold('Tips:'));
1158
- console.log(` 1. ${chalk.yellow.bold('Open the link below in your browser to see the QR code, then scan it.')}`);
1159
- console.log(` ${chalk.cyan.underline(qrCodeUrl)}`);
1160
- console.log(` 2. ${chalk.yellow.bold('Will auto upload compiled resource to client.')}`);
1161
- console.log(` 3. ${chalk.yellow.bold('Debug your game in the browser.')}\n`);
1162
- /**
1163
- * 自动打开浏览器
1164
- */
1165
- openUrl(qrCodeUrl);
1362
+ // 1. 创建请求流
1363
+ const stream = got.stream.post(url, {
1364
+ body: form,
1365
+ });
1366
+ // 2. 监听上传进度 (这个回调是并行的,不影响封装)
1367
+ stream.on('uploadProgress', progress => {
1368
+ const percent = progress.percent;
1369
+ // const transferred = progress.transferred;
1370
+ // const total = progress.total;
1371
+ process.stdout.write(`\r${chalk.cyan('Uploading progress: ')}${chalk.green((percent * 100).toFixed(0) + '%')}`);
1372
+ callback({
1373
+ status: 'process',
1374
+ percent,
1375
+ });
1376
+ });
1377
+ // 3. 【核心封装】将流的处理过程包装在 Promise 中
1378
+ const response = await new Promise((resolve, reject) => {
1379
+ // 用于拼接响应数据
1380
+ const chunks = [];
1381
+ // 当流传输数据时,收集数据块
1382
+ stream.on('data', chunk => {
1383
+ chunks.push(chunk);
1384
+ });
1385
+ // 当流成功结束时
1386
+ stream.on('end', () => {
1387
+ // 拼接所有数据块,并转换为字符串
1388
+ // const responseBody = Buffer.concat(chunks).toString('utf-8');
1389
+ // stream.response 在流结束后才可用
1390
+ // 将完整的响应对象 resolve 出去
1391
+ callback({
1392
+ status: 'success',
1393
+ percent: 1,
1394
+ });
1395
+ resolve({
1396
+ statusCode: 200,
1397
+ });
1398
+ });
1399
+ // 当流发生错误时
1400
+ stream.on('error', err => {
1401
+ // 将错误 reject 出去,这样外层的 try...catch 就能捕获到
1402
+ reject(err);
1403
+ callback({
1404
+ status: 'error',
1405
+ percent: 0,
1406
+ msg: err.message,
1407
+ });
1408
+ });
1409
+ });
1410
+ // 4. 当 await 完成后,说明流已成功结束,可以安全地执行后续操作
1411
+ process.stdout.write('\n'); // 换行,保持终端整洁
1412
+ console.log(chalk.green.bold('✔ Upload completed successfully!'));
1413
+ return response;
1166
1414
  }
1167
1415
  catch (err) {
1168
- console.error(chalk.red('Failed to generate QR code image.'), err);
1416
+ callback({
1417
+ status: 'error',
1418
+ percent: 0,
1419
+ msg: err?.message,
1420
+ });
1421
+ process.stdout.write('\n');
1422
+ console.log('\n');
1423
+ console.error(chalk.red.bold('✖ Upload failed with server error, please scan qrcode to reupload'));
1169
1424
  }
1170
1425
  }
1171
1426
 
1427
+ function listen() {
1428
+ eventEmitter.on('compileSuccess', () => {
1429
+ wsServer.sendUploadStatus('start');
1430
+ store.setState({
1431
+ isWaitingForUpload: false,
1432
+ });
1433
+ uploadGame(({ status, percent, msg }) => {
1434
+ if (status === 'process') {
1435
+ wsServer.sendUploadStatus('process', {
1436
+ status: 'process',
1437
+ progress: percent,
1438
+ });
1439
+ }
1440
+ else if (status === 'error') {
1441
+ wsServer.sendUploadStatus('error', {
1442
+ status: 'error',
1443
+ errMsg: msg,
1444
+ isSuccess: false,
1445
+ });
1446
+ }
1447
+ else if (status === 'success') {
1448
+ wsServer.sendUploadStatus('success', {
1449
+ status: 'success',
1450
+ packages: store.getState().packages,
1451
+ clientKey: getClientKey().clientKey,
1452
+ isSuccess: true,
1453
+ });
1454
+ }
1455
+ });
1456
+ });
1457
+ }
1458
+
1172
1459
  async function dev() {
1173
- /**
1174
- * 1. 准备游戏资源
1175
- */
1176
- prepareResource();
1177
- /**
1178
- * 2. 创建本地调试服务
1179
- */
1180
- await createServer();
1181
- /**
1182
- * 3. 显示调试二维码信息,扫码启动客户端调试服务
1183
- */
1184
- await showSchema();
1185
- /**
1186
- * 4. 监听游戏资源变化
1187
- */
1188
- await watchChange();
1460
+ await start();
1461
+ compile();
1462
+ watch();
1463
+ listen();
1189
1464
  }
1190
1465
 
1191
- var version = "0.1.5";
1466
+ /**
1467
+ * 获取配置文件中的 cookie
1468
+ */
1469
+ async function upload() {
1470
+ const res = await request({
1471
+ url: 'https://developers.tiktok.com/tiktok/v3/devportal/minigame/info',
1472
+ method: 'GET',
1473
+ params: {
1474
+ client_key: 'mg7oas80sthix6xy',
1475
+ version_type: 1,
1476
+ },
1477
+ headers: {
1478
+ 'x-tt-env': 'ppe_dev_tool',
1479
+ 'x-use-ppe': '1',
1480
+ },
1481
+ });
1482
+ console.log('app info', res?.data);
1483
+ }
1484
+
1485
+ var version = "0.1.6";
1192
1486
  var pkg = {
1193
1487
  version: version};
1194
1488
 
1195
1489
  const program = new commander.Command();
1196
- (async () => {
1197
- try {
1198
- await checkUpdate();
1199
- // eslint-disable-next-line no-empty
1200
- }
1201
- catch (err) { }
1202
- })();
1203
1490
  program
1204
1491
  .name('ttmg')
1205
1492
  .description('TikTok Mini Games Command Line Tool')
@@ -1239,6 +1526,11 @@ program
1239
1526
  }
1240
1527
  else {
1241
1528
  dev();
1529
+ try {
1530
+ checkUpdate();
1531
+ // eslint-disable-next-line no-empty
1532
+ }
1533
+ catch (err) { }
1242
1534
  }
1243
1535
  });
1244
1536
  /**
@@ -1258,10 +1550,10 @@ program
1258
1550
  }
1259
1551
  });
1260
1552
  program
1261
- .command('login')
1262
- .description('User Dev Portal Account to Login')
1553
+ .command('upload')
1554
+ .description('Upload Native Mini Game')
1263
1555
  .action(async () => {
1264
- console.log('will support soon');
1556
+ await upload();
1265
1557
  });
1266
1558
  program.parse(process.argv);
1267
1559
  //# sourceMappingURL=index.js.map