@ttmg/cli 0.1.3-beta.4 → 0.1.4

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,137 +578,88 @@ 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
  }
783
590
  catch (e) {
784
- console.log('read project.config.json failed', e);
591
+ console.log('read project.config.json failed, please check it');
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,57 @@ 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
+ resolve();
1005
780
  });
1006
- archive.finalize();
1007
- }
1008
- catch (err) {
1009
- reject(err);
781
+ archive.on('warning', err => console.warn('Archiver warning:', err));
782
+ output.on('error', err => reject(err));
783
+ archive.on('error', err => reject(err));
784
+ });
785
+ archive.pipe(output);
786
+ // 4. 使用 glob 在临时目录中查找文件,并应用过滤规则
787
+ const files = await glob__namespace.glob('**/*', {
788
+ cwd: tempDir,
789
+ nodir: true,
790
+ ignore: '**/*.js.map', // 过滤规则
791
+ });
792
+ // 5. 将过滤后的文件逐个添加到压缩包
793
+ for (const file of files) {
794
+ const filePath = path.join(tempDir, file);
795
+ archive.file(filePath, { name: file });
1010
796
  }
1011
- });
797
+ // 6. 完成压缩
798
+ await archive.finalize();
799
+ // 等待文件流关闭
800
+ await archivePromise;
801
+ }
802
+ catch (err) {
803
+ console.error('压缩过程中发生错误:', err);
804
+ throw err;
805
+ }
806
+ finally {
807
+ // 7. 无论成功还是失败,都清理临时目录
808
+ await fs.promises.rm(tempDir, { recursive: true, force: true });
809
+ }
1012
810
  }
1013
811
  async function uploadZip(zipPath, callback) {
1014
812
  const form = new FormData();
@@ -1019,8 +817,8 @@ async function uploadZip(zipPath, callback) {
1019
817
  // 帮我计算下文件大小,变成 MB 为单位
1020
818
  const fileSize = fs.statSync(zipPath).size / 1024 / 1024;
1021
819
  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`;
820
+ const { clientHttpPort, clientHost } = store.getState();
821
+ const url = `http://${clientHost}:${clientHttpPort}/game/upload`;
1024
822
  try {
1025
823
  // 1. 创建请求流
1026
824
  const stream = got.stream.post(url, {
@@ -1028,13 +826,13 @@ async function uploadZip(zipPath, callback) {
1028
826
  });
1029
827
  // 2. 监听上传进度 (这个回调是并行的,不影响封装)
1030
828
  stream.on('uploadProgress', progress => {
1031
- const percent = (progress.percent * 100).toFixed(1);
829
+ const percent = progress.percent;
1032
830
  // const transferred = progress.transferred;
1033
831
  // const total = progress.total;
1034
- process.stdout.write(`\r${chalk.cyan('Uploading progress: ')}${chalk.green(percent + '%')}`);
832
+ process.stdout.write(`\r${chalk.cyan('Uploading progress: ')}${chalk.green((percent * 100).toFixed(0) + '%')}`);
1035
833
  callback({
1036
834
  status: 'process',
1037
- percent: `${percent}%`,
835
+ percent,
1038
836
  });
1039
837
  });
1040
838
  // 3. 【核心封装】将流的处理过程包装在 Promise 中
@@ -1053,7 +851,7 @@ async function uploadZip(zipPath, callback) {
1053
851
  // 将完整的响应对象 resolve 出去
1054
852
  callback({
1055
853
  status: 'success',
1056
- percent: '100%',
854
+ percent: 1,
1057
855
  });
1058
856
  resolve({
1059
857
  statusCode: 200,
@@ -1065,7 +863,7 @@ async function uploadZip(zipPath, callback) {
1065
863
  reject(err);
1066
864
  callback({
1067
865
  status: 'error',
1068
- percent: '',
866
+ percent: 0,
1069
867
  msg: err.message,
1070
868
  });
1071
869
  });
@@ -1073,13 +871,12 @@ async function uploadZip(zipPath, callback) {
1073
871
  // 4. 当 await 完成后,说明流已成功结束,可以安全地执行后续操作
1074
872
  process.stdout.write('\n'); // 换行,保持终端整洁
1075
873
  console.log(chalk.green.bold('✔ Upload completed successfully!'));
1076
- fs.unlinkSync(zipPath);
1077
874
  return response;
1078
875
  }
1079
876
  catch (err) {
1080
877
  callback({
1081
878
  status: 'error',
1082
- percent: '',
879
+ percent: 0,
1083
880
  msg: err?.message,
1084
881
  });
1085
882
  process.stdout.write('\n');
@@ -1099,16 +896,26 @@ class WsServer {
1099
896
  constructor() {
1100
897
  this.ws = new WebSocket.Server({ port: DEV_WS_PORT });
1101
898
  this.ws.on('connection', ws => {
899
+ const { clientHttpPort, clientHost, clientWsPort } = store.getState();
900
+ if (clientHost) {
901
+ this.send({
902
+ method: 'clientDebugInfo',
903
+ payload: {
904
+ clientHttpPort,
905
+ clientHost,
906
+ clientWsPort,
907
+ },
908
+ });
909
+ }
1102
910
  ws.on('message', message => {
1103
911
  /** 客户端发送的消息 */
1104
912
  const clientMessage = JSON.parse(message.toString());
1105
- console.log('Client Message', clientMessage);
1106
913
  const from = clientMessage.from;
1107
914
  if (from === 'browser') {
915
+ console.log(chalk.yellow.bold('Browser message'), clientMessage);
1108
916
  const method = clientMessage.method;
1109
917
  switch (method) {
1110
918
  case 'startUpload':
1111
- console.log('startUpload');
1112
919
  this.sendUploadStatus('start');
1113
920
  uploadGame(({ status, percent, msg }) => {
1114
921
  if (status === 'process') {
@@ -1147,26 +954,67 @@ class WsServer {
1147
954
  }
1148
955
  }
1149
956
  else {
957
+ console.log(chalk.green.bold('Client message'), clientMessage);
1150
958
  const method = clientMessage.method;
1151
959
  switch (method) {
960
+ /**
961
+ * 客户端完成扫码成功,返回客户端的 host 和 port
962
+ */
963
+ case 'startScanQRcode': {
964
+ const payload = clientMessage.payload;
965
+ console.log('startQRcode', payload);
966
+ this.send({
967
+ method: 'startScanQRcode',
968
+ });
969
+ break;
970
+ }
971
+ case 'scanQRCodeResult': {
972
+ const payload = clientMessage.payload || {};
973
+ const { host, port, wsPort, errMsg, isSuccess } = payload;
974
+ if (isSuccess) {
975
+ store.setState({
976
+ clientHttpPort: port,
977
+ clientHost: host,
978
+ clientWsPort: wsPort,
979
+ });
980
+ this.send({
981
+ method: 'scanQRCodeSuccess',
982
+ payload: {
983
+ clientHttpPort: port,
984
+ clientHost: host,
985
+ clientWsPort: wsPort,
986
+ },
987
+ });
988
+ console.log('scanQRcodeSuccess');
989
+ }
990
+ else {
991
+ this.send({
992
+ method: 'scanQRCodeFailed',
993
+ payload: {
994
+ errMsg,
995
+ },
996
+ });
997
+ }
998
+ break;
999
+ }
1000
+ // 待废弃
1152
1001
  case 'shareDevParams':
1153
- console.log('shareDevParams', clientMessage);
1154
1002
  const payload = clientMessage.payload;
1155
- console.log('shareDevParams', payload);
1156
1003
  const { host, port, wsPort } = payload;
1157
1004
  store.setState({
1158
- clientServerPort: port,
1159
- clientServerHost: host,
1005
+ clientHttpPort: port,
1006
+ clientHost: host,
1007
+ clientWsPort: wsPort,
1008
+ clientWsHost: host,
1009
+ });
1010
+ this.send({
1011
+ method: 'scanQRCodeSuccess',
1012
+ payload: {
1013
+ clientHttpPort: port,
1014
+ clientHost: host,
1015
+ clientWsPort: wsPort,
1016
+ },
1160
1017
  });
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
1018
  break;
1171
1019
  }
1172
1020
  }
@@ -1184,6 +1032,11 @@ class WsServer {
1184
1032
  }
1185
1033
  });
1186
1034
  }
1035
+ sendResourceChange() {
1036
+ this.send({
1037
+ method: 'resourceChange',
1038
+ });
1039
+ }
1187
1040
  close() {
1188
1041
  this.ws.close();
1189
1042
  }
@@ -1197,13 +1050,22 @@ class WsServer {
1197
1050
  }
1198
1051
  const wsServer = new WsServer();
1199
1052
 
1200
- async function prepareResource() {
1201
- console.log(chalk.yellow.bold('Start compile game for debug'));
1053
+ async function prepareResource(context) {
1202
1054
  const entryDir = process.cwd();
1203
1055
  const outputDir = getOutputDir();
1056
+ const clientKey = getClientKey();
1057
+ if (!clientKey) {
1058
+ if (context.mode !== 'watch') {
1059
+ console.log(chalk.red.bold('No appid found in project.config.json, you should provide it in project.config.json'));
1060
+ process.exit(1);
1061
+ }
1062
+ return;
1063
+ }
1204
1064
  if (!fs.existsSync(outputDir)) {
1205
1065
  fs.mkdirSync(outputDir, { recursive: true });
1206
1066
  }
1067
+ const tip = context?.mode === 'watch' ? chalk.yellow('game resource change, restart to upload') : chalk.yellow.bold('Start compile game for debug');
1068
+ console.log(tip);
1207
1069
  const { isSuccess, errorMsg, packages } = await ttmgPack.debugPkgs({
1208
1070
  entry: entryDir,
1209
1071
  output: outputDir,
@@ -1215,6 +1077,7 @@ async function prepareResource() {
1215
1077
  enableLog: false,
1216
1078
  },
1217
1079
  build: {
1080
+ enableOdr: false,
1218
1081
  pkgSizeLimit: 30 * 1024 * 1024,
1219
1082
  mainPkgSizeLimit: 4 * 1024 * 1024,
1220
1083
  },
@@ -1229,6 +1092,11 @@ async function prepareResource() {
1229
1092
  packages,
1230
1093
  });
1231
1094
  console.log(chalk.green.bold('Compile game package success \n'));
1095
+ return {
1096
+ isSuccess,
1097
+ errorMsg,
1098
+ packages,
1099
+ };
1232
1100
  }
1233
1101
  }
1234
1102
 
@@ -1241,11 +1109,10 @@ async function watchChange() {
1241
1109
  clearTimeout(debounceTimer);
1242
1110
  // 重新设置定时器
1243
1111
  debounceTimer = setTimeout(async () => {
1244
- console.log(chalk.yellow('game resource change, restart to upload'));
1245
- await prepareResource();
1246
- wsServer.send({
1247
- method: 'resourceChange',
1112
+ await prepareResource({
1113
+ mode: 'watch',
1248
1114
  });
1115
+ wsServer.sendResourceChange();
1249
1116
  // TODO:只做文件预准备,但不主动上传
1250
1117
  // uploadGame()
1251
1118
  // .then((res) => {
@@ -1285,7 +1152,7 @@ async function showSchema() {
1285
1152
  margin: 1,
1286
1153
  });
1287
1154
  // 4. 构建可通过静态服务器访问的 URL
1288
- const qrCodeUrl = `http://localhost:${DEV_PORT}/game/qrcode`;
1155
+ const qrCodeUrl = `http://localhost:${DEV_PORT}`;
1289
1156
  // 5. 打印更新后的提示信息
1290
1157
  console.log(chalk.green.bold('Tips:'));
1291
1158
  console.log(` 1. ${chalk.yellow.bold('Open the link below in your browser to see the QR code, then scan it.')}`);
@@ -1321,7 +1188,7 @@ async function dev() {
1321
1188
  await watchChange();
1322
1189
  }
1323
1190
 
1324
- var version = "0.1.3-beta.4";
1191
+ var version = "0.1.4";
1325
1192
  var pkg = {
1326
1193
  version: version};
1327
1194
 
@@ -1329,6 +1196,7 @@ const program = new commander.Command();
1329
1196
  (async () => {
1330
1197
  try {
1331
1198
  await checkUpdate();
1199
+ // eslint-disable-next-line no-empty
1332
1200
  }
1333
1201
  catch (err) { }
1334
1202
  })();
@@ -1345,7 +1213,7 @@ program
1345
1213
  .action(() => {
1346
1214
  const options = program.opts(); // 获取 options
1347
1215
  if (options.h5) {
1348
- index$2();
1216
+ init();
1349
1217
  }
1350
1218
  else {
1351
1219
  console.log('Native Mini Game initialize');
@@ -1361,7 +1229,7 @@ program
1361
1229
  .action(() => {
1362
1230
  const options = program.opts();
1363
1231
  if (options.h5) {
1364
- index$1();
1232
+ dev$1();
1365
1233
  }
1366
1234
  else {
1367
1235
  dev();
@@ -1377,7 +1245,7 @@ program
1377
1245
  .action(() => {
1378
1246
  const options = program.opts(); // 获取 options
1379
1247
  if (options.h5) {
1380
- index();
1248
+ build();
1381
1249
  }
1382
1250
  else {
1383
1251
  console.log('Native Mini Game bundle');
@@ -1390,3 +1258,4 @@ program
1390
1258
  console.log('will support soon');
1391
1259
  });
1392
1260
  program.parse(process.argv);
1261
+ //# sourceMappingURL=index.js.map