@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/dist/index.js CHANGED
@@ -2,22 +2,22 @@
2
2
  'use strict';
3
3
 
4
4
  var commander = require('commander');
5
- var require$$0 = require('inquirer');
5
+ var inquirer = require('inquirer');
6
6
  var fs = require('fs');
7
- var require$$1 = require('jsdom');
8
- var require$$2 = require('prettier');
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 require$$4 = require('cheerio');
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 require$$6 = require('crypto');
19
- var require$$5$1 = require('archiver');
20
- var multer = require('multer');
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
- 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);
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
- function getDefaultExportFromCjs (x) {
48
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
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
- MINIS_MANIFEST_FILE_NAME: 'minis.manifest.json',
96
- MINIS_RUNTIME_URL: 'https://www.tiktok.com/minigames/runtime',
97
- };
98
- return config;
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
- var inject;
102
- var hasRequiredInject;
103
-
104
- function requireInject () {
105
- if (hasRequiredInject) return inject;
106
- hasRequiredInject = 1;
107
- const fs$1 = fs;
108
- const jsdom = require$$1;
109
- const prettier = require$$2;
110
- const chalk$1 = chalk;
111
- const { JSDOM } = jsdom;
112
- const {
113
- CONFIG_FILE_NAME,
114
- SDK_URL,
115
- VCONSOLE_URL,
116
- VCONSOLE_INIT,
117
- } = requireConfig();
118
- const CONFIG_PATH = `${process.cwd()}/${CONFIG_FILE_NAME}`;
119
- const INDEX_HTML_PATH = `${process.cwd()}/index.html`;
120
-
121
- function isSandbox(clientKey) {
122
- /**
123
- * sb 开头的 clientKey 都是 sandbox 环境
124
- */
125
- return clientKey.startsWith('sb');
126
- }
127
- // 判断是否是 TTMinis.game.init 的初始化脚本
128
- function isTTMinisInitScript(script, clientKey) {
129
- if (!script.innerHTML) return false;
130
- return (
131
- script.innerHTML.includes('TTMinis.game.init') &&
132
- script.innerHTML.includes(clientKey)
133
- );
134
- }
135
-
136
- async function injectScripts({ config, clientKey }) {
137
- // 1. 检查 config 文件是否已存在
138
- fs$1.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
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
- head.insertBefore(initScript, sdkScript.nextSibling);
183
- lastInitScript = initScript;
184
- } else {
185
- // 已经有 SDK,查找 TTMinis.game.init 脚本
186
- const headScripts = Array.from(head.querySelectorAll('script'));
187
- lastInitScript = headScripts.find(script =>
188
- isTTMinisInitScript(script, clientKey),
189
- );
190
- if (lastInitScript) {
191
- console.log('JS SDK 已接入,跳过 SDK 相关脚本注入');
192
- } else {
193
- // 没有 TTMinis.game.init,则查找最后一个 SDK 脚本
194
- const sdkScripts = headScripts.filter(
195
- script => script.src && script.src.includes(SDK_URL),
196
- );
197
- if (sdkScripts.length > 0) {
198
- lastInitScript = sdkScripts[sdkScripts.length - 1];
199
- }
200
- }
201
- }
202
-
203
- /**
204
- * 只有 Sandbox 环境才需要注入 vConsole
205
- */
206
- if (isSandbox(clientKey)) {
207
- console.log('Sandbox 环境,跳过 vConsole 相关脚本注入');
208
- // 8. 插入 vConsole 相关脚本(如果需要)
209
- if (!hasVConsole) {
210
- // vConsole 相关脚本的插入点
211
- let insertAfterNode = lastInitScript;
212
- if (insertAfterNode) {
213
- insertAfterNode = insertAfterNode.nextSibling;
214
- } else {
215
- insertAfterNode = head.firstChild;
216
- }
217
-
218
- // vConsole 源码
219
- const vconsoleSourceScript = document.createElement('script');
220
- vconsoleSourceScript.src = VCONSOLE_URL;
221
- // vConsole 初始化
222
- const vconsoleInitScript = document.createElement('script');
223
- vconsoleInitScript.innerHTML = VCONSOLE_INIT;
224
-
225
- head.insertBefore(vconsoleSourceScript, insertAfterNode);
226
- head.insertBefore(vconsoleInitScript, insertAfterNode);
227
- }
228
- }
229
-
230
- // 9. 格式化并写回 index.html
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
- var init;
247
- var hasRequiredInit;
248
-
249
- function requireInit () {
250
- if (hasRequiredInit) return init;
251
- hasRequiredInit = 1;
252
- const inquirer = require$$0;
253
- const inject = requireInject();
254
-
255
- init = function init() {
256
- const promptModule = inquirer.createPromptModule();
257
- promptModule([
258
- {
259
- type: 'input',
260
- name: 'clientKey',
261
- message: 'Please input client key',
262
- },
263
- {
264
- type: 'input',
265
- name: 'devPort',
266
- message: 'Please input dev port',
267
- default: 9527,
268
- },
269
- ])
270
- .then(async answers => {
271
- const { clientKey, devPort } = answers;
272
- const config = {
273
- _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.`,
274
- orientation: 'VERTICAL',
275
- dev: {
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
- var libs = /*#__PURE__*/Object.freeze({
418
- __proto__: null,
419
- centerQRCode: centerQRCode,
420
- checkUpdate: checkUpdate,
421
- getDesktopPath: getDesktopPath,
422
- getLocalIP: getLocalIP,
423
- openUrl: openUrl
424
- });
425
-
426
- var require$$5 = /*@__PURE__*/getAugmentedNamespace(libs);
427
-
428
- var dev$1;
429
- var hasRequiredDev;
430
-
431
- function requireDev () {
432
- if (hasRequiredDev) return dev$1;
433
- hasRequiredDev = 1;
434
- const express$1 = express;
435
- const path$1 = path;
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
- var devExports = requireDev();
561
- var index$1 = /*@__PURE__*/getDefaultExportFromCjs(devExports);
562
-
563
- var manifest;
564
- var hasRequiredManifest;
565
-
566
- function requireManifest () {
567
- if (hasRequiredManifest) return manifest;
568
- hasRequiredManifest = 1;
569
- const chalk$1 = chalk;
570
- const fs$1 = fs;
571
- const path$1 = path;
572
- const { MINIS_MANIFEST_FILE_NAME } = requireConfig();
573
- async function buildMinisManifest() {
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
- var bundle;
649
- var hasRequiredBundle;
650
-
651
- function requireBundle () {
652
- if (hasRequiredBundle) return bundle;
653
- hasRequiredBundle = 1;
654
- const fs$1 = fs;
655
- const os$1 = os;
656
- const path$1 = path;
657
- const inquirer = require$$0;
658
- const chalk$1 = chalk;
659
- const archiver = require$$5$1;
660
- const promptModule = inquirer.createPromptModule();
661
- const { buildMinisManifest } = requireManifest();
662
-
663
- bundle = async function build() {
664
- try {
665
- /**
666
- * 用户输入打包后的游戏压缩包名称
667
- */
668
- const { zipName: zipNameInput } = await promptModule({
669
- type: 'input',
670
- name: 'zipName',
671
- default: 'game',
672
- message: 'Please input zip name',
673
- });
674
-
675
- const startTime = Date.now();
676
- console.log(
677
- chalk$1.bold.blue('start build your game, it will take a few minutes...'),
678
- );
679
-
680
- /**
681
- * 移除掉当前根目录下的文件包
682
- */
683
-
684
- const files = fs$1.readdirSync(process.cwd());
685
- files.forEach(file => {
686
- if (file.endsWith('.zip')) {
687
- fs$1.unlinkSync(path$1.join(process.cwd(), file));
688
- }
689
- });
690
-
691
- await buildMinisManifest();
692
- const archive = archiver('zip', {
693
- zlib: { level: 9 }, // Sets the compression level.
694
- });
695
-
696
- archive.on('error', function (err) {
697
- throw err;
698
- });
699
-
700
- /**
701
- * 把当前文件压缩成 game-${dateStr}.zip,并添加在桌面
702
- */
703
- const desktopPath = getDesktopPath();
704
- const zipPath = path$1.join(desktopPath, `${zipNameInput}.zip`);
705
-
706
- await archive.pipe(fs$1.createWriteStream(zipPath));
707
-
708
- await archive.directory(path$1.resolve(process.cwd()), false);
709
-
710
- await archive.finalize();
711
- console.log(
712
- chalk$1.yellow.bold(
713
- `build ${zipNameInput}.zip success, you can find it in desktop, use time ${
714
- Date.now() - startTime
715
- } ms`,
716
- ),
717
- );
718
-
719
- process.exit(0);
720
- } catch (error) {
721
- console.log(
722
- chalk$1.red(
723
- `auto build ${zipNameInput}.zip failed: ${error.message}, you should zip it manually`,
724
- ),
725
- );
726
- process.exit(1);
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
- * 获取当前操作系统的桌面路径,兼容 Windows 和 macOS。
732
- * @returns {string} 桌面的绝对路径。
733
- */
734
- function getDesktopPath() {
735
- // 1. 获取用户的主目录,这是所有平台通用的基础
736
- // - Windows: C:\Users\<username>
737
- // - macOS/Linux: /Users/<username>
738
- const homeDir = os$1.homedir();
739
- // 2. 定义可能的桌面文件夹名称列表
740
- // - 'Desktop' 是英文系统的标准名称
741
- // - '桌面' 是中文系统的标准名称
742
- // 可以根据需要添加其他语言,例如 'Bureau' (法语), 'Schreibtisch' (德语)
743
- const desktopNames = ['Desktop', '桌面'];
744
- // 3. 遍历列表,检查哪个路径真实存在
745
- for (const name of desktopNames) {
746
- const possiblePath = path$1.join(homeDir, name);
747
- // fs.existsSync 会同步检查文件或文件夹是否存在
748
- if (fs$1.existsSync(possiblePath)) {
749
- // 找到一个就立即返回,这是最可靠的桌面路径
750
- return possiblePath;
751
- }
752
- }
753
- // 4. 兜底策略 (Fallback)
754
- // 如果上面的常见名称都没有找到(例如在一些极简或非标准的系统环境中),
755
- // 我们就默认返回英文的 'Desktop' 路径。
756
- // 这是一种安全的默认行为,因为即使文件夹不存在,程序后续创建文件时
757
- // 也可以选择自动创建这个目录。
758
- const defaultPath = path$1.join(homeDir, 'Desktop');
759
- console.log(`未找到特定桌面文件夹,使用默认路径: ${defaultPath}`);
760
- return defaultPath;
761
- }
762
- return bundle;
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
- var bundleExports = requireBundle();
766
- var index = /*@__PURE__*/getDefaultExportFromCjs(bundleExports);
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
- if (clientKey) {
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
- const port = DEV_PORT; // 你可以自定义端口
906
- const upload = multer({ dest: os.tmpdir() }); // 上传到 uploads 目录
907
- // 解析 JSON body
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.post('/game/upload', upload.single('file'), async (req, res) => {
949
- // 文件信息在 req.file
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: '0%',
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
- const output = fs.createWriteStream(outPath);
990
- const archive = require$$5$1('zip', { zlib: { level: 9 } });
991
- return new Promise(async (resolve, reject) => {
992
- output.on('close', () => resolve());
993
- output.on('error', err => reject(err));
994
- archive.on('error', err => reject(err));
995
- archive.pipe(output);
996
- try {
997
- // 注意这里用 await glob.glob
998
- const files = await glob__namespace.glob('**/*', {
999
- cwd: sourceDir,
1000
- nodir: true,
1001
- ignore: '**/*.js.map',
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.finalize();
1007
- }
1008
- catch (err) {
1009
- reject(err);
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 { clientServerHost, clientServerPort } = store.getState();
1023
- const url = `http://${clientServerHost}:${clientServerPort}/game/upload`;
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 = (progress.percent * 100).toFixed(1);
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: `${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: '100%',
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
- clientServerPort: port,
1159
- clientServerHost: host,
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
- console.log(chalk.yellow('game resource change, restart to upload'));
1245
- await prepareResource();
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}/game/qrcode`;
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-beta.4";
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
- index$2();
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
- index$1();
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
- index();
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