@ttmg/cli 0.1.9-beta.7 → 0.1.9-beta.9
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 +19 -51
- package/dist/index.js +233 -142
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -6
- package/dist/public/assets/index-B-OUKblc.js +67 -0
- package/dist/public/assets/index-B-OUKblc.js.br +0 -0
- package/dist/public/assets/index-BNwkkghM.css +1 -0
- package/dist/public/assets/index-BNwkkghM.css.br +0 -0
- package/dist/public/assets/index-Bo2GE0iI.js +67 -0
- package/dist/public/assets/index-Bo2GE0iI.js.br +0 -0
- package/dist/public/assets/index-BuMld4Sl.css +1 -0
- package/dist/public/assets/index-BuMld4Sl.css.br +0 -0
- package/dist/public/assets/index-Cq-NkIfK.js +67 -0
- package/dist/public/assets/index-Cq-NkIfK.js.br +0 -0
- package/dist/public/assets/index-CrM2XxXK.js +67 -0
- package/dist/public/assets/index-CrM2XxXK.js.br +0 -0
- package/dist/public/assets/index-D1dpaK4P.js +67 -0
- package/dist/public/assets/index-D1dpaK4P.js.br +0 -0
- package/dist/public/assets/index-D80wbca2.js +67 -0
- package/dist/public/assets/index-D80wbca2.js.br +0 -0
- package/dist/public/assets/index-DIkT1K6H.js +67 -0
- package/dist/public/assets/index-DIkT1K6H.js.br +0 -0
- package/dist/public/assets/index-D_6UwS0L.css +1 -0
- package/dist/public/assets/index-D_6UwS0L.css.br +0 -0
- package/dist/public/assets/index-Dq6XWWnQ.js +67 -0
- package/dist/public/assets/index-Dq6XWWnQ.js.br +0 -0
- package/dist/public/assets/index-OaV93pLr.js +67 -0
- package/dist/public/assets/index-OaV93pLr.js.br +0 -0
- package/dist/public/assets/index-rO4GFCVV.js +67 -0
- package/dist/public/assets/index-rO4GFCVV.js.br +0 -0
- package/dist/public/assets/index-vHZ0A7MO.css +1 -0
- package/dist/public/assets/index-vHZ0A7MO.css.br +0 -0
- package/dist/public/index.html +2 -2
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -1,72 +1,40 @@
|
|
|
1
|
-
|
|
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`
|
|
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
|
|
26
|
+
ttmg login
|
|
59
27
|
```
|
|
60
28
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
打包项目用于发布。
|
|
29
|
+
#### Debugging 调试
|
|
64
30
|
|
|
65
|
-
-
|
|
31
|
+
- By default, debug native mini-games:
|
|
32
|
+
- 默认调试原生小游戏:
|
|
66
33
|
|
|
67
34
|
```
|
|
68
|
-
ttmg
|
|
35
|
+
ttmg dev
|
|
69
36
|
```
|
|
70
37
|
|
|
71
|
-
-
|
|
72
|
-
|
|
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);
|
|
@@ -243,27 +245,26 @@ async function checkUpdate() {
|
|
|
243
245
|
});
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
let config
|
|
248
|
+
let config = null;
|
|
247
249
|
const CONFIG_PATH = path.join(os.homedir(), '.ttmgrc');
|
|
248
|
-
const
|
|
249
|
-
if (config
|
|
250
|
-
return config
|
|
250
|
+
const getTTMGRC = () => {
|
|
251
|
+
if (config) {
|
|
252
|
+
return config;
|
|
251
253
|
}
|
|
252
254
|
else {
|
|
253
255
|
// only check one time
|
|
254
256
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
255
|
-
|
|
256
|
-
return {};
|
|
257
|
+
return null;
|
|
257
258
|
}
|
|
258
259
|
else {
|
|
259
260
|
// safe parse
|
|
260
261
|
let res = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
261
|
-
config
|
|
262
|
+
config = res;
|
|
262
263
|
return res;
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
266
|
};
|
|
266
|
-
const
|
|
267
|
+
const setTTMGRC = (config) => {
|
|
267
268
|
// updata cache config
|
|
268
269
|
config = config;
|
|
269
270
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config));
|
|
@@ -335,27 +336,26 @@ async function login() {
|
|
|
335
336
|
user_id: response?.data?.data?.user_id,
|
|
336
337
|
cookie: response?.headers['set-cookie'].join('; '),
|
|
337
338
|
};
|
|
338
|
-
|
|
339
|
+
setTTMGRC(data);
|
|
339
340
|
spinner$2.succeed(chalk.bold.green('login successfully!'));
|
|
340
341
|
process.exit(0);
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
/**
|
|
345
|
-
* 获取配置文件中的 cookie
|
|
346
|
-
*/
|
|
347
|
-
const config = getTTmgrcConfig();
|
|
348
|
-
const cookie = config.cookie;
|
|
349
|
-
axios.defaults.headers.common['Cookie'] = cookie;
|
|
350
345
|
// ppe_dev_tool
|
|
351
346
|
async function request({ url, method, data, headers, params, }) {
|
|
347
|
+
const config = getTTMGRC();
|
|
348
|
+
const cookie = config?.cookie;
|
|
352
349
|
try {
|
|
353
350
|
const res = await axios({
|
|
354
351
|
url,
|
|
355
352
|
method,
|
|
356
353
|
data,
|
|
357
354
|
params,
|
|
358
|
-
headers
|
|
355
|
+
headers: {
|
|
356
|
+
...(headers || {}),
|
|
357
|
+
Cookie: cookie,
|
|
358
|
+
},
|
|
359
359
|
});
|
|
360
360
|
return {
|
|
361
361
|
data: res.data,
|
|
@@ -399,16 +399,35 @@ async function fetchGameInfo(gameId) {
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
async function uploadGameToPlatform({ data, name, gameId, desc = '--', }) {
|
|
403
|
+
const formData = new FormData();
|
|
404
|
+
formData.append('file', new Blob([data], { type: 'application/zip' }), name);
|
|
405
|
+
formData.append('client_key', gameId);
|
|
406
|
+
formData.append('desc', desc);
|
|
407
|
+
const response = await request({
|
|
408
|
+
method: 'POST',
|
|
409
|
+
url: 'https://developers.tiktok.com/tiktok/v3/devportal/minigame/devtool/upload',
|
|
410
|
+
data: formData,
|
|
411
|
+
headers: {
|
|
412
|
+
'x-use-ppe': '1',
|
|
413
|
+
'x-tt-env': 'ppe_dev_tool',
|
|
414
|
+
// @ts-ignore
|
|
415
|
+
'Content-Type': `multipart/form-data; boundary=${formData._boundary}`,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
data: response?.data,
|
|
420
|
+
error: response?.error,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
402
424
|
function getCurrentUser() {
|
|
403
425
|
try {
|
|
404
|
-
const config =
|
|
405
|
-
return
|
|
406
|
-
email: config?.email || '',
|
|
407
|
-
user_id: config?.user_id || '',
|
|
408
|
-
};
|
|
426
|
+
const config = getTTMGRC();
|
|
427
|
+
return config;
|
|
409
428
|
}
|
|
410
429
|
catch (err) {
|
|
411
|
-
return
|
|
430
|
+
return null;
|
|
412
431
|
}
|
|
413
432
|
}
|
|
414
433
|
|
|
@@ -729,10 +748,67 @@ function getOutputDir() {
|
|
|
729
748
|
return path.join(os.homedir(), '__TTMG__', clientKey);
|
|
730
749
|
}
|
|
731
750
|
|
|
732
|
-
const DEV_PORT =
|
|
733
|
-
const DEV_WS_PORT =
|
|
751
|
+
const DEV_PORT = 3000;
|
|
752
|
+
const DEV_WS_PORT = 4000;
|
|
734
753
|
path.join(os.homedir(), '__TTMG__');
|
|
735
754
|
|
|
755
|
+
// store.ts
|
|
756
|
+
class Store {
|
|
757
|
+
constructor(initialState) {
|
|
758
|
+
this.listeners = [];
|
|
759
|
+
this.state = initialState;
|
|
760
|
+
}
|
|
761
|
+
// 获取单例实例
|
|
762
|
+
static getInstance(initialState) {
|
|
763
|
+
if (!Store.instance) {
|
|
764
|
+
Store.instance = new Store(initialState);
|
|
765
|
+
}
|
|
766
|
+
return Store.instance;
|
|
767
|
+
}
|
|
768
|
+
// 获取状态
|
|
769
|
+
getState() {
|
|
770
|
+
return this.state;
|
|
771
|
+
}
|
|
772
|
+
// 设置状态
|
|
773
|
+
setState(newState) {
|
|
774
|
+
this.state = { ...this.state, ...newState };
|
|
775
|
+
this.listeners.forEach(listener => listener(this.state));
|
|
776
|
+
}
|
|
777
|
+
// 订阅状态变化
|
|
778
|
+
subscribe(listener) {
|
|
779
|
+
this.listeners.push(listener);
|
|
780
|
+
return () => {
|
|
781
|
+
this.listeners = this.listeners.filter(l => l !== listener);
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
// 重置状态
|
|
785
|
+
reset(newState) {
|
|
786
|
+
this.state = newState;
|
|
787
|
+
this.listeners.forEach(listener => listener(this.state));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const store = Store.getInstance({
|
|
791
|
+
clientHost: '',
|
|
792
|
+
clientHttpPort: '',
|
|
793
|
+
clientWsPort: '',
|
|
794
|
+
clientWsHost: '',
|
|
795
|
+
clientKey: '',
|
|
796
|
+
nodeServerPort: DEV_PORT,
|
|
797
|
+
nodeWsPort: DEV_WS_PORT,
|
|
798
|
+
packages: {},
|
|
799
|
+
isUnderCompiling: false,
|
|
800
|
+
isWaitingForUpload: false,
|
|
801
|
+
projectInfo: {
|
|
802
|
+
projectSize: 0,
|
|
803
|
+
mainPkg: {
|
|
804
|
+
pkgSize: 0,
|
|
805
|
+
isMatchSizeLimit: false,
|
|
806
|
+
},
|
|
807
|
+
independentPkgs: {},
|
|
808
|
+
},
|
|
809
|
+
checkResults: [],
|
|
810
|
+
});
|
|
811
|
+
|
|
736
812
|
function showTips(context) {
|
|
737
813
|
console.log(chalk.gray('─────────────────────────────────────────────'));
|
|
738
814
|
console.log(chalk.yellow.bold('1.') +
|
|
@@ -753,76 +829,55 @@ function showTips(context) {
|
|
|
753
829
|
}
|
|
754
830
|
|
|
755
831
|
const successCode = 0;
|
|
832
|
+
const errorCode = -1;
|
|
756
833
|
const outputDir = getOutputDir();
|
|
757
834
|
const publicPath = path.join(__dirname, 'public');
|
|
758
|
-
let server;
|
|
759
835
|
async function start() {
|
|
760
836
|
const startTime = Date.now();
|
|
761
|
-
if (server) {
|
|
762
|
-
closeServer();
|
|
763
|
-
}
|
|
764
837
|
const app = express();
|
|
838
|
+
app.use(fileUpload()); // 启用 express-fileupload 中间件
|
|
839
|
+
// --- 中间件和路由设置 ---
|
|
765
840
|
app.use(expressStaticGzip(publicPath, {
|
|
766
841
|
enableBrotli: true,
|
|
767
842
|
orderPreference: ['br'],
|
|
768
843
|
}));
|
|
769
|
-
/**
|
|
770
|
-
* 支持跨域请求
|
|
771
|
-
*/
|
|
772
844
|
app.use((req, res, next) => {
|
|
773
845
|
res.header('Access-Control-Allow-Origin', '*');
|
|
774
846
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
775
847
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
776
848
|
next();
|
|
777
849
|
});
|
|
778
|
-
const port = DEV_PORT;
|
|
779
850
|
app.use(express.json());
|
|
780
|
-
// 解析 FormData body
|
|
781
851
|
app.use(express.urlencoded({ extended: true }));
|
|
782
|
-
/**
|
|
783
|
-
* 支持静态资源
|
|
784
|
-
*/
|
|
785
852
|
app.use('/game/files', express.static(outputDir));
|
|
786
853
|
app.get('/game/config', async (req, res) => {
|
|
787
|
-
const basic = await ttmgPack.getPkgs({
|
|
788
|
-
|
|
789
|
-
});
|
|
790
|
-
const { email, user_id } = getCurrentUser();
|
|
854
|
+
const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
|
|
855
|
+
const user = getCurrentUser();
|
|
791
856
|
const { clientKey } = getClientKey();
|
|
792
857
|
res.send({
|
|
793
858
|
error: null,
|
|
794
859
|
data: {
|
|
795
|
-
|
|
796
|
-
user_id,
|
|
860
|
+
user,
|
|
797
861
|
code: successCode,
|
|
798
|
-
nodeWsPort:
|
|
862
|
+
nodeWsPort: store.getState().nodeWsPort,
|
|
799
863
|
clientKey: clientKey,
|
|
800
|
-
schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&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
|
-
|
|
820
|
-
error: null,
|
|
821
|
-
|
|
822
|
-
...basic,
|
|
823
|
-
...gameInfo,
|
|
824
|
-
},
|
|
825
|
-
});
|
|
878
|
+
else {
|
|
879
|
+
res.send({ error: null, data: { ...basic, ...gameInfo } });
|
|
880
|
+
}
|
|
826
881
|
});
|
|
827
882
|
app.get('/game/check', async (req, res) => {
|
|
828
883
|
const checkResult = await ttmgPack.checkPkgs({
|
|
@@ -832,7 +887,7 @@ async function start() {
|
|
|
832
887
|
output: outputDir,
|
|
833
888
|
dev: {
|
|
834
889
|
enable: true,
|
|
835
|
-
port:
|
|
890
|
+
port: store.getState().nodeServerPort,
|
|
836
891
|
host: 'localhost',
|
|
837
892
|
enableSourcemap: false,
|
|
838
893
|
enableLog: false,
|
|
@@ -845,36 +900,113 @@ async function start() {
|
|
|
845
900
|
},
|
|
846
901
|
},
|
|
847
902
|
});
|
|
848
|
-
res.send({
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
903
|
+
res.send({ code: successCode, data: checkResult });
|
|
904
|
+
});
|
|
905
|
+
/**
|
|
906
|
+
* @description 上传游戏代码到服务器
|
|
907
|
+
*/
|
|
908
|
+
app.post('/game/upload', async (req, res) => {
|
|
909
|
+
const fileKeys = Object.keys(req.files);
|
|
910
|
+
const uploadedFile = req.files[fileKeys[0]];
|
|
911
|
+
if (!uploadedFile) {
|
|
912
|
+
res.status(400).send({ code: errorCode, data: 'No file uploaded' }); // 使用正确的 HTTP 状态码
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
// 通过 header 获取 desc
|
|
917
|
+
const desc = req.headers['ttmg-game-desc'];
|
|
918
|
+
// 直接传递需要的信息
|
|
919
|
+
const { data, error } = await uploadGameToPlatform({
|
|
920
|
+
data: uploadedFile.data, // 这是 Buffer
|
|
921
|
+
name: uploadedFile.name, // 这是文件名
|
|
922
|
+
gameId: getClientKey().clientKey,
|
|
923
|
+
desc,
|
|
924
|
+
});
|
|
925
|
+
if (error) {
|
|
926
|
+
res.send({ code: errorCode, error });
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
res.send({ code: successCode, data });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
// 错误处理可以更具体
|
|
934
|
+
let errorMessage = 'An unknown error occurred.';
|
|
935
|
+
if (error instanceof Error) {
|
|
936
|
+
errorMessage = error.message;
|
|
937
|
+
}
|
|
938
|
+
// 打印详细错误到服务器日志,方便排查
|
|
939
|
+
res.status(500).send({ code: errorCode, data: errorMessage }); // 使用正确的 HTTP 状态码
|
|
940
|
+
}
|
|
852
941
|
});
|
|
853
942
|
app.get('*', (req, res) => {
|
|
854
943
|
res.sendFile(path.join(publicPath, 'index.html'));
|
|
855
944
|
});
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
945
|
+
// --- 中间件和路由设置结束 ---
|
|
946
|
+
// 步骤 2: 用配置好的 app 实例创建一个 http.Server。我们只创建这一次!
|
|
947
|
+
const server = http.createServer(app);
|
|
948
|
+
/**
|
|
949
|
+
* @description 尝试在指定端口启动服务。这是个纯粹的辅助函数。
|
|
950
|
+
* @param {number} port - 要尝试的端口号。
|
|
951
|
+
* @returns {Promise<boolean>} 成功返回 true,因端口占用失败则返回 false。
|
|
952
|
+
*/
|
|
953
|
+
function tryListen(port) {
|
|
954
|
+
return new Promise(resolve => {
|
|
955
|
+
// 定义错误处理函数
|
|
956
|
+
const onError = err => {
|
|
957
|
+
// 清理掉另一个事件的监听器,防止内存泄漏
|
|
958
|
+
server.removeListener('listening', onListening);
|
|
959
|
+
if (err.code === 'EADDRINUSE') {
|
|
960
|
+
console.log(chalk(`Port ${port} is already in use, trying ${port + 1}...`));
|
|
961
|
+
resolve(false); // 明确表示因端口占用而失败
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
// 对于其他致命错误,直接退出进程
|
|
965
|
+
console.log(chalk.red.bold(err.message));
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
// 定义成功处理函数
|
|
970
|
+
const onListening = () => {
|
|
971
|
+
// 清理掉另一个事件的监听器
|
|
972
|
+
server.removeListener('error', onError);
|
|
973
|
+
resolve(true); // 明确表示成功
|
|
974
|
+
};
|
|
975
|
+
// 使用 .once() 来确保监听器只执行一次然后自动移除
|
|
976
|
+
server.once('error', onError);
|
|
977
|
+
server.once('listening', onListening);
|
|
978
|
+
// 执行监听动作
|
|
979
|
+
server.listen(port);
|
|
865
980
|
});
|
|
866
|
-
}
|
|
867
|
-
|
|
981
|
+
}
|
|
982
|
+
// 步骤 3: 使用循环来线性、串行地尝试启动服务
|
|
983
|
+
let isListening = false;
|
|
984
|
+
const maxRetries = 20; // 设置一个最大重试次数,以防万一
|
|
985
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
986
|
+
const currentPort = store.getState().nodeServerPort;
|
|
987
|
+
isListening = await tryListen(currentPort);
|
|
988
|
+
if (isListening) {
|
|
989
|
+
break; // 成功,跳出循环
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
// 失败(端口占用),更新端口号,准备下一次循环
|
|
993
|
+
store.setState({ nodeServerPort: currentPort + 1 });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// 步骤 4: 检查最终结果,如果所有尝试都失败了,则退出
|
|
997
|
+
if (!isListening) {
|
|
998
|
+
console.log(chalk.red.bold(`Failed to start server after trying ${maxRetries} ports.`));
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
// --- 服务启动成功后的逻辑 ---
|
|
1002
|
+
// @ts-ignore
|
|
1003
|
+
const finalPort = server.address().port; // 从成功的 server 实例安全地获取最终端口
|
|
1004
|
+
const version = require(path.join(__dirname, 'package.json')).version;
|
|
1005
|
+
console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(`ready in`), chalk.bold(`${Date.now() - startTime}ms`));
|
|
1006
|
+
const baseUrl = `http://localhost:${finalPort}`;
|
|
868
1007
|
showTips({ server: baseUrl });
|
|
869
1008
|
openUrl(baseUrl);
|
|
870
1009
|
}
|
|
871
|
-
function closeServer() {
|
|
872
|
-
server.close(() => {
|
|
873
|
-
console.log('Dev server closed');
|
|
874
|
-
server = null;
|
|
875
|
-
process.exit(0);
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
1010
|
|
|
879
1011
|
/**
|
|
880
1012
|
* 一个类型安全的、通用的事件发射器类。
|
|
@@ -951,63 +1083,12 @@ class TypedEventEmitter {
|
|
|
951
1083
|
}
|
|
952
1084
|
const eventEmitter = new TypedEventEmitter();
|
|
953
1085
|
|
|
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
1086
|
class WsServer {
|
|
1009
1087
|
constructor() {
|
|
1010
|
-
this.
|
|
1088
|
+
this.startWsServer(store.getState().nodeWsPort);
|
|
1089
|
+
}
|
|
1090
|
+
startWsServer(port) {
|
|
1091
|
+
this.ws = new WebSocket.Server({ port });
|
|
1011
1092
|
this.ws.on('connection', ws => {
|
|
1012
1093
|
const { clientHttpPort, clientHost, clientWsPort } = store.getState();
|
|
1013
1094
|
if (clientHost) {
|
|
@@ -1041,8 +1122,6 @@ class WsServer {
|
|
|
1041
1122
|
* 关闭调试服务
|
|
1042
1123
|
*/
|
|
1043
1124
|
this.ws.close();
|
|
1044
|
-
closeServer();
|
|
1045
|
-
console.log('close server');
|
|
1046
1125
|
break;
|
|
1047
1126
|
}
|
|
1048
1127
|
}
|
|
@@ -1112,6 +1191,18 @@ class WsServer {
|
|
|
1112
1191
|
}
|
|
1113
1192
|
});
|
|
1114
1193
|
});
|
|
1194
|
+
this.ws.on('error', err => {
|
|
1195
|
+
if (err.code === 'EADDRINUSE') {
|
|
1196
|
+
store.setState({
|
|
1197
|
+
nodeWsPort: store.getState().nodeWsPort + 1,
|
|
1198
|
+
});
|
|
1199
|
+
this.startWsServer(store.getState().nodeWsPort);
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
console.log(chalk.red.bold(err.message));
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1115
1206
|
}
|
|
1116
1207
|
send(params) {
|
|
1117
1208
|
this.ws.clients.forEach(client => {
|
|
@@ -1225,7 +1316,7 @@ function compile(context) {
|
|
|
1225
1316
|
context: {
|
|
1226
1317
|
clientKey,
|
|
1227
1318
|
outputDir,
|
|
1228
|
-
devPort:
|
|
1319
|
+
devPort: store.getState().nodeServerPort,
|
|
1229
1320
|
entryDir,
|
|
1230
1321
|
},
|
|
1231
1322
|
});
|
|
@@ -1331,7 +1422,7 @@ async function zipDirectory(sourceDir, outPath) {
|
|
|
1331
1422
|
}
|
|
1332
1423
|
async function uploadZip(zipPath, callback) {
|
|
1333
1424
|
const startTime = Date.now();
|
|
1334
|
-
const form = new FormData();
|
|
1425
|
+
const form = new FormData$1();
|
|
1335
1426
|
form.append('file', fs.createReadStream(zipPath), {
|
|
1336
1427
|
filename: 'upload.zip',
|
|
1337
1428
|
contentType: 'application/zip',
|
|
@@ -1467,7 +1558,7 @@ async function upload() {
|
|
|
1467
1558
|
});
|
|
1468
1559
|
}
|
|
1469
1560
|
|
|
1470
|
-
var version = "0.1.9-beta.
|
|
1561
|
+
var version = "0.1.9-beta.9";
|
|
1471
1562
|
var pkg = {
|
|
1472
1563
|
version: version};
|
|
1473
1564
|
|