@ttmg/cli 0.1.3-beta.4 → 0.1.3
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/CHANGELOG.md +11 -2
- package/dist/index.js +687 -816
- package/dist/index.js.map +1 -1
- package/dist/public/assets/index-B05gzsFQ.css +1 -0
- package/dist/public/dev-setup.js +332 -0
- package/dist/public/index.html +24 -0
- package/dist/public/scripts/krypton.js +11 -0
- package/dist/public/scripts/ttmg-core.js +24 -0
- package/dist/template/open_context.html.hbs +30 -0
- package/dist/template/open_context_sdk.js.hbs +16 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var commander = require('commander');
|
|
5
|
-
var
|
|
5
|
+
var inquirer = require('inquirer');
|
|
6
6
|
var fs = require('fs');
|
|
7
|
-
var
|
|
8
|
-
var
|
|
7
|
+
var jsdom = require('jsdom');
|
|
8
|
+
var prettier = require('prettier');
|
|
9
9
|
var chalk = require('chalk');
|
|
10
10
|
var express = require('express');
|
|
11
11
|
var path = require('path');
|
|
12
|
-
var
|
|
12
|
+
var cheerio = require('cheerio');
|
|
13
13
|
var chromeLauncher = require('chrome-launcher');
|
|
14
14
|
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
|
|
19
|
-
var
|
|
20
|
-
var
|
|
18
|
+
var handlebars = require('handlebars');
|
|
19
|
+
var esbuild = require('esbuild');
|
|
20
|
+
var archiver = require('archiver');
|
|
21
21
|
var WebSocket = require('ws');
|
|
22
22
|
var glob = require('glob');
|
|
23
23
|
var got = require('got');
|
|
@@ -26,270 +26,171 @@ var ttmgPack = require('ttmg-pack');
|
|
|
26
26
|
var QRCode = require('qrcode');
|
|
27
27
|
|
|
28
28
|
function _interopNamespaceDefault(e) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
var n = Object.create(null);
|
|
30
|
+
if (e) {
|
|
31
|
+
Object.keys(e).forEach(function (k) {
|
|
32
|
+
if (k !== 'default') {
|
|
33
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
34
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
get: function () { return e[k]; }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
n.default = e;
|
|
42
|
+
return Object.freeze(n);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function getAugmentedNamespace(n) {
|
|
52
|
-
if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
|
|
53
|
-
var f = n.default;
|
|
54
|
-
if (typeof f == "function") {
|
|
55
|
-
var a = function a () {
|
|
56
|
-
var isInstance = false;
|
|
57
|
-
try {
|
|
58
|
-
isInstance = this instanceof a;
|
|
59
|
-
} catch {}
|
|
60
|
-
if (isInstance) {
|
|
61
|
-
return Reflect.construct(f, arguments, this.constructor);
|
|
62
|
-
}
|
|
63
|
-
return f.apply(this, arguments);
|
|
64
|
-
};
|
|
65
|
-
a.prototype = f.prototype;
|
|
66
|
-
} else a = {};
|
|
67
|
-
Object.defineProperty(a, '__esModule', {value: true});
|
|
68
|
-
Object.keys(n).forEach(function (k) {
|
|
69
|
-
var d = Object.getOwnPropertyDescriptor(n, k);
|
|
70
|
-
Object.defineProperty(a, k, d.get ? d : {
|
|
71
|
-
enumerable: true,
|
|
72
|
-
get: function () {
|
|
73
|
-
return n[k];
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
return a;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
var config;
|
|
81
|
-
var hasRequiredConfig;
|
|
82
|
-
|
|
83
|
-
function requireConfig () {
|
|
84
|
-
if (hasRequiredConfig) return config;
|
|
85
|
-
hasRequiredConfig = 1;
|
|
86
|
-
config = {
|
|
87
|
-
CONFIG_FILE_NAME: 'minigame.config.json',
|
|
88
|
-
SDK_URL: 'https://connect.tiktok-minis.com/game/sdk.js',
|
|
89
|
-
VCONSOLE_URL: 'https://connect.tiktok-minis.com/libs/vConsole.js',
|
|
90
|
-
VCONSOLE_INIT: `
|
|
47
|
+
const CONFIG_FILE_NAME = 'minigame.config.json';
|
|
48
|
+
const SDK_URL = 'https://connect.tiktok-minis.com/game/sdk.js';
|
|
49
|
+
const VCONSOLE_URL = 'https://connect.tiktok-minis.com/libs/vConsole.js';
|
|
50
|
+
const VCONSOLE_INIT = `
|
|
91
51
|
if(typeof VConsole === 'function') {
|
|
92
52
|
window.vConsole = new VConsole();
|
|
93
53
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
54
|
+
`;
|
|
55
|
+
const MINIS_MANIFEST_FILE_NAME = 'minis.manifest.json';
|
|
56
|
+
const MINIS_RUNTIME_URL = 'https://www.tiktok.com/minigames/runtime';
|
|
57
|
+
|
|
58
|
+
const { JSDOM } = jsdom;
|
|
59
|
+
const CONFIG_PATH = `${process.cwd()}/${CONFIG_FILE_NAME}`;
|
|
60
|
+
const INDEX_HTML_PATH = `${process.cwd()}/index.html`;
|
|
61
|
+
function isSandbox(clientKey) {
|
|
62
|
+
/**
|
|
63
|
+
* sb 开头的 clientKey 都是 sandbox 环境
|
|
64
|
+
*/
|
|
65
|
+
return clientKey.startsWith('sb');
|
|
99
66
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// 2. 读取 index.html
|
|
141
|
-
if (!fs$1.existsSync(INDEX_HTML_PATH)) {
|
|
142
|
-
console.error('index.html does not exist');
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const indexHtml = fs$1.readFileSync(INDEX_HTML_PATH, 'utf8');
|
|
146
|
-
const dom = new JSDOM(indexHtml);
|
|
147
|
-
const { document } = dom.window;
|
|
148
|
-
|
|
149
|
-
// 3. head 标签
|
|
150
|
-
let head = document.querySelector('head');
|
|
151
|
-
if (!head) {
|
|
152
|
-
head = document.createElement('head');
|
|
153
|
-
document.documentElement.insertBefore(head, document.body);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 4. 检查是否已注入 SDK
|
|
157
|
-
const scriptList = Array.from(document.querySelectorAll('script'));
|
|
158
|
-
const hasSDK = scriptList.some(
|
|
159
|
-
script => script.src && script.src.includes(SDK_URL),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// 5. 检查是否已注入 vConsole
|
|
163
|
-
const hasVConsole = scriptList.some(
|
|
164
|
-
script => script.src && script.src.includes('vConsole.js'),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
let lastInitScript = null;
|
|
168
|
-
if (!hasSDK) {
|
|
169
|
-
// 插入 SDK 脚本
|
|
170
|
-
const sdkScript = document.createElement('script');
|
|
171
|
-
sdkScript.src = SDK_URL;
|
|
172
|
-
head.insertBefore(sdkScript, head.firstChild);
|
|
173
|
-
|
|
174
|
-
// 插入 SDK init 脚本
|
|
175
|
-
const initScript = document.createElement('script');
|
|
176
|
-
initScript.innerHTML = `
|
|
67
|
+
// 判断是否是 TTMinis.game.init 的初始化脚本
|
|
68
|
+
function isTTMinisInitScript(script, clientKey) {
|
|
69
|
+
if (!script.innerHTML) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return (script.innerHTML.includes('TTMinis.game.init') &&
|
|
73
|
+
script.innerHTML.includes(clientKey));
|
|
74
|
+
}
|
|
75
|
+
async function injectScripts({ config, clientKey }) {
|
|
76
|
+
// 1. 检查 config 文件是否已存在
|
|
77
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
78
|
+
// 2. 读取 index.html
|
|
79
|
+
if (!fs.existsSync(INDEX_HTML_PATH)) {
|
|
80
|
+
console.error('index.html does not exist');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const indexHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8');
|
|
84
|
+
const dom = new JSDOM(indexHtml);
|
|
85
|
+
const { document } = dom.window;
|
|
86
|
+
// 3. head 标签
|
|
87
|
+
let head = document.querySelector('head');
|
|
88
|
+
if (!head) {
|
|
89
|
+
head = document.createElement('head');
|
|
90
|
+
document.documentElement.insertBefore(head, document.body);
|
|
91
|
+
}
|
|
92
|
+
// 4. 检查是否已注入 SDK
|
|
93
|
+
const scriptList = Array.from(document.querySelectorAll('script'));
|
|
94
|
+
const hasSDK = scriptList.some(script => script.src && script.src.includes(SDK_URL));
|
|
95
|
+
// 5. 检查是否已注入 vConsole
|
|
96
|
+
const hasVConsole = scriptList.some(script => script.src && script.src.includes('vConsole.js'));
|
|
97
|
+
let lastInitScript = null;
|
|
98
|
+
if (!hasSDK) {
|
|
99
|
+
// 插入 SDK 脚本
|
|
100
|
+
const sdkScript = document.createElement('script');
|
|
101
|
+
sdkScript.src = SDK_URL;
|
|
102
|
+
head.insertBefore(sdkScript, head.firstChild);
|
|
103
|
+
// 插入 SDK init 脚本
|
|
104
|
+
const initScript = document.createElement('script');
|
|
105
|
+
initScript.innerHTML = `
|
|
177
106
|
window.TTMinis = TTMinis;
|
|
178
107
|
TTMinis.game.init({
|
|
179
108
|
clientKey: "${clientKey}",
|
|
180
109
|
});
|
|
181
110
|
`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const formattedHtml = await prettier.format(dom.serialize(), {
|
|
232
|
-
parser: 'html',
|
|
233
|
-
});
|
|
234
|
-
fs$1.writeFileSync(INDEX_HTML_PATH, formattedHtml);
|
|
235
|
-
console.log(
|
|
236
|
-
chalk$1.green.bold(
|
|
237
|
-
'TikTok H5 Mini Game initialization has been completed...',
|
|
238
|
-
),
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
inject = injectScripts;
|
|
243
|
-
return inject;
|
|
111
|
+
head.insertBefore(initScript, sdkScript.nextSibling);
|
|
112
|
+
lastInitScript = initScript;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// 已经有 SDK,查找 TTMinis.game.init 脚本
|
|
116
|
+
const headScripts = Array.from(head.querySelectorAll('script'));
|
|
117
|
+
lastInitScript = headScripts.find(script => isTTMinisInitScript(script, clientKey));
|
|
118
|
+
if (lastInitScript) {
|
|
119
|
+
console.log('JS SDK 已接入,跳过 SDK 相关脚本注入');
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// 没有 TTMinis.game.init,则查找最后一个 SDK 脚本
|
|
123
|
+
const sdkScripts = headScripts.filter(script => script.src && script.src.includes(SDK_URL));
|
|
124
|
+
if (sdkScripts.length > 0) {
|
|
125
|
+
lastInitScript = sdkScripts[sdkScripts.length - 1];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 只有 Sandbox 环境才需要注入 vConsole
|
|
131
|
+
*/
|
|
132
|
+
if (isSandbox(clientKey)) {
|
|
133
|
+
console.log('Sandbox 环境,跳过 vConsole 相关脚本注入');
|
|
134
|
+
// 8. 插入 vConsole 相关脚本(如果需要)
|
|
135
|
+
if (!hasVConsole) {
|
|
136
|
+
// vConsole 相关脚本的插入点
|
|
137
|
+
let insertAfterNode = lastInitScript;
|
|
138
|
+
if (insertAfterNode) {
|
|
139
|
+
insertAfterNode = insertAfterNode.nextSibling;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
insertAfterNode = head.firstChild;
|
|
143
|
+
}
|
|
144
|
+
// vConsole 源码
|
|
145
|
+
const vconsoleSourceScript = document.createElement('script');
|
|
146
|
+
vconsoleSourceScript.src = VCONSOLE_URL;
|
|
147
|
+
// vConsole 初始化
|
|
148
|
+
const vconsoleInitScript = document.createElement('script');
|
|
149
|
+
vconsoleInitScript.innerHTML = VCONSOLE_INIT;
|
|
150
|
+
head.insertBefore(vconsoleSourceScript, insertAfterNode);
|
|
151
|
+
head.insertBefore(vconsoleInitScript, insertAfterNode);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 9. 格式化并写回 index.html
|
|
155
|
+
const formattedHtml = await prettier.format(dom.serialize(), {
|
|
156
|
+
parser: 'html',
|
|
157
|
+
});
|
|
158
|
+
fs.writeFileSync(INDEX_HTML_PATH, formattedHtml);
|
|
159
|
+
console.log(chalk.green.bold('TikTok H5 Mini Game initialization has been completed...'));
|
|
244
160
|
}
|
|
245
161
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
port: devPort,
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
await inject({ clientKey, config });
|
|
280
|
-
|
|
281
|
-
process.exit(0);
|
|
282
|
-
})
|
|
283
|
-
.catch(() => {
|
|
284
|
-
process.exit(1);
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
|
-
return init;
|
|
162
|
+
function init() {
|
|
163
|
+
const promptModule = inquirer.createPromptModule();
|
|
164
|
+
promptModule([
|
|
165
|
+
{
|
|
166
|
+
type: 'input',
|
|
167
|
+
name: 'clientKey',
|
|
168
|
+
message: 'Please input client key',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'input',
|
|
172
|
+
name: 'devPort',
|
|
173
|
+
message: 'Please input dev port',
|
|
174
|
+
default: 9527,
|
|
175
|
+
},
|
|
176
|
+
])
|
|
177
|
+
.then(async (answers) => {
|
|
178
|
+
const { clientKey, devPort } = answers;
|
|
179
|
+
const config = {
|
|
180
|
+
_comment: 'orientation is the orientation of the game. It can be either \'VERTICAL\' or \'HORIZONTAL\'.our game default is VERTICAL; minigame.config.json dev is a configuration file for minigame development. You can use it to configure the minigame.',
|
|
181
|
+
orientation: 'VERTICAL',
|
|
182
|
+
dev: {
|
|
183
|
+
port: devPort,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
await injectScripts({ clientKey, config });
|
|
187
|
+
process.exit(0);
|
|
188
|
+
})
|
|
189
|
+
.catch(() => {
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|
|
288
192
|
}
|
|
289
193
|
|
|
290
|
-
var initExports = requireInit();
|
|
291
|
-
var index$2 = /*@__PURE__*/getDefaultExportFromCjs(initExports);
|
|
292
|
-
|
|
293
194
|
async function openUrl(url) {
|
|
294
195
|
try {
|
|
295
196
|
await chromeLauncher.launch({
|
|
@@ -302,6 +203,7 @@ async function openUrl(url) {
|
|
|
302
203
|
'--remote-allow-origins=*',
|
|
303
204
|
'--user-data-dir=/tmp/chrome-debug-profile',
|
|
304
205
|
'--disable-popup-blocking',
|
|
206
|
+
'--force-dark-mode=off',
|
|
305
207
|
],
|
|
306
208
|
});
|
|
307
209
|
await new Promise(() => { });
|
|
@@ -312,29 +214,6 @@ async function openUrl(url) {
|
|
|
312
214
|
}
|
|
313
215
|
}
|
|
314
216
|
|
|
315
|
-
function getDesktopPath() {
|
|
316
|
-
const homeDir = os.homedir();
|
|
317
|
-
// 常见桌面文件夹名
|
|
318
|
-
const desktopNames = ['Desktop', '桌面'];
|
|
319
|
-
for (const name of desktopNames) {
|
|
320
|
-
const desktopPath = path.join(homeDir, name);
|
|
321
|
-
if (fs.existsSync(desktopPath)) {
|
|
322
|
-
return desktopPath;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// 没找到就默认返回 Desktop
|
|
326
|
-
return path.join(homeDir, 'Desktop');
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function centerQRCode(qrString) {
|
|
330
|
-
const terminalWidth = process.stdout.columns || 80; // 获取终端宽度,默认80
|
|
331
|
-
const lines = qrString.split('\n');
|
|
332
|
-
const qrWidth = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
333
|
-
const padding = Math.floor((terminalWidth - qrWidth) / 3);
|
|
334
|
-
const padStr = ' '.repeat(padding > 0 ? padding : 0);
|
|
335
|
-
return lines.map(line => padStr + line).join('\n');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
217
|
function getLocalIP() {
|
|
339
218
|
const networkInterfaces = os.networkInterfaces();
|
|
340
219
|
for (const interfaceName in networkInterfaces) {
|
|
@@ -414,356 +293,281 @@ To update, run: ${chalk.magenta(`npm i -g ${pkgName}`)}
|
|
|
414
293
|
}
|
|
415
294
|
}
|
|
416
295
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const fs$1 = fs;
|
|
437
|
-
const chalk$1 = chalk;
|
|
438
|
-
const cheerio = require$$4;
|
|
439
|
-
const app = express$1();
|
|
440
|
-
const { openUrl } = require$$5;
|
|
441
|
-
const crypto = require$$6;
|
|
442
|
-
// const open = require('open'); // 引入 open 包
|
|
443
|
-
const { CONFIG_FILE_NAME, MINIS_RUNTIME_URL } = requireConfig();
|
|
444
|
-
|
|
445
|
-
// 1. 检查配置文件是否存在
|
|
446
|
-
dev$1 = function dev() {
|
|
447
|
-
const configPath = path$1.join(process.cwd(), CONFIG_FILE_NAME);
|
|
448
|
-
if (!fs$1.existsSync(configPath)) {
|
|
449
|
-
console.log(
|
|
450
|
-
chalk$1.red.bold(
|
|
451
|
-
`${CONFIG_FILE_NAME} is not exist, please run minis game init first`,
|
|
452
|
-
),
|
|
453
|
-
);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// 2. 读取配置
|
|
458
|
-
const gameConfig = JSON.parse(fs$1.readFileSync(configPath, 'utf8'));
|
|
459
|
-
const devPort = gameConfig.dev?.port || 9527;
|
|
460
|
-
|
|
461
|
-
// 3. 打印开发前提示
|
|
462
|
-
console.log(
|
|
463
|
-
chalk$1.yellow.bold(
|
|
464
|
-
`⚠️ Before dev, please ensure:\n 1. The account used to login www.tiktok.com is in the sandbox target user range of Minis developer platform, otherwise login authorization will throw an error.\n 2. The browser allows www.tiktok.com <popup and redirect>, because the authorization login linkage needs to open a new tab popup for operation, otherwise the authorization login linkage will not be able to debug normally.`,
|
|
465
|
-
),
|
|
466
|
-
);
|
|
467
|
-
console.log(
|
|
468
|
-
chalk$1.bold.blue(
|
|
469
|
-
'\n \n============== start dev your game, it will take a few seconds ============ \n \n',
|
|
470
|
-
),
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* 支持 .br 文件, 支持 gzip
|
|
475
|
-
*/
|
|
476
|
-
|
|
477
|
-
app.use((req, res, next) => {
|
|
478
|
-
if (req.url.endsWith('.br')) {
|
|
479
|
-
res.setHeader('Content-Encoding', 'br');
|
|
480
|
-
} else if (req.url.endsWith('.gz')) {
|
|
481
|
-
res.setHeader('Content-Encoding', 'gzip');
|
|
482
|
-
}
|
|
483
|
-
next();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* 给所有的请求返回设置 CSP
|
|
488
|
-
*/
|
|
489
|
-
app.use((req, res, next) => {
|
|
490
|
-
/**
|
|
491
|
-
* 计算 HTML 中的内联脚本生成 hash 插入 CSP 中
|
|
492
|
-
*/
|
|
493
|
-
try {
|
|
494
|
-
// 1. 读取 HTML 文件内容
|
|
495
|
-
const htmlPath = path$1.join(process.cwd(), 'index.html');
|
|
496
|
-
const html = fs$1.readFileSync(htmlPath, 'utf8');
|
|
497
|
-
|
|
498
|
-
// 2. 用 cheerio 解析 HTML
|
|
499
|
-
const $ = cheerio.load(html);
|
|
500
|
-
|
|
501
|
-
// 3. 提取所有无 src 属性的内联 <script> 内容
|
|
502
|
-
const scripts = [];
|
|
503
|
-
$('script:not([src])').each((i, elem) => {
|
|
504
|
-
const content = $(elem).html();
|
|
505
|
-
if (content && content.trim()) {
|
|
506
|
-
scripts.push(content);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// 4. 计算每段脚本的 SHA-256 hash 并 base64 编码
|
|
511
|
-
const hashes = scripts.map(script => {
|
|
512
|
-
const hash = crypto
|
|
513
|
-
.createHash('sha256')
|
|
514
|
-
.update(script, 'utf8')
|
|
515
|
-
.digest('base64');
|
|
516
|
-
return `'sha256-${hash}'`;
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// 开发者本地调试,信任的域名默认为 * 便于调试
|
|
520
|
-
const devTrustedDomain = '*';
|
|
521
|
-
|
|
522
|
-
res.setHeader(
|
|
523
|
-
'Content-Security-Policy',
|
|
524
|
-
`default-src 'self';script-src 'self' data: blob: 'unsafe-eval' 'unsafe-inline' connect.tiktok-minis.com sf-connect.tiktokminis.us;img-src 'self' ${devTrustedDomain} data: blob: *; connect-src 'self' ${devTrustedDomain} data: blob: ; style-src 'self' ${devTrustedDomain} 'unsafe-inline' fonts.googleapis.com data: blob: *; font-src 'self' fonts.gstatic.com blob: data: *; media-src 'self' ${devTrustedDomain} data: blob: *; frame-src 'none'; base-uri 'self'; worker-src 'self' blob: data: ;`,
|
|
525
|
-
);
|
|
526
|
-
} catch (e) {
|
|
527
|
-
// 如果 index.html 不存在或有异常,CSP 头就不设置
|
|
528
|
-
console.warn(chalk$1.red('Failed to set CSP header:'), e.message);
|
|
529
|
-
}
|
|
530
|
-
next();
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
// 4. 静态资源服务
|
|
534
|
-
app.use(express$1.static(path$1.join(process.cwd())));
|
|
535
|
-
|
|
536
|
-
// 5. 启动服务并自动打开浏览器
|
|
537
|
-
app.listen(devPort, () => {
|
|
538
|
-
const gameUrl = `http://localhost:${devPort}`;
|
|
539
|
-
const devUrl = `${MINIS_RUNTIME_URL}?minis_url=${gameUrl}&enable_log=1`;
|
|
540
|
-
|
|
541
|
-
console.log(
|
|
542
|
-
`you can access ${chalk$1.green.underline.bold(
|
|
543
|
-
devUrl,
|
|
544
|
-
)} to debug your game in browser...`,
|
|
545
|
-
);
|
|
546
|
-
try {
|
|
547
|
-
// 自动打开浏览器,跨平台
|
|
548
|
-
openUrl(devUrl);
|
|
549
|
-
} catch (e) {
|
|
550
|
-
console.warn(
|
|
551
|
-
chalk$1.red('Failed to open browser, you can access it manually'),
|
|
552
|
-
e.message,
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
};
|
|
557
|
-
return dev$1;
|
|
296
|
+
function buildOpenContext(sourcePath) {
|
|
297
|
+
const result = esbuild.buildSync({
|
|
298
|
+
entryPoints: [sourcePath],
|
|
299
|
+
bundle: true,
|
|
300
|
+
platform: 'browser',
|
|
301
|
+
format: 'iife',
|
|
302
|
+
minify: true,
|
|
303
|
+
write: false,
|
|
304
|
+
loader: {
|
|
305
|
+
'.png': 'dataurl',
|
|
306
|
+
'.jpg': 'dataurl',
|
|
307
|
+
'.jpeg': 'dataurl',
|
|
308
|
+
'.gif': 'dataurl',
|
|
309
|
+
'.svg': 'dataurl',
|
|
310
|
+
'.webp': 'dataurl',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const jsCode = result.outputFiles[0].text;
|
|
314
|
+
return jsCode;
|
|
558
315
|
}
|
|
559
316
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
try {
|
|
575
|
-
const buildPath = path$1.join(process.cwd());
|
|
576
|
-
const resourceList = [];
|
|
577
|
-
const allFiles = collectAllFiles(buildPath);
|
|
578
|
-
|
|
579
|
-
Object.keys(allFiles)
|
|
580
|
-
.filter(file => !file.endsWith('.map'))
|
|
581
|
-
.forEach(file => {
|
|
582
|
-
const relativeFilePath = allFiles[file];
|
|
583
|
-
const filePathArr = relativeFilePath.split('/').filter(Boolean); // Split filename
|
|
584
|
-
const fileName = filePathArr.pop() || ''; // Get filename
|
|
585
|
-
if (filePathArr.length === 0) {
|
|
586
|
-
resourceList.push({ type: 'file', name: fileName });
|
|
587
|
-
} else {
|
|
588
|
-
const folder = findOrCreateFolder(filePathArr, resourceList);
|
|
589
|
-
folder.children.push({ type: 'file', name: fileName });
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
fs$1.writeFileSync(
|
|
594
|
-
path$1.join(buildPath, MINIS_MANIFEST_FILE_NAME),
|
|
595
|
-
JSON.stringify(
|
|
596
|
-
{ name: MINIS_MANIFEST_FILE_NAME, resource_list: resourceList },
|
|
597
|
-
null,
|
|
598
|
-
2,
|
|
599
|
-
),
|
|
600
|
-
);
|
|
601
|
-
} catch (error) {
|
|
602
|
-
console.error(chalk$1.red(`Error during debug process: ${error.message}`));
|
|
603
|
-
if (error instanceof Error && error.stack) {
|
|
604
|
-
console.error(chalk$1.red(`Stack trace: ${error.stack}`));
|
|
605
|
-
}
|
|
606
|
-
process.exit(1);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function findOrCreateFolder(pathArray, currentFolder) {
|
|
611
|
-
let folder = currentFolder.find(
|
|
612
|
-
item => item.type === 'folder' && item.name === pathArray[0],
|
|
613
|
-
);
|
|
614
|
-
if (!folder) {
|
|
615
|
-
folder = { type: 'folder', name: pathArray[0], children: [] };
|
|
616
|
-
currentFolder.push(folder);
|
|
617
|
-
}
|
|
618
|
-
if (pathArray.length > 1) {
|
|
619
|
-
return findOrCreateFolder(pathArray.slice(1), folder.children);
|
|
620
|
-
}
|
|
621
|
-
return folder;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function collectAllFiles(dir, baseDir = dir) {
|
|
625
|
-
const files = {};
|
|
626
|
-
fs$1.readdirSync(dir).forEach(file => {
|
|
627
|
-
const filePath = path$1.join(dir, file);
|
|
628
|
-
const fileStat = fs$1.statSync(filePath);
|
|
629
|
-
if (fileStat.isDirectory()) {
|
|
630
|
-
// Process subdirectories recursively
|
|
631
|
-
const subFiles = collectAllFiles(filePath, baseDir);
|
|
632
|
-
Object.assign(files, subFiles);
|
|
633
|
-
} else if (path$1.extname(file) !== '' && file) {
|
|
634
|
-
const relativePath =
|
|
635
|
-
'/' + path$1.relative(baseDir, filePath).replace(/\\/g, '/');
|
|
636
|
-
files[filePath] = relativePath;
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
return files;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
manifest = {
|
|
643
|
-
buildMinisManifest,
|
|
644
|
-
};
|
|
645
|
-
return manifest;
|
|
317
|
+
function generateOpenContextHtml(openContextPath, appId) {
|
|
318
|
+
const templatePath = path.join(__dirname, 'template/open_context.html.hbs');
|
|
319
|
+
const sdkPath = path.join(__dirname, 'template/open_context_sdk.js.hbs');
|
|
320
|
+
const templateSource = fs.readFileSync(templatePath).toString();
|
|
321
|
+
const sdkSource = fs.readFileSync(sdkPath).toString();
|
|
322
|
+
const template = handlebars.compile(templateSource);
|
|
323
|
+
const openContextJsPath = path.join(process.cwd(), `${openContextPath}/index.js`);
|
|
324
|
+
const jsCode = buildOpenContext(openContextJsPath);
|
|
325
|
+
const openContextRes = template({
|
|
326
|
+
open_context_code: encodeURIComponent(jsCode),
|
|
327
|
+
open_context_sdk: sdkSource,
|
|
328
|
+
client_key: appId,
|
|
329
|
+
});
|
|
330
|
+
fs.writeFileSync('./open_context.html', openContextRes);
|
|
646
331
|
}
|
|
647
332
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
333
|
+
const app = express();
|
|
334
|
+
// 1. 检查配置文件是否存在
|
|
335
|
+
function dev$1() {
|
|
336
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
337
|
+
if (!fs.existsSync(configPath)) {
|
|
338
|
+
console.log(chalk.red.bold(`${CONFIG_FILE_NAME} is not exist, please run minis game init first`));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// 2. 读取配置
|
|
342
|
+
const gameConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
343
|
+
const devPort = gameConfig.dev?.port || 9527;
|
|
344
|
+
const hasOpenContext = !!gameConfig.openDataContext;
|
|
345
|
+
if (hasOpenContext) {
|
|
346
|
+
generateOpenContextHtml(gameConfig.openDataContext, gameConfig.app_id);
|
|
347
|
+
}
|
|
348
|
+
// 3. 打印开发前提示
|
|
349
|
+
console.log(chalk.yellow.bold('⚠️ Before dev, please ensure:\n 1. The account used to login www.tiktok.com is in the sandbox target user range of Minis developer platform, otherwise login authorization will throw an error.\n 2. The browser allows www.tiktok.com <popup and redirect>, because the authorization login linkage needs to open a new tab popup for operation, otherwise the authorization login linkage will not be able to debug normally.'));
|
|
350
|
+
console.log(chalk.bold.blue('\n \n============== start dev your game, it will take a few seconds ============ \n \n'));
|
|
351
|
+
/**
|
|
352
|
+
* 支持 .br 文件, 支持 gzip
|
|
353
|
+
*/
|
|
354
|
+
app.use((req, res, next) => {
|
|
355
|
+
if (req.url.endsWith('.br')) {
|
|
356
|
+
res.setHeader('Content-Encoding', 'br');
|
|
357
|
+
}
|
|
358
|
+
else if (req.url.endsWith('.gz')) {
|
|
359
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
360
|
+
}
|
|
361
|
+
next();
|
|
362
|
+
});
|
|
363
|
+
/**
|
|
364
|
+
* 给所有的请求返回设置 CSP
|
|
365
|
+
*/
|
|
366
|
+
app.use((req, res, next) => {
|
|
367
|
+
/**
|
|
368
|
+
* 计算 HTML 中的内联脚本生成 hash 插入 CSP 中
|
|
369
|
+
*/
|
|
370
|
+
try {
|
|
371
|
+
// 1. 读取 HTML 文件内容
|
|
372
|
+
const htmlPath = path.join(process.cwd(), 'index.html');
|
|
373
|
+
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
374
|
+
// 2. 用 cheerio 解析 HTML
|
|
375
|
+
const $ = cheerio.load(html);
|
|
376
|
+
// 3. 提取所有无 src 属性的内联 <script> 内容
|
|
377
|
+
const scripts = [];
|
|
378
|
+
$('script:not([src])').each((i, elem) => {
|
|
379
|
+
const content = $(elem).html();
|
|
380
|
+
if (content && content.trim()) {
|
|
381
|
+
scripts.push(content);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// 4. 计算每段脚本的 SHA-256 hash 并 base64 编码
|
|
385
|
+
// const hashes = scripts.map(script => {
|
|
386
|
+
// const hash = crypto
|
|
387
|
+
// .createHash('sha256')
|
|
388
|
+
// .update(script, 'utf8')
|
|
389
|
+
// .digest('base64');
|
|
390
|
+
// return `'sha256-${hash}'`;
|
|
391
|
+
// });
|
|
392
|
+
// 开发者本地调试,信任的域名默认为 * 便于调试
|
|
393
|
+
const devTrustedDomain = '*';
|
|
394
|
+
res.setHeader('Content-Security-Policy', `default-src 'self';script-src 'self' data: blob: 'unsafe-eval' 'unsafe-inline' connect.tiktok-minis.com sf-connect.tiktokminis.us;img-src 'self' ${devTrustedDomain} data: blob: *; connect-src 'self' ${devTrustedDomain} data: blob: ; style-src 'self' ${devTrustedDomain} 'unsafe-inline' fonts.googleapis.com data: blob: *; font-src 'self' fonts.gstatic.com blob: data: *; media-src 'self' ${devTrustedDomain} data: blob: *; frame-src 'none'; base-uri 'self'; worker-src 'self' blob: data: ;`);
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
// 如果 index.html 不存在或有异常,CSP 头就不设置
|
|
398
|
+
console.warn(chalk.red('Failed to set CSP header:'), e.message);
|
|
399
|
+
}
|
|
400
|
+
next();
|
|
401
|
+
});
|
|
402
|
+
// 4. 静态资源服务
|
|
403
|
+
app.use(express.static(path.join(process.cwd())));
|
|
404
|
+
// 5. 启动服务并自动打开浏览器
|
|
405
|
+
app.listen(devPort, () => {
|
|
406
|
+
const gameUrl = `http://localhost:${devPort}`;
|
|
407
|
+
const openContextUrl = `http://localhost:${devPort}/open_context.html`;
|
|
408
|
+
let devUrl = `${MINIS_RUNTIME_URL}?minis_url=${gameUrl}&enable_log=1`;
|
|
409
|
+
if (hasOpenContext) {
|
|
410
|
+
devUrl += `&open_context_url=${openContextUrl}`;
|
|
411
|
+
}
|
|
412
|
+
console.log(`you can access ${chalk.green.underline.bold(devUrl)} to debug your game in browser...`);
|
|
413
|
+
try {
|
|
414
|
+
// 自动打开浏览器,跨平台
|
|
415
|
+
openUrl(devUrl);
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
console.warn(chalk.red('Failed to open browser, you can access it manually'), e.message);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
729
422
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
423
|
+
async function buildMinisManifest() {
|
|
424
|
+
try {
|
|
425
|
+
const buildPath = path.join(process.cwd());
|
|
426
|
+
const resourceList = [];
|
|
427
|
+
const allFiles = collectAllFiles(buildPath);
|
|
428
|
+
Object.keys(allFiles)
|
|
429
|
+
.filter(file => !file.endsWith('.map'))
|
|
430
|
+
.forEach(file => {
|
|
431
|
+
const relativeFilePath = allFiles[file];
|
|
432
|
+
const filePathArr = relativeFilePath.split('/').filter(Boolean); // Split filename
|
|
433
|
+
const fileName = filePathArr.pop() || ''; // Get filename
|
|
434
|
+
if (filePathArr.length === 0) {
|
|
435
|
+
resourceList.push({ type: 'file', name: fileName });
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const folder = findOrCreateFolder(filePathArr, resourceList);
|
|
439
|
+
folder.children.push({ type: 'file', name: fileName });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
fs.writeFileSync(path.join(buildPath, MINIS_MANIFEST_FILE_NAME), JSON.stringify({ name: MINIS_MANIFEST_FILE_NAME, resource_list: resourceList }, null, 2));
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
console.error(chalk.red(`Error during debug process: ${error.message}`));
|
|
446
|
+
if (error instanceof Error && error.stack) {
|
|
447
|
+
console.error(chalk.red(`Stack trace: ${error.stack}`));
|
|
448
|
+
}
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function findOrCreateFolder(pathArray, currentFolder) {
|
|
453
|
+
let folder = currentFolder.find(item => item.type === 'folder' && item.name === pathArray[0]);
|
|
454
|
+
if (!folder) {
|
|
455
|
+
folder = { type: 'folder', name: pathArray[0], children: [] };
|
|
456
|
+
currentFolder.push(folder);
|
|
457
|
+
}
|
|
458
|
+
if (pathArray.length > 1) {
|
|
459
|
+
return findOrCreateFolder(pathArray.slice(1), folder.children);
|
|
460
|
+
}
|
|
461
|
+
return folder;
|
|
462
|
+
}
|
|
463
|
+
function collectAllFiles(dir, baseDir = dir) {
|
|
464
|
+
const files = {};
|
|
465
|
+
fs.readdirSync(dir).forEach(file => {
|
|
466
|
+
const filePath = path.join(dir, file);
|
|
467
|
+
const fileStat = fs.statSync(filePath);
|
|
468
|
+
if (fileStat.isDirectory()) {
|
|
469
|
+
// Process subdirectories recursively
|
|
470
|
+
const subFiles = collectAllFiles(filePath, baseDir);
|
|
471
|
+
Object.assign(files, subFiles);
|
|
472
|
+
}
|
|
473
|
+
else if (path.extname(file) !== '' && file) {
|
|
474
|
+
const relativePath = '/' + path.relative(baseDir, filePath).replace(/\\/g, '/');
|
|
475
|
+
files[filePath] = relativePath;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return files;
|
|
763
479
|
}
|
|
764
480
|
|
|
765
|
-
|
|
766
|
-
|
|
481
|
+
const promptModule = inquirer.createPromptModule();
|
|
482
|
+
async function build() {
|
|
483
|
+
let zName = '';
|
|
484
|
+
try {
|
|
485
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
486
|
+
if (!fs.existsSync(configPath)) {
|
|
487
|
+
console.log(chalk.red.bold(`${CONFIG_FILE_NAME} is not exist, please run minis game init first`));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const gameConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
491
|
+
const hasOpenContext = !!gameConfig.openDataContext;
|
|
492
|
+
if (hasOpenContext) {
|
|
493
|
+
generateOpenContextHtml(gameConfig.openDataContext, gameConfig.app_id);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 用户输入打包后的游戏压缩包名称
|
|
497
|
+
*/
|
|
498
|
+
const { zipName: zipNameInput } = await promptModule({
|
|
499
|
+
type: 'input',
|
|
500
|
+
name: 'zipName',
|
|
501
|
+
default: 'game',
|
|
502
|
+
message: 'Please input zip name',
|
|
503
|
+
});
|
|
504
|
+
zName = zipNameInput;
|
|
505
|
+
const startTime = Date.now();
|
|
506
|
+
console.log(chalk.bold.blue('start build your game, it will take a few minutes...'));
|
|
507
|
+
/**
|
|
508
|
+
* 移除掉当前根目录下的文件包
|
|
509
|
+
*/
|
|
510
|
+
const files = fs.readdirSync(process.cwd());
|
|
511
|
+
files.forEach(file => {
|
|
512
|
+
if (file.endsWith('.zip')) {
|
|
513
|
+
fs.unlinkSync(path.join(process.cwd(), file));
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
await buildMinisManifest();
|
|
517
|
+
const archive = archiver('zip', {
|
|
518
|
+
zlib: { level: 9 }, // Sets the compression level.
|
|
519
|
+
});
|
|
520
|
+
archive.on('error', function (err) {
|
|
521
|
+
throw err;
|
|
522
|
+
});
|
|
523
|
+
/**
|
|
524
|
+
* 把当前文件压缩成 game-${dateStr}.zip,并添加在桌面
|
|
525
|
+
*/
|
|
526
|
+
const desktopPath = getDesktopPath();
|
|
527
|
+
const zipPath = path.join(desktopPath, `${zipNameInput}.zip`);
|
|
528
|
+
await archive.pipe(fs.createWriteStream(zipPath));
|
|
529
|
+
await archive.directory(path.resolve(process.cwd()), false);
|
|
530
|
+
await archive.finalize();
|
|
531
|
+
console.log(chalk.yellow.bold(`build ${zipNameInput}.zip success, you can find it in desktop, use time ${Date.now() - startTime} ms`));
|
|
532
|
+
process.exit(0);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
console.log(chalk.red(`auto build ${zName}.zip failed: ${error.message}, you should zip it manually`));
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* 获取当前操作系统的桌面路径,兼容 Windows 和 macOS。
|
|
541
|
+
* @returns {string} 桌面的绝对路径。
|
|
542
|
+
*/
|
|
543
|
+
function getDesktopPath() {
|
|
544
|
+
// 1. 获取用户的主目录,这是所有平台通用的基础
|
|
545
|
+
// - Windows: C:\Users\<username>
|
|
546
|
+
// - macOS/Linux: /Users/<username>
|
|
547
|
+
const homeDir = os.homedir();
|
|
548
|
+
// 2. 定义可能的桌面文件夹名称列表
|
|
549
|
+
// - 'Desktop' 是英文系统的标准名称
|
|
550
|
+
// - '桌面' 是中文系统的标准名称
|
|
551
|
+
// 可以根据需要添加其他语言,例如 'Bureau' (法语), 'Schreibtisch' (德语) 等
|
|
552
|
+
const desktopNames = ['Desktop', '桌面'];
|
|
553
|
+
// 3. 遍历列表,检查哪个路径真实存在
|
|
554
|
+
for (const name of desktopNames) {
|
|
555
|
+
const possiblePath = path.join(homeDir, name);
|
|
556
|
+
// fs.existsSync 会同步检查文件或文件夹是否存在
|
|
557
|
+
if (fs.existsSync(possiblePath)) {
|
|
558
|
+
// 找到一个就立即返回,这是最可靠的桌面路径
|
|
559
|
+
return possiblePath;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// 4. 兜底策略 (Fallback)
|
|
563
|
+
// 如果上面的常见名称都没有找到(例如在一些极简或非标准的系统环境中),
|
|
564
|
+
// 我们就默认返回英文的 'Desktop' 路径。
|
|
565
|
+
// 这是一种安全的默认行为,因为即使文件夹不存在,程序后续创建文件时
|
|
566
|
+
// 也可以选择自动创建这个目录。
|
|
567
|
+
const defaultPath = path.join(homeDir, 'Desktop');
|
|
568
|
+
console.log(`未找到特定桌面文件夹,使用默认路径: ${defaultPath}`);
|
|
569
|
+
return defaultPath;
|
|
570
|
+
}
|
|
767
571
|
|
|
768
572
|
const CONFIG_DIR = path.join(os.homedir(), '.ttmg-cli');
|
|
769
573
|
path.join(CONFIG_DIR, 'config.json');
|
|
@@ -774,9 +578,12 @@ function getClientKey() {
|
|
|
774
578
|
/**
|
|
775
579
|
* 读取 project.config.json 中的 appid/appId
|
|
776
580
|
*/
|
|
777
|
-
const projectConfigPath = path.join(process.cwd(), 'project.config.json');
|
|
778
581
|
let clientKey;
|
|
779
582
|
try {
|
|
583
|
+
const projectConfigPath = path.join(process.cwd(), 'project.config.json');
|
|
584
|
+
if (!fs.existsSync(projectConfigPath)) {
|
|
585
|
+
return '';
|
|
586
|
+
}
|
|
780
587
|
const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf-8'));
|
|
781
588
|
clientKey = projectConfig.appid || projectConfig.appId;
|
|
782
589
|
}
|
|
@@ -784,127 +591,75 @@ function getClientKey() {
|
|
|
784
591
|
console.log('read project.config.json failed', e);
|
|
785
592
|
clientKey = '';
|
|
786
593
|
}
|
|
787
|
-
|
|
788
|
-
return clientKey;
|
|
789
|
-
}
|
|
790
|
-
else {
|
|
791
|
-
console.log(chalk.red.bold('No appid found in project.config.json, you should provide it in project.config.json'));
|
|
792
|
-
process.exit(1);
|
|
793
|
-
}
|
|
794
|
-
// const keys = readClientKeys();
|
|
795
|
-
// if (keys.length > 0) {
|
|
796
|
-
// // 有历史 key,展示选择框
|
|
797
|
-
// const { selectedKey } = await inquirer.prompt([
|
|
798
|
-
// {
|
|
799
|
-
// type: 'list',
|
|
800
|
-
// name: 'selectedKey',
|
|
801
|
-
// message: 'Please select your game id(client_key):',
|
|
802
|
-
// choices: [...keys, new inquirer.Separator(), 'Add new game id'],
|
|
803
|
-
// },
|
|
804
|
-
// ]);
|
|
805
|
-
// if (selectedKey === 'Add new game id') {
|
|
806
|
-
// // 输入新 key
|
|
807
|
-
// const { newKey } = await inquirer.prompt([
|
|
808
|
-
// {
|
|
809
|
-
// type: 'input',
|
|
810
|
-
// name: 'newKey',
|
|
811
|
-
// message: 'Please input your new game id(client_key):',
|
|
812
|
-
// validate: (input: string) => (input ? true : 'Please input game id'),
|
|
813
|
-
// },
|
|
814
|
-
// ]);
|
|
815
|
-
// saveClientKey(newKey);
|
|
816
|
-
// clientKey = newKey;
|
|
817
|
-
// } else {
|
|
818
|
-
// clientKey = selectedKey;
|
|
819
|
-
// }
|
|
820
|
-
// } else {
|
|
821
|
-
// // 没有历史 key,让用户输入
|
|
822
|
-
// const { newKey } = await inquirer.prompt([
|
|
823
|
-
// {
|
|
824
|
-
// type: 'input',
|
|
825
|
-
// name: 'newKey',
|
|
826
|
-
// message: 'Please input your game id(client_key):',
|
|
827
|
-
// validate: (input: string) => (input ? true : 'Please input game id'),
|
|
828
|
-
// },
|
|
829
|
-
// ]);
|
|
830
|
-
// saveClientKey(newKey);
|
|
831
|
-
// clientKey = newKey;
|
|
832
|
-
// }
|
|
833
|
-
// return clientKey;
|
|
594
|
+
return clientKey;
|
|
834
595
|
}
|
|
596
|
+
// const keys = readClientKeys();
|
|
597
|
+
// if (keys.length > 0) {
|
|
598
|
+
// // 有历史 key,展示选择框
|
|
599
|
+
// const { selectedKey } = await inquirer.prompt([
|
|
600
|
+
// {
|
|
601
|
+
// type: 'list',
|
|
602
|
+
// name: 'selectedKey',
|
|
603
|
+
// message: 'Please select your game id(client_key):',
|
|
604
|
+
// choices: [...keys, new inquirer.Separator(), 'Add new game id'],
|
|
605
|
+
// },
|
|
606
|
+
// ]);
|
|
607
|
+
// if (selectedKey === 'Add new game id') {
|
|
608
|
+
// // 输入新 key
|
|
609
|
+
// const { newKey } = await inquirer.prompt([
|
|
610
|
+
// {
|
|
611
|
+
// type: 'input',
|
|
612
|
+
// name: 'newKey',
|
|
613
|
+
// message: 'Please input your new game id(client_key):',
|
|
614
|
+
// validate: (input: string) => (input ? true : 'Please input game id'),
|
|
615
|
+
// },
|
|
616
|
+
// ]);
|
|
617
|
+
// saveClientKey(newKey);
|
|
618
|
+
// clientKey = newKey;
|
|
619
|
+
// } else {
|
|
620
|
+
// clientKey = selectedKey;
|
|
621
|
+
// }
|
|
622
|
+
// } else {
|
|
623
|
+
// // 没有历史 key,让用户输入
|
|
624
|
+
// const { newKey } = await inquirer.prompt([
|
|
625
|
+
// {
|
|
626
|
+
// type: 'input',
|
|
627
|
+
// name: 'newKey',
|
|
628
|
+
// message: 'Please input your game id(client_key):',
|
|
629
|
+
// validate: (input: string) => (input ? true : 'Please input game id'),
|
|
630
|
+
// },
|
|
631
|
+
// ]);
|
|
632
|
+
// saveClientKey(newKey);
|
|
633
|
+
// clientKey = newKey;
|
|
634
|
+
// }
|
|
635
|
+
// return clientKey;
|
|
835
636
|
|
|
836
637
|
function getOutputDir() {
|
|
837
638
|
const clientKey = getClientKey();
|
|
838
639
|
return path.join(os.homedir(), '__TTMG__', clientKey);
|
|
839
640
|
}
|
|
840
641
|
|
|
841
|
-
class Store {
|
|
842
|
-
constructor(initialState) {
|
|
843
|
-
this.listeners = [];
|
|
844
|
-
this.state = initialState;
|
|
845
|
-
}
|
|
846
|
-
// 获取单例实例
|
|
847
|
-
static getInstance(initialState) {
|
|
848
|
-
if (!Store.instance) {
|
|
849
|
-
Store.instance = new Store(initialState);
|
|
850
|
-
}
|
|
851
|
-
return Store.instance;
|
|
852
|
-
}
|
|
853
|
-
// 获取状态
|
|
854
|
-
getState() {
|
|
855
|
-
return this.state;
|
|
856
|
-
}
|
|
857
|
-
// 设置状态
|
|
858
|
-
setState(newState) {
|
|
859
|
-
this.state = { ...this.state, ...newState };
|
|
860
|
-
this.listeners.forEach(listener => listener(this.state));
|
|
861
|
-
}
|
|
862
|
-
// 订阅状态变化
|
|
863
|
-
subscribe(listener) {
|
|
864
|
-
this.listeners.push(listener);
|
|
865
|
-
return () => {
|
|
866
|
-
this.listeners = this.listeners.filter(l => l !== listener);
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
// 重置状态
|
|
870
|
-
reset(newState) {
|
|
871
|
-
this.state = newState;
|
|
872
|
-
this.listeners.forEach(listener => listener(this.state));
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
const store = Store.getInstance({
|
|
876
|
-
clientServerPort: '',
|
|
877
|
-
clientServerHost: '',
|
|
878
|
-
clientKey: '',
|
|
879
|
-
packages: {},
|
|
880
|
-
});
|
|
881
|
-
|
|
882
642
|
const DEV_PORT = 9528;
|
|
883
643
|
const DEV_WS_PORT = 9529;
|
|
884
644
|
const OUTPUT_DIR = path.join(os.homedir(), '__TTMG__');
|
|
885
645
|
|
|
886
|
-
function getSessionId({ clientWsPort }) {
|
|
887
|
-
const config = {
|
|
888
|
-
ws_port: DEV_WS_PORT,
|
|
889
|
-
nodeWsPort: DEV_WS_PORT,
|
|
890
|
-
// http_port: DEV_PORT,
|
|
891
|
-
clientWsPort,
|
|
892
|
-
};
|
|
893
|
-
return btoa(JSON.stringify(config));
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* 把我的调试相关的数据,生成 base64 字符串
|
|
898
|
-
*/
|
|
899
646
|
let server;
|
|
900
647
|
async function createServer() {
|
|
901
648
|
if (server) {
|
|
902
649
|
closeServer();
|
|
903
650
|
}
|
|
651
|
+
const publicPath = path.join(__dirname, 'public');
|
|
904
652
|
const app = express();
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
653
|
+
/**
|
|
654
|
+
* 支持跨域请求
|
|
655
|
+
*/
|
|
656
|
+
app.use((req, res, next) => {
|
|
657
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
658
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
659
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
660
|
+
next();
|
|
661
|
+
});
|
|
662
|
+
const port = DEV_PORT;
|
|
908
663
|
app.use(express.json());
|
|
909
664
|
// 解析 FormData body
|
|
910
665
|
app.use(express.urlencoded({ extended: true }));
|
|
@@ -912,6 +667,10 @@ async function createServer() {
|
|
|
912
667
|
* 支持文件访问
|
|
913
668
|
*/
|
|
914
669
|
const outputDir = getOutputDir();
|
|
670
|
+
/**
|
|
671
|
+
*
|
|
672
|
+
*/
|
|
673
|
+
app.use(express.static(publicPath));
|
|
915
674
|
/**
|
|
916
675
|
* 支持静态资源
|
|
917
676
|
*/
|
|
@@ -919,42 +678,11 @@ async function createServer() {
|
|
|
919
678
|
app.get('/game/qrcode', async (req, res) => {
|
|
920
679
|
res.sendFile(path.join(OUTPUT_DIR, `dev-qrcode.png`));
|
|
921
680
|
});
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
*/
|
|
925
|
-
/**
|
|
926
|
-
* TODO: 提供的接口供客户端进行调用,告诉 NodeServer 客户端的 host 和 port
|
|
927
|
-
*/
|
|
928
|
-
app.post('/game/env', async (req, res) => {
|
|
929
|
-
// 获取请求体
|
|
930
|
-
const body = req.body;
|
|
931
|
-
const { host, port, wsPort } = body;
|
|
932
|
-
store.setState({
|
|
933
|
-
clientServerPort: port,
|
|
934
|
-
clientServerHost: host,
|
|
935
|
-
});
|
|
936
|
-
// 响应内容(可以根据实际业务返回需要的数据)
|
|
937
|
-
const devUrl = `http://${host}:${port}?session=${getSessionId({ clientWsPort: wsPort })}`;
|
|
938
|
-
openUrl(devUrl);
|
|
939
|
-
console.log(chalk.bold.yellow(`Game debug is ready! Visit ${devUrl} in your browser.`));
|
|
940
|
-
res.json({
|
|
941
|
-
code: 0,
|
|
942
|
-
msg: 'ok',
|
|
943
|
-
data: {
|
|
944
|
-
devUrl,
|
|
945
|
-
},
|
|
946
|
-
});
|
|
681
|
+
app.get('/game/config', async (req, res) => {
|
|
682
|
+
res.send({ nodeWsPort: DEV_WS_PORT });
|
|
947
683
|
});
|
|
948
|
-
app.
|
|
949
|
-
|
|
950
|
-
res.json({
|
|
951
|
-
code: 0,
|
|
952
|
-
msg: 'ok',
|
|
953
|
-
filename: req.file.filename, // multer生成的临时文件名
|
|
954
|
-
originalname: req.file.originalname, // 上传时的原始文件名
|
|
955
|
-
size: req.file.size,
|
|
956
|
-
path: req.file.path,
|
|
957
|
-
});
|
|
684
|
+
app.get('*', (req, res) => {
|
|
685
|
+
res.sendFile(path.join(publicPath, 'index.html'));
|
|
958
686
|
});
|
|
959
687
|
// 启动服务
|
|
960
688
|
server = app.listen(port, () => {
|
|
@@ -973,11 +701,54 @@ function closeServer() {
|
|
|
973
701
|
});
|
|
974
702
|
}
|
|
975
703
|
|
|
704
|
+
class Store {
|
|
705
|
+
constructor(initialState) {
|
|
706
|
+
this.listeners = [];
|
|
707
|
+
this.state = initialState;
|
|
708
|
+
}
|
|
709
|
+
// 获取单例实例
|
|
710
|
+
static getInstance(initialState) {
|
|
711
|
+
if (!Store.instance) {
|
|
712
|
+
Store.instance = new Store(initialState);
|
|
713
|
+
}
|
|
714
|
+
return Store.instance;
|
|
715
|
+
}
|
|
716
|
+
// 获取状态
|
|
717
|
+
getState() {
|
|
718
|
+
return this.state;
|
|
719
|
+
}
|
|
720
|
+
// 设置状态
|
|
721
|
+
setState(newState) {
|
|
722
|
+
this.state = { ...this.state, ...newState };
|
|
723
|
+
this.listeners.forEach(listener => listener(this.state));
|
|
724
|
+
}
|
|
725
|
+
// 订阅状态变化
|
|
726
|
+
subscribe(listener) {
|
|
727
|
+
this.listeners.push(listener);
|
|
728
|
+
return () => {
|
|
729
|
+
this.listeners = this.listeners.filter(l => l !== listener);
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
// 重置状态
|
|
733
|
+
reset(newState) {
|
|
734
|
+
this.state = newState;
|
|
735
|
+
this.listeners.forEach(listener => listener(this.state));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const store = Store.getInstance({
|
|
739
|
+
clientHost: '',
|
|
740
|
+
clientHttpPort: '',
|
|
741
|
+
clientWsPort: '',
|
|
742
|
+
clientWsHost: '',
|
|
743
|
+
clientKey: '',
|
|
744
|
+
packages: {},
|
|
745
|
+
});
|
|
746
|
+
|
|
976
747
|
async function uploadGame(callback) {
|
|
977
748
|
const outputDir = getOutputDir();
|
|
978
749
|
callback({
|
|
979
750
|
status: 'start',
|
|
980
|
-
percent:
|
|
751
|
+
percent: 0,
|
|
981
752
|
});
|
|
982
753
|
console.log(chalk.yellow.bold('Start compress game resource'));
|
|
983
754
|
const zipPath = path.join(os.homedir(), '__TTMG__', 'upload.zip');
|
|
@@ -985,30 +756,59 @@ async function uploadGame(callback) {
|
|
|
985
756
|
console.log(chalk.green.bold('Compress game package resource success \n'));
|
|
986
757
|
await uploadZip(zipPath, callback);
|
|
987
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* 复制源目录内容到临时目录,然后根据 glob 模式过滤并压缩文件。
|
|
761
|
+
* 原始目录保持不变。
|
|
762
|
+
*
|
|
763
|
+
* @param sourceDir - 要压缩的源文件夹路径。
|
|
764
|
+
* @param outPath - 输出的 zip 文件路径。
|
|
765
|
+
*/
|
|
988
766
|
async function zipDirectory(sourceDir, outPath) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
files.forEach(file => {
|
|
1004
|
-
archive.file(path.join(sourceDir, file), { name: file });
|
|
767
|
+
// 1. 创建一个唯一的临时目录
|
|
768
|
+
// fsp 现在是 fs.promises 的别名
|
|
769
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'zip-temp-'));
|
|
770
|
+
try {
|
|
771
|
+
// 2. 将源目录的所有内容复制到临时目录
|
|
772
|
+
await fs.promises.cp(sourceDir, tempDir, { recursive: true });
|
|
773
|
+
// 3. 对临时目录进行压缩
|
|
774
|
+
const output = fs.createWriteStream(outPath);
|
|
775
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
776
|
+
// 使用 Promise 包装流操作
|
|
777
|
+
const archivePromise = new Promise((resolve, reject) => {
|
|
778
|
+
output.on('close', () => {
|
|
779
|
+
console.log(`压缩完成,总大小: ${archive.pointer()} bytes`);
|
|
780
|
+
resolve();
|
|
1005
781
|
});
|
|
1006
|
-
archive.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
782
|
+
archive.on('warning', err => console.warn('Archiver warning:', err));
|
|
783
|
+
output.on('error', err => reject(err));
|
|
784
|
+
archive.on('error', err => reject(err));
|
|
785
|
+
});
|
|
786
|
+
archive.pipe(output);
|
|
787
|
+
// 4. 使用 glob 在临时目录中查找文件,并应用过滤规则
|
|
788
|
+
const files = await glob__namespace.glob('**/*', {
|
|
789
|
+
cwd: tempDir,
|
|
790
|
+
nodir: true,
|
|
791
|
+
ignore: '**/*.js.map', // 过滤规则
|
|
792
|
+
});
|
|
793
|
+
// 5. 将过滤后的文件逐个添加到压缩包
|
|
794
|
+
for (const file of files) {
|
|
795
|
+
const filePath = path.join(tempDir, file);
|
|
796
|
+
archive.file(filePath, { name: file });
|
|
1010
797
|
}
|
|
1011
|
-
|
|
798
|
+
// 6. 完成压缩
|
|
799
|
+
await archive.finalize();
|
|
800
|
+
// 等待文件流关闭
|
|
801
|
+
await archivePromise;
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
console.error('压缩过程中发生错误:', err);
|
|
805
|
+
throw err;
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
// 7. 无论成功还是失败,都清理临时目录
|
|
809
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
810
|
+
console.log(`临时目录 ${tempDir} 已清理。`);
|
|
811
|
+
}
|
|
1012
812
|
}
|
|
1013
813
|
async function uploadZip(zipPath, callback) {
|
|
1014
814
|
const form = new FormData();
|
|
@@ -1019,8 +819,8 @@ async function uploadZip(zipPath, callback) {
|
|
|
1019
819
|
// 帮我计算下文件大小,变成 MB 为单位
|
|
1020
820
|
const fileSize = fs.statSync(zipPath).size / 1024 / 1024;
|
|
1021
821
|
console.log(chalk.yellow.bold(`Start upload resource to client, size: ${fileSize.toFixed(2)} MB`));
|
|
1022
|
-
const {
|
|
1023
|
-
const url = `http://${
|
|
822
|
+
const { clientHttpPort, clientHost } = store.getState();
|
|
823
|
+
const url = `http://${clientHost}:${clientHttpPort}/game/upload`;
|
|
1024
824
|
try {
|
|
1025
825
|
// 1. 创建请求流
|
|
1026
826
|
const stream = got.stream.post(url, {
|
|
@@ -1028,13 +828,13 @@ async function uploadZip(zipPath, callback) {
|
|
|
1028
828
|
});
|
|
1029
829
|
// 2. 监听上传进度 (这个回调是并行的,不影响封装)
|
|
1030
830
|
stream.on('uploadProgress', progress => {
|
|
1031
|
-
const percent =
|
|
831
|
+
const percent = progress.percent;
|
|
1032
832
|
// const transferred = progress.transferred;
|
|
1033
833
|
// const total = progress.total;
|
|
1034
834
|
process.stdout.write(`\r${chalk.cyan('Uploading progress: ')}${chalk.green(percent + '%')}`);
|
|
1035
835
|
callback({
|
|
1036
836
|
status: 'process',
|
|
1037
|
-
percent
|
|
837
|
+
percent,
|
|
1038
838
|
});
|
|
1039
839
|
});
|
|
1040
840
|
// 3. 【核心封装】将流的处理过程包装在 Promise 中
|
|
@@ -1053,7 +853,7 @@ async function uploadZip(zipPath, callback) {
|
|
|
1053
853
|
// 将完整的响应对象 resolve 出去
|
|
1054
854
|
callback({
|
|
1055
855
|
status: 'success',
|
|
1056
|
-
percent:
|
|
856
|
+
percent: 1,
|
|
1057
857
|
});
|
|
1058
858
|
resolve({
|
|
1059
859
|
statusCode: 200,
|
|
@@ -1065,7 +865,7 @@ async function uploadZip(zipPath, callback) {
|
|
|
1065
865
|
reject(err);
|
|
1066
866
|
callback({
|
|
1067
867
|
status: 'error',
|
|
1068
|
-
percent:
|
|
868
|
+
percent: 0,
|
|
1069
869
|
msg: err.message,
|
|
1070
870
|
});
|
|
1071
871
|
});
|
|
@@ -1073,13 +873,12 @@ async function uploadZip(zipPath, callback) {
|
|
|
1073
873
|
// 4. 当 await 完成后,说明流已成功结束,可以安全地执行后续操作
|
|
1074
874
|
process.stdout.write('\n'); // 换行,保持终端整洁
|
|
1075
875
|
console.log(chalk.green.bold('✔ Upload completed successfully!'));
|
|
1076
|
-
fs.unlinkSync(zipPath);
|
|
1077
876
|
return response;
|
|
1078
877
|
}
|
|
1079
878
|
catch (err) {
|
|
1080
879
|
callback({
|
|
1081
880
|
status: 'error',
|
|
1082
|
-
percent:
|
|
881
|
+
percent: 0,
|
|
1083
882
|
msg: err?.message,
|
|
1084
883
|
});
|
|
1085
884
|
process.stdout.write('\n');
|
|
@@ -1099,16 +898,26 @@ class WsServer {
|
|
|
1099
898
|
constructor() {
|
|
1100
899
|
this.ws = new WebSocket.Server({ port: DEV_WS_PORT });
|
|
1101
900
|
this.ws.on('connection', ws => {
|
|
901
|
+
const { clientHttpPort, clientHost, clientWsPort } = store.getState();
|
|
902
|
+
if (clientHost) {
|
|
903
|
+
this.send({
|
|
904
|
+
method: 'clientDebugInfo',
|
|
905
|
+
payload: {
|
|
906
|
+
clientHttpPort,
|
|
907
|
+
clientHost,
|
|
908
|
+
clientWsPort,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
1102
912
|
ws.on('message', message => {
|
|
1103
913
|
/** 客户端发送的消息 */
|
|
1104
914
|
const clientMessage = JSON.parse(message.toString());
|
|
1105
|
-
console.log('Client Message', clientMessage);
|
|
1106
915
|
const from = clientMessage.from;
|
|
1107
916
|
if (from === 'browser') {
|
|
917
|
+
console.log(chalk.yellow.bold('Browser message'), clientMessage);
|
|
1108
918
|
const method = clientMessage.method;
|
|
1109
919
|
switch (method) {
|
|
1110
920
|
case 'startUpload':
|
|
1111
|
-
console.log('startUpload');
|
|
1112
921
|
this.sendUploadStatus('start');
|
|
1113
922
|
uploadGame(({ status, percent, msg }) => {
|
|
1114
923
|
if (status === 'process') {
|
|
@@ -1147,26 +956,67 @@ class WsServer {
|
|
|
1147
956
|
}
|
|
1148
957
|
}
|
|
1149
958
|
else {
|
|
959
|
+
console.log(chalk.green.bold('Client message'), clientMessage);
|
|
1150
960
|
const method = clientMessage.method;
|
|
1151
961
|
switch (method) {
|
|
962
|
+
/**
|
|
963
|
+
* 客户端完成扫码成功,返回客户端的 host 和 port
|
|
964
|
+
*/
|
|
965
|
+
case 'startScanQRcode': {
|
|
966
|
+
const payload = clientMessage.payload;
|
|
967
|
+
console.log('startQRcode', payload);
|
|
968
|
+
this.send({
|
|
969
|
+
method: 'startScanQRcode',
|
|
970
|
+
});
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case 'scanQRCodeResult': {
|
|
974
|
+
const payload = clientMessage.payload || {};
|
|
975
|
+
const { host, port, wsPort, errMsg, isSuccess } = payload;
|
|
976
|
+
if (isSuccess) {
|
|
977
|
+
store.setState({
|
|
978
|
+
clientHttpPort: port,
|
|
979
|
+
clientHost: host,
|
|
980
|
+
clientWsPort: wsPort,
|
|
981
|
+
});
|
|
982
|
+
this.send({
|
|
983
|
+
method: 'scanQRCodeSuccess',
|
|
984
|
+
payload: {
|
|
985
|
+
clientHttpPort: port,
|
|
986
|
+
clientHost: host,
|
|
987
|
+
clientWsPort: wsPort,
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
console.log('scanQRcodeSuccess');
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
this.send({
|
|
994
|
+
method: 'scanQRCodeFailed',
|
|
995
|
+
payload: {
|
|
996
|
+
errMsg,
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
// 待废弃
|
|
1152
1003
|
case 'shareDevParams':
|
|
1153
|
-
console.log('shareDevParams', clientMessage);
|
|
1154
1004
|
const payload = clientMessage.payload;
|
|
1155
|
-
console.log('shareDevParams', payload);
|
|
1156
1005
|
const { host, port, wsPort } = payload;
|
|
1157
1006
|
store.setState({
|
|
1158
|
-
|
|
1159
|
-
|
|
1007
|
+
clientHttpPort: port,
|
|
1008
|
+
clientHost: host,
|
|
1009
|
+
clientWsPort: wsPort,
|
|
1010
|
+
clientWsHost: host,
|
|
1011
|
+
});
|
|
1012
|
+
this.send({
|
|
1013
|
+
method: 'scanQRCodeSuccess',
|
|
1014
|
+
payload: {
|
|
1015
|
+
clientHttpPort: port,
|
|
1016
|
+
clientHost: host,
|
|
1017
|
+
clientWsPort: wsPort,
|
|
1018
|
+
},
|
|
1160
1019
|
});
|
|
1161
|
-
// 响应内容(可以根据实际业务返回需要的数据)
|
|
1162
|
-
const devUrl = `http://${host}:${port}?session=${getSessionId({ clientWsPort: wsPort })}`;
|
|
1163
|
-
openUrl(devUrl);
|
|
1164
|
-
console.log(chalk.bold.yellow(`Game debug is ready! Visit ${devUrl} in your browser.`));
|
|
1165
|
-
break;
|
|
1166
|
-
// 鉴权失败
|
|
1167
|
-
case 'checkPermissionFailed':
|
|
1168
|
-
// 提醒使用客户端先完成测试用户授权,再重新扫描二维码开启调试服务
|
|
1169
|
-
console.log(chalk.red.bold('Check permission failed! Please authorize in client first.'));
|
|
1170
1020
|
break;
|
|
1171
1021
|
}
|
|
1172
1022
|
}
|
|
@@ -1184,6 +1034,11 @@ class WsServer {
|
|
|
1184
1034
|
}
|
|
1185
1035
|
});
|
|
1186
1036
|
}
|
|
1037
|
+
sendResourceChange() {
|
|
1038
|
+
this.send({
|
|
1039
|
+
method: 'resourceChange',
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1187
1042
|
close() {
|
|
1188
1043
|
this.ws.close();
|
|
1189
1044
|
}
|
|
@@ -1197,13 +1052,22 @@ class WsServer {
|
|
|
1197
1052
|
}
|
|
1198
1053
|
const wsServer = new WsServer();
|
|
1199
1054
|
|
|
1200
|
-
async function prepareResource() {
|
|
1201
|
-
console.log(chalk.yellow.bold('Start compile game for debug'));
|
|
1055
|
+
async function prepareResource(context) {
|
|
1202
1056
|
const entryDir = process.cwd();
|
|
1203
1057
|
const outputDir = getOutputDir();
|
|
1058
|
+
const clientKey = getClientKey();
|
|
1059
|
+
if (!clientKey) {
|
|
1060
|
+
if (context.mode !== 'watch') {
|
|
1061
|
+
console.log(chalk.red.bold('No appid found in project.config.json, you should provide it in project.config.json'));
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1204
1066
|
if (!fs.existsSync(outputDir)) {
|
|
1205
1067
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
1206
1068
|
}
|
|
1069
|
+
const tip = context?.mode === 'watch' ? chalk.yellow('game resource change, restart to upload') : chalk.yellow.bold('Start compile game for debug');
|
|
1070
|
+
console.log(tip);
|
|
1207
1071
|
const { isSuccess, errorMsg, packages } = await ttmgPack.debugPkgs({
|
|
1208
1072
|
entry: entryDir,
|
|
1209
1073
|
output: outputDir,
|
|
@@ -1215,6 +1079,7 @@ async function prepareResource() {
|
|
|
1215
1079
|
enableLog: false,
|
|
1216
1080
|
},
|
|
1217
1081
|
build: {
|
|
1082
|
+
enableOdr: false,
|
|
1218
1083
|
pkgSizeLimit: 30 * 1024 * 1024,
|
|
1219
1084
|
mainPkgSizeLimit: 4 * 1024 * 1024,
|
|
1220
1085
|
},
|
|
@@ -1229,6 +1094,11 @@ async function prepareResource() {
|
|
|
1229
1094
|
packages,
|
|
1230
1095
|
});
|
|
1231
1096
|
console.log(chalk.green.bold('Compile game package success \n'));
|
|
1097
|
+
return {
|
|
1098
|
+
isSuccess,
|
|
1099
|
+
errorMsg,
|
|
1100
|
+
packages,
|
|
1101
|
+
};
|
|
1232
1102
|
}
|
|
1233
1103
|
}
|
|
1234
1104
|
|
|
@@ -1241,11 +1111,10 @@ async function watchChange() {
|
|
|
1241
1111
|
clearTimeout(debounceTimer);
|
|
1242
1112
|
// 重新设置定时器
|
|
1243
1113
|
debounceTimer = setTimeout(async () => {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
wsServer.send({
|
|
1247
|
-
method: 'resourceChange',
|
|
1114
|
+
await prepareResource({
|
|
1115
|
+
mode: 'watch',
|
|
1248
1116
|
});
|
|
1117
|
+
wsServer.sendResourceChange();
|
|
1249
1118
|
// TODO:只做文件预准备,但不主动上传
|
|
1250
1119
|
// uploadGame()
|
|
1251
1120
|
// .then((res) => {
|
|
@@ -1285,7 +1154,7 @@ async function showSchema() {
|
|
|
1285
1154
|
margin: 1,
|
|
1286
1155
|
});
|
|
1287
1156
|
// 4. 构建可通过静态服务器访问的 URL
|
|
1288
|
-
const qrCodeUrl = `http://localhost:${DEV_PORT}
|
|
1157
|
+
const qrCodeUrl = `http://localhost:${DEV_PORT}?enableLog=1`;
|
|
1289
1158
|
// 5. 打印更新后的提示信息
|
|
1290
1159
|
console.log(chalk.green.bold('Tips:'));
|
|
1291
1160
|
console.log(` 1. ${chalk.yellow.bold('Open the link below in your browser to see the QR code, then scan it.')}`);
|
|
@@ -1321,7 +1190,7 @@ async function dev() {
|
|
|
1321
1190
|
await watchChange();
|
|
1322
1191
|
}
|
|
1323
1192
|
|
|
1324
|
-
var version = "0.1.3
|
|
1193
|
+
var version = "0.1.3";
|
|
1325
1194
|
var pkg = {
|
|
1326
1195
|
version: version};
|
|
1327
1196
|
|
|
@@ -1329,6 +1198,7 @@ const program = new commander.Command();
|
|
|
1329
1198
|
(async () => {
|
|
1330
1199
|
try {
|
|
1331
1200
|
await checkUpdate();
|
|
1201
|
+
// eslint-disable-next-line no-empty
|
|
1332
1202
|
}
|
|
1333
1203
|
catch (err) { }
|
|
1334
1204
|
})();
|
|
@@ -1345,7 +1215,7 @@ program
|
|
|
1345
1215
|
.action(() => {
|
|
1346
1216
|
const options = program.opts(); // 获取 options
|
|
1347
1217
|
if (options.h5) {
|
|
1348
|
-
|
|
1218
|
+
init();
|
|
1349
1219
|
}
|
|
1350
1220
|
else {
|
|
1351
1221
|
console.log('Native Mini Game initialize');
|
|
@@ -1361,7 +1231,7 @@ program
|
|
|
1361
1231
|
.action(() => {
|
|
1362
1232
|
const options = program.opts();
|
|
1363
1233
|
if (options.h5) {
|
|
1364
|
-
|
|
1234
|
+
dev$1();
|
|
1365
1235
|
}
|
|
1366
1236
|
else {
|
|
1367
1237
|
dev();
|
|
@@ -1377,7 +1247,7 @@ program
|
|
|
1377
1247
|
.action(() => {
|
|
1378
1248
|
const options = program.opts(); // 获取 options
|
|
1379
1249
|
if (options.h5) {
|
|
1380
|
-
|
|
1250
|
+
build();
|
|
1381
1251
|
}
|
|
1382
1252
|
else {
|
|
1383
1253
|
console.log('Native Mini Game bundle');
|
|
@@ -1390,3 +1260,4 @@ program
|
|
|
1390
1260
|
console.log('will support soon');
|
|
1391
1261
|
});
|
|
1392
1262
|
program.parse(process.argv);
|
|
1263
|
+
//# sourceMappingURL=index.js.map
|