@ttmg/cli 0.1.9-beta.6 → 0.1.9-beta.8

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.
package/README.md CHANGED
@@ -1,72 +1,40 @@
1
- ## @ttmg/cli
1
+ ### @ttmg/cli
2
+ `ttmg` is a command-line tool designed for managing and developing mini-game projects. It supports initialization, development, debugging, and packaging for both H5 and native mini-games.
3
+ `ttmg` 是一款专为小游戏项目管理与开发设计的命令行工具,支持 H5 小游戏和原生小游戏的初始化、开发调试及打包构建。
4
+ #### Installation 安装
2
5
 
3
- `ttmg` 是一个用于管理和开发小游戏项目的命令行工具,支持 H5 小游戏和原生小游戏的初始化、开发调试和打包构建。
4
-
5
- ### 安装
6
-
7
- 你可以通过 npm 本地安装或全局安装:
6
+ You can install `ttmg` globally or as a project dependency via npm:
7
+ 你可以通过 npm 全局或本地安装 `ttmg`:
8
8
 
9
9
  ```
10
10
  npm install @ttmg/cli -g
11
11
  ```
12
12
 
13
- 或者在项目中作为开发依赖:
13
+ Or add it as a development dependency in your project:
14
+ 或者作为项目开发依赖安装:
14
15
 
15
16
  ```
16
17
  npm install @ttmg/cli --save-dev
17
18
  ```
18
19
 
19
- ### 使用方法
20
-
21
- 安装后,可以在命令行中使用 `ttmg` 命令。
22
-
23
- #### 查看帮助
24
-
25
- ```
26
- ttmg --help
27
- ```
28
-
29
- #### 命令说明
30
-
31
- ##### 1. 初始化项目
32
-
33
- - 对基于游戏引擎构建好的 H5 小游戏进行初始化
34
-
35
- ```
36
- ttmg init --h5
37
- ```
38
-
39
- - 对基于游戏引擎构建好的 Native 小游戏进行初始化
40
-
41
- ```
42
- ttmg init --native
43
- ```
44
-
45
- ##### 2. 开发调试
20
+ #### Login 登录
46
21
 
47
- 启动本地开发环境,打开浏览器进行调试。
48
-
49
- - H5 小游戏调试
50
-
51
- ```
52
- ttmg dev --h5
53
- ```
54
-
55
- - 原生小游戏调试
22
+ Before running any `ttmg` commands, please log in with your TikTok account. Once logged in, the tool will automatically use your account for project initialization, development, debugging, and packaging.
23
+ 在首次使用 `ttmg` 命令前,请先登录你的 TikTok 账号。登录后,工具会自动使用该账号进行项目初始化、开发调试和打包构建。
56
24
 
57
25
  ```
58
- ttmg dev --native
26
+ ttmg login
59
27
  ```
60
28
 
61
- ##### 3. 项目打包
62
-
63
- 打包项目用于发布。
29
+ #### Debugging 调试
64
30
 
65
- - 打包 H5 小游戏
31
+ - By default, debug native mini-games:
32
+ - 默认调试原生小游戏:
66
33
 
67
34
  ```
68
- ttmg build --h5
35
+ ttmg dev
69
36
  ```
70
37
 
71
- - 打包原生小游戏
72
- ttmg build --native
38
+ - 调试 H5 小游戏:
39
+ - To debug H5 mini-games:
40
+ ttmg dev --h5
package/dist/index.js CHANGED
@@ -17,13 +17,15 @@ var axios = require('axios');
17
17
  var handlebars = require('handlebars');
18
18
  var esbuild = require('esbuild');
19
19
  var archiver = require('archiver');
20
+ var http = require('http');
20
21
  var ttmgPack = require('ttmg-pack');
21
22
  var expressStaticGzip = require('express-static-gzip');
23
+ var fileUpload = require('express-fileupload');
22
24
  var chokidar = require('chokidar');
23
25
  var WebSocket = require('ws');
24
26
  var glob = require('glob');
25
27
  var got = require('got');
26
- var FormData = require('form-data');
28
+ var FormData$1 = require('form-data');
27
29
 
28
30
  function _interopNamespaceDefault(e) {
29
31
  var n = Object.create(null);
@@ -245,7 +247,7 @@ async function checkUpdate() {
245
247
 
246
248
  let config$1 = null;
247
249
  const CONFIG_PATH = path.join(os.homedir(), '.ttmgrc');
248
- const getTTmgrcConfig = () => {
250
+ const getTTMGRC = () => {
249
251
  if (config$1) {
250
252
  return config$1;
251
253
  }
@@ -263,7 +265,7 @@ const getTTmgrcConfig = () => {
263
265
  }
264
266
  }
265
267
  };
266
- const setTTmgrcConfig = (config) => {
268
+ const setTTMGRC = (config) => {
267
269
  // updata cache config
268
270
  config = config;
269
271
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config));
@@ -335,16 +337,13 @@ async function login() {
335
337
  user_id: response?.data?.data?.user_id,
336
338
  cookie: response?.headers['set-cookie'].join('; '),
337
339
  };
338
- setTTmgrcConfig(data);
340
+ setTTMGRC(data);
339
341
  spinner$2.succeed(chalk.bold.green('login successfully!'));
340
342
  process.exit(0);
341
343
  }
342
344
  }
343
345
 
344
- /**
345
- * 获取配置文件中的 cookie
346
- */
347
- const config = getTTmgrcConfig();
346
+ const config = getTTMGRC();
348
347
  const cookie = config.cookie;
349
348
  axios.defaults.headers.common['Cookie'] = cookie;
350
349
  // ppe_dev_tool
@@ -399,9 +398,28 @@ async function fetchGameInfo(gameId) {
399
398
  }
400
399
  }
401
400
 
401
+ async function uploadGameToPlatform({ data, name, gameId, desc = '--', }) {
402
+ const formData = new FormData();
403
+ formData.append('file', new Blob([data], { type: 'application/zip' }), name);
404
+ formData.append('client_key', gameId);
405
+ formData.append('desc', desc);
406
+ const response = await request({
407
+ method: 'POST',
408
+ url: 'https://developers.tiktok.com/tiktok/v3/devportal/minigame/devtool/upload',
409
+ data: formData,
410
+ headers: {
411
+ 'x-use-ppe': '1',
412
+ 'x-tt-env': 'ppe_dev_tool',
413
+ // @ts-ignore
414
+ 'Content-Type': `multipart/form-data; boundary=${formData._boundary}`,
415
+ },
416
+ });
417
+ return response?.data;
418
+ }
419
+
402
420
  function getCurrentUser() {
403
421
  try {
404
- const config = getTTmgrcConfig();
422
+ const config = getTTMGRC();
405
423
  return {
406
424
  email: config?.email || '',
407
425
  user_id: config?.user_id || '',
@@ -729,10 +747,67 @@ function getOutputDir() {
729
747
  return path.join(os.homedir(), '__TTMG__', clientKey);
730
748
  }
731
749
 
732
- const DEV_PORT = 9528;
733
- const DEV_WS_PORT = 9529;
750
+ const DEV_PORT = 3000;
751
+ const DEV_WS_PORT = 4000;
734
752
  path.join(os.homedir(), '__TTMG__');
735
753
 
754
+ // store.ts
755
+ class Store {
756
+ constructor(initialState) {
757
+ this.listeners = [];
758
+ this.state = initialState;
759
+ }
760
+ // 获取单例实例
761
+ static getInstance(initialState) {
762
+ if (!Store.instance) {
763
+ Store.instance = new Store(initialState);
764
+ }
765
+ return Store.instance;
766
+ }
767
+ // 获取状态
768
+ getState() {
769
+ return this.state;
770
+ }
771
+ // 设置状态
772
+ setState(newState) {
773
+ this.state = { ...this.state, ...newState };
774
+ this.listeners.forEach(listener => listener(this.state));
775
+ }
776
+ // 订阅状态变化
777
+ subscribe(listener) {
778
+ this.listeners.push(listener);
779
+ return () => {
780
+ this.listeners = this.listeners.filter(l => l !== listener);
781
+ };
782
+ }
783
+ // 重置状态
784
+ reset(newState) {
785
+ this.state = newState;
786
+ this.listeners.forEach(listener => listener(this.state));
787
+ }
788
+ }
789
+ const store = Store.getInstance({
790
+ clientHost: '',
791
+ clientHttpPort: '',
792
+ clientWsPort: '',
793
+ clientWsHost: '',
794
+ clientKey: '',
795
+ nodeServerPort: DEV_PORT,
796
+ nodeWsPort: DEV_WS_PORT,
797
+ packages: {},
798
+ isUnderCompiling: false,
799
+ isWaitingForUpload: false,
800
+ projectInfo: {
801
+ projectSize: 0,
802
+ mainPkg: {
803
+ pkgSize: 0,
804
+ isMatchSizeLimit: false,
805
+ },
806
+ independentPkgs: {},
807
+ },
808
+ checkResults: [],
809
+ });
810
+
736
811
  function showTips(context) {
737
812
  console.log(chalk.gray('─────────────────────────────────────────────'));
738
813
  console.log(chalk.yellow.bold('1.') +
@@ -753,40 +828,29 @@ function showTips(context) {
753
828
  }
754
829
 
755
830
  const successCode = 0;
831
+ const errorCode = -1;
756
832
  const outputDir = getOutputDir();
757
833
  const publicPath = path.join(__dirname, 'public');
758
- let server;
759
834
  async function start() {
760
835
  const startTime = Date.now();
761
- if (server) {
762
- closeServer();
763
- }
764
836
  const app = express();
837
+ app.use(fileUpload()); // 启用 express-fileupload 中间件
838
+ // --- 中间件和路由设置 ---
765
839
  app.use(expressStaticGzip(publicPath, {
766
840
  enableBrotli: true,
767
841
  orderPreference: ['br'],
768
842
  }));
769
- /**
770
- * 支持跨域请求
771
- */
772
843
  app.use((req, res, next) => {
773
844
  res.header('Access-Control-Allow-Origin', '*');
774
845
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
775
846
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
776
847
  next();
777
848
  });
778
- const port = DEV_PORT;
779
849
  app.use(express.json());
780
- // 解析 FormData body
781
850
  app.use(express.urlencoded({ extended: true }));
782
- /**
783
- * 支持静态资源
784
- */
785
851
  app.use('/game/files', express.static(outputDir));
786
852
  app.get('/game/config', async (req, res) => {
787
- const basic = await ttmgPack.getPkgs({
788
- entryDir: process.cwd(),
789
- });
853
+ const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
790
854
  const { email, user_id } = getCurrentUser();
791
855
  const { clientKey } = getClientKey();
792
856
  res.send({
@@ -795,34 +859,23 @@ async function start() {
795
859
  email,
796
860
  user_id,
797
861
  code: successCode,
798
- nodeWsPort: DEV_WS_PORT,
862
+ nodeWsPort: store.getState().nodeWsPort,
799
863
  clientKey: clientKey,
800
- schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${DEV_WS_PORT}`,
864
+ schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${store.getState().nodeWsPort}`,
801
865
  ...basic,
802
866
  devToolVersion: require(path.join(__dirname, 'package.json')).version,
803
867
  },
804
868
  });
805
869
  });
806
870
  app.get('/game/detail', async (req, res) => {
807
- const basic = await ttmgPack.getPkgs({
808
- entryDir: process.cwd(),
809
- });
871
+ const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
810
872
  const { clientKey } = getClientKey();
811
873
  const { error, data: gameInfo } = await fetchGameInfo(clientKey);
812
874
  if (error) {
813
- res.send({
814
- error,
815
- data: null,
816
- });
875
+ res.send({ error, data: null });
817
876
  return;
818
877
  }
819
- res.send({
820
- error: null,
821
- data: {
822
- ...basic,
823
- ...gameInfo,
824
- },
825
- });
878
+ res.send({ error: null, data: { ...basic, ...gameInfo } });
826
879
  });
827
880
  app.get('/game/check', async (req, res) => {
828
881
  const checkResult = await ttmgPack.checkPkgs({
@@ -832,7 +885,7 @@ async function start() {
832
885
  output: outputDir,
833
886
  dev: {
834
887
  enable: true,
835
- port: DEV_PORT,
888
+ port: store.getState().nodeServerPort,
836
889
  host: 'localhost',
837
890
  enableSourcemap: false,
838
891
  enableLog: false,
@@ -845,36 +898,108 @@ async function start() {
845
898
  },
846
899
  },
847
900
  });
848
- res.send({
849
- code: successCode,
850
- data: checkResult,
851
- });
901
+ res.send({ code: successCode, data: checkResult });
902
+ });
903
+ /**
904
+ * @description 上传游戏代码到服务器
905
+ */
906
+ app.post('/game/upload', async (req, res) => {
907
+ const fileKeys = Object.keys(req.files);
908
+ const uploadedFile = req.files[fileKeys[0]];
909
+ if (!uploadedFile) {
910
+ res.status(400).send({ code: errorCode, data: 'No file uploaded' }); // 使用正确的 HTTP 状态码
911
+ return;
912
+ }
913
+ try {
914
+ // 通过 header 获取 desc
915
+ const desc = req.headers['ttmg-game-desc'];
916
+ // 直接传递需要的信息
917
+ const uploadResult = await uploadGameToPlatform({
918
+ data: uploadedFile.data, // 这是 Buffer
919
+ name: uploadedFile.name, // 这是文件名
920
+ gameId: getClientKey().clientKey,
921
+ desc,
922
+ });
923
+ res.send({ code: successCode, data: uploadResult });
924
+ }
925
+ catch (error) {
926
+ // 错误处理可以更具体
927
+ let errorMessage = 'An unknown error occurred.';
928
+ if (error instanceof Error) {
929
+ errorMessage = error.message;
930
+ }
931
+ // 打印详细错误到服务器日志,方便排查
932
+ res.status(500).send({ code: errorCode, data: errorMessage }); // 使用正确的 HTTP 状态码
933
+ }
852
934
  });
853
935
  app.get('*', (req, res) => {
854
936
  res.sendFile(path.join(publicPath, 'index.html'));
855
937
  });
856
- // 启动服务
857
- server = await new Promise((resolve, reject) => {
858
- const server = app.listen(port, () => {
859
- /**
860
- * 获取当前版本号
861
- */
862
- const version = require(path.join(__dirname, 'package.json')).version;
863
- console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(`ready in`), chalk.bold(`${Date.now() - startTime}ms`));
864
- resolve(server);
938
+ // --- 中间件和路由设置结束 ---
939
+ // 步骤 2: 用配置好的 app 实例创建一个 http.Server。我们只创建这一次!
940
+ const server = http.createServer(app);
941
+ /**
942
+ * @description 尝试在指定端口启动服务。这是个纯粹的辅助函数。
943
+ * @param {number} port - 要尝试的端口号。
944
+ * @returns {Promise<boolean>} 成功返回 true,因端口占用失败则返回 false。
945
+ */
946
+ function tryListen(port) {
947
+ return new Promise(resolve => {
948
+ // 定义错误处理函数
949
+ const onError = err => {
950
+ // 清理掉另一个事件的监听器,防止内存泄漏
951
+ server.removeListener('listening', onListening);
952
+ if (err.code === 'EADDRINUSE') {
953
+ console.log(chalk(`Port ${port} is already in use, trying ${port + 1}...`));
954
+ resolve(false); // 明确表示因端口占用而失败
955
+ }
956
+ else {
957
+ // 对于其他致命错误,直接退出进程
958
+ console.log(chalk.red.bold(err.message));
959
+ process.exit(1);
960
+ }
961
+ };
962
+ // 定义成功处理函数
963
+ const onListening = () => {
964
+ // 清理掉另一个事件的监听器
965
+ server.removeListener('error', onError);
966
+ resolve(true); // 明确表示成功
967
+ };
968
+ // 使用 .once() 来确保监听器只执行一次然后自动移除
969
+ server.once('error', onError);
970
+ server.once('listening', onListening);
971
+ // 执行监听动作
972
+ server.listen(port);
865
973
  });
866
- });
867
- const baseUrl = `http://localhost:${port}`;
974
+ }
975
+ // 步骤 3: 使用循环来线性、串行地尝试启动服务
976
+ let isListening = false;
977
+ const maxRetries = 20; // 设置一个最大重试次数,以防万一
978
+ for (let i = 0; i < maxRetries; i++) {
979
+ const currentPort = store.getState().nodeServerPort;
980
+ isListening = await tryListen(currentPort);
981
+ if (isListening) {
982
+ break; // 成功,跳出循环
983
+ }
984
+ else {
985
+ // 失败(端口占用),更新端口号,准备下一次循环
986
+ store.setState({ nodeServerPort: currentPort + 1 });
987
+ }
988
+ }
989
+ // 步骤 4: 检查最终结果,如果所有尝试都失败了,则退出
990
+ if (!isListening) {
991
+ console.log(chalk.red.bold(`Failed to start server after trying ${maxRetries} ports.`));
992
+ process.exit(1);
993
+ }
994
+ // --- 服务启动成功后的逻辑 ---
995
+ // @ts-ignore
996
+ const finalPort = server.address().port; // 从成功的 server 实例安全地获取最终端口
997
+ const version = require(path.join(__dirname, 'package.json')).version;
998
+ console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(`ready in`), chalk.bold(`${Date.now() - startTime}ms`), chalk.green(`on port`), chalk.green.bold(finalPort));
999
+ const baseUrl = `http://localhost:${finalPort}`;
868
1000
  showTips({ server: baseUrl });
869
1001
  openUrl(baseUrl);
870
1002
  }
871
- function closeServer() {
872
- server.close(() => {
873
- console.log('Dev server closed');
874
- server = null;
875
- process.exit(0);
876
- });
877
- }
878
1003
 
879
1004
  /**
880
1005
  * 一个类型安全的、通用的事件发射器类。
@@ -951,63 +1076,12 @@ class TypedEventEmitter {
951
1076
  }
952
1077
  const eventEmitter = new TypedEventEmitter();
953
1078
 
954
- class Store {
955
- constructor(initialState) {
956
- this.listeners = [];
957
- this.state = initialState;
958
- }
959
- // 获取单例实例
960
- static getInstance(initialState) {
961
- if (!Store.instance) {
962
- Store.instance = new Store(initialState);
963
- }
964
- return Store.instance;
965
- }
966
- // 获取状态
967
- getState() {
968
- return this.state;
969
- }
970
- // 设置状态
971
- setState(newState) {
972
- this.state = { ...this.state, ...newState };
973
- this.listeners.forEach(listener => listener(this.state));
974
- }
975
- // 订阅状态变化
976
- subscribe(listener) {
977
- this.listeners.push(listener);
978
- return () => {
979
- this.listeners = this.listeners.filter(l => l !== listener);
980
- };
981
- }
982
- // 重置状态
983
- reset(newState) {
984
- this.state = newState;
985
- this.listeners.forEach(listener => listener(this.state));
986
- }
987
- }
988
- const store = Store.getInstance({
989
- clientHost: '',
990
- clientHttpPort: '',
991
- clientWsPort: '',
992
- clientWsHost: '',
993
- clientKey: '',
994
- packages: {},
995
- isUnderCompiling: false,
996
- isWaitingForUpload: false,
997
- projectInfo: {
998
- projectSize: 0,
999
- mainPkg: {
1000
- pkgSize: 0,
1001
- isMatchSizeLimit: false,
1002
- },
1003
- independentPkgs: {},
1004
- },
1005
- checkResults: [],
1006
- });
1007
-
1008
1079
  class WsServer {
1009
1080
  constructor() {
1010
- this.ws = new WebSocket.Server({ port: DEV_WS_PORT });
1081
+ this.startWsServer(store.getState().nodeWsPort);
1082
+ }
1083
+ startWsServer(port) {
1084
+ this.ws = new WebSocket.Server({ port });
1011
1085
  this.ws.on('connection', ws => {
1012
1086
  const { clientHttpPort, clientHost, clientWsPort } = store.getState();
1013
1087
  if (clientHost) {
@@ -1041,8 +1115,6 @@ class WsServer {
1041
1115
  * 关闭调试服务
1042
1116
  */
1043
1117
  this.ws.close();
1044
- closeServer();
1045
- console.log('close server');
1046
1118
  break;
1047
1119
  }
1048
1120
  }
@@ -1112,6 +1184,18 @@ class WsServer {
1112
1184
  }
1113
1185
  });
1114
1186
  });
1187
+ this.ws.on('error', err => {
1188
+ if (err.code === 'EADDRINUSE') {
1189
+ store.setState({
1190
+ nodeWsPort: store.getState().nodeWsPort + 1,
1191
+ });
1192
+ this.startWsServer(store.getState().nodeWsPort);
1193
+ }
1194
+ else {
1195
+ console.log(chalk.red.bold(err.message));
1196
+ process.exit(1);
1197
+ }
1198
+ });
1115
1199
  }
1116
1200
  send(params) {
1117
1201
  this.ws.clients.forEach(client => {
@@ -1225,7 +1309,7 @@ function compile(context) {
1225
1309
  context: {
1226
1310
  clientKey,
1227
1311
  outputDir,
1228
- devPort: DEV_PORT,
1312
+ devPort: store.getState().nodeServerPort,
1229
1313
  entryDir,
1230
1314
  },
1231
1315
  });
@@ -1331,7 +1415,7 @@ async function zipDirectory(sourceDir, outPath) {
1331
1415
  }
1332
1416
  async function uploadZip(zipPath, callback) {
1333
1417
  const startTime = Date.now();
1334
- const form = new FormData();
1418
+ const form = new FormData$1();
1335
1419
  form.append('file', fs.createReadStream(zipPath), {
1336
1420
  filename: 'upload.zip',
1337
1421
  contentType: 'application/zip',
@@ -1467,7 +1551,7 @@ async function upload() {
1467
1551
  });
1468
1552
  }
1469
1553
 
1470
- var version = "0.1.9-beta.6";
1554
+ var version = "0.1.9-beta.8";
1471
1555
  var pkg = {
1472
1556
  version: version};
1473
1557
 
@@ -1512,11 +1596,6 @@ program
1512
1596
  }
1513
1597
  else {
1514
1598
  dev();
1515
- try {
1516
- checkUpdate();
1517
- // eslint-disable-next-line no-empty
1518
- }
1519
- catch (err) { }
1520
1599
  }
1521
1600
  });
1522
1601
  /**