@ttmg/cli 0.1.0-beta.8 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,16 @@
1
1
  ## 0.1.0
2
+
2
3
  发布正式版本,支持 H5 小游戏和原生小游戏的初始化、开发调试和打包构建。支持 Windows 系统
3
4
 
4
5
  ## 0.1.0-beta.1
6
+
5
7
  移除 Client WS 端口和 Http 端口默认 -1 的配置,变成通过和 Node CLI 通信显示发端口
6
8
 
7
9
  ## 0.1.0-beta.8
8
- 支持自动打开浏览器,且聚焦在 devTools 窗口
10
+
11
+ 支持自动打开浏览器,且聚焦在 devTools 窗口
12
+
13
+ ## 0.1.0-beta.9
14
+
15
+ 1. 支持文件上传进度显示
16
+ 2. 支持自动打开浏览器调试模式
package/dist/index.js CHANGED
@@ -1,2 +1,1246 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var e,o,t,n,s,r,i=require("commander"),c=require("inquirer"),a=require("fs"),l=require("jsdom"),u=require("prettier"),d=require("chalk"),p=require("express"),f=require("path"),g=require("cheerio"),m=require("chrome-launcher"),h=require("open"),y=require("os"),w=require("crypto"),b=require("archiver"),S=require("multer"),v=require("ws"),k=require("axios"),M=require("form-data"),j=require("qrcode-terminal"),_=require("ttmg-pack");function T(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function E(e){if(Object.prototype.hasOwnProperty.call(e,"__esModule"))return e;var o=e.default;if("function"==typeof o){var t=function e(){var t=!1;try{t=this instanceof e}catch{}return t?Reflect.construct(o,arguments,this.constructor):o.apply(this,arguments)};t.prototype=o.prototype}else t={};return Object.defineProperty(t,"__esModule",{value:!0}),Object.keys(e).forEach(function(o){var n=Object.getOwnPropertyDescriptor(e,o);Object.defineProperty(t,o,n.get?n:{enumerable:!0,get:function(){return e[o]}})}),t}function O(){return o?e:(o=1,e={CONFIG_FILE_NAME:"minigame.config.json",SDK_URL:"https://connect.tiktok-minis.com/game/sdk.js",VCONSOLE_URL:"https://connect.tiktok-minis.com/libs/vConsole.js",VCONSOLE_INIT:"\n if(typeof VConsole === 'function') {\n window.vConsole = new VConsole();\n }\n ",MINIS_MANIFEST_FILE_NAME:"minis.manifest.json",MINIS_RUNTIME_URL:"https://www.tiktok.com/minigames/runtime"})}function N(){if(n)return t;n=1;const e=a,o=l,s=u,r=d,{JSDOM:i}=o,{CONFIG_FILE_NAME:c,SDK_URL:p,VCONSOLE_URL:f,VCONSOLE_INIT:g}=O(),m=`${process.cwd()}/${c}`,h=`${process.cwd()}/index.html`;return t=async function({config:o,clientKey:t}){if(e.writeFileSync(m,JSON.stringify(o,null,2)),!e.existsSync(h))return void console.error("index.html does not exist");const n=e.readFileSync(h,"utf8"),c=new i(n),{document:a}=c.window;let l=a.querySelector("head");l||(l=a.createElement("head"),a.documentElement.insertBefore(l,a.body));const u=Array.from(a.querySelectorAll("script")),d=u.some(e=>e.src&&e.src.includes(p)),y=u.some(e=>e.src&&e.src.includes("vConsole.js"));let w=null;if(d){const e=Array.from(l.querySelectorAll("script"));if(w=e.find(e=>function(e,o){return!!e.innerHTML&&e.innerHTML.includes("TTMinis.game.init")&&e.innerHTML.includes(o)}(e,t)),w)console.log("JS SDK 已接入,跳过 SDK 相关脚本注入");else{const o=e.filter(e=>e.src&&e.src.includes(p));o.length>0&&(w=o[o.length-1])}}else{const e=a.createElement("script");e.src=p,l.insertBefore(e,l.firstChild);const o=a.createElement("script");o.innerHTML=`\n window.TTMinis = TTMinis;\n TTMinis.game.init({\n clientKey: "${t}",\n });\n `,l.insertBefore(o,e.nextSibling),w=o}if(function(e){return e.startsWith("sb")}(t)&&(console.log("Sandbox 环境,跳过 vConsole 相关脚本注入"),!y)){let e=w;e=e?e.nextSibling:l.firstChild;const o=a.createElement("script");o.src=f;const t=a.createElement("script");t.innerHTML=g,l.insertBefore(o,e),l.insertBefore(t,e)}const b=await s.format(c.serialize(),{parser:"html"});e.writeFileSync(h,b),console.log(r.green.bold("TikTok H5 Mini Game initialization has been completed..."))},t}var P=function(){if(r)return s;r=1;const e=c,o=N();return s=function(){e.createPromptModule()([{type:"input",name:"clientKey",message:"Please input client key"},{type:"input",name:"devPort",message:"Please input dev port",default:9527}]).then(async e=>{const{clientKey:t,devPort:n}=e,s={_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.",orientation:"VERTICAL",dev:{port:n}};await o({clientKey:t,config:s}),process.exit(0)}).catch(()=>{process.exit(1)})},s}(),$=T(P);async function I(e){try{return await m.launch({startingUrl:e,chromeFlags:["--auto-open-devtools-for-tabs","--no-default-browser-check","--disable-web-security","--allow-insecure-localhost","--ignore-certificate-errors","--allow-running-insecure-content","--remote-allow-origins=*","--user-data-dir=/tmp/chrome-debug-profile"]})}catch(o){return console.warn("启动 Chrome 失败,回退到系统默认浏览器:",(null==o?void 0:o.message)||o),await h(e),null}}function C(){const e=y.networkInterfaces();for(const o in e){const t=e[o];if(t)for(const e of t)if("IPv4"===e.family&&!e.internal)return e.address}}var z,L,x=E(Object.freeze({__proto__:null,centerQRCode:function(e){const o=process.stdout.columns||80,t=e.split("\n"),n=t.reduce((e,o)=>Math.max(e,o.length),0),s=Math.floor((o-n)/3),r=" ".repeat(s>0?s:0);return t.map(e=>r+e).join("\n")},getDesktopPath:function(){const e=y.homedir(),o=["Desktop","桌面"];for(const t of o){const o=f.join(e,t);if(a.existsSync(o))return o}return f.join(e,"Desktop")},getLocalIP:C,openUrl:I}));var q,U,D,F,H=function(){if(L)return z;L=1;const e=p,o=f,t=a,n=d,s=g,r=e(),{openUrl:i}=x,c=w,{CONFIG_FILE_NAME:l,MINIS_RUNTIME_URL:u}=O();return z=function(){const a=o.join(process.cwd(),l);if(!t.existsSync(a))return void console.log(n.red.bold(`${l} is not exist, please run minis game init first`));const d=JSON.parse(t.readFileSync(a,"utf8")),p=d.dev?.port||9527;console.log(n.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.")),console.log(n.bold.blue("\n \n============== start dev your game, it will take a few seconds ============ \n \n")),r.use((e,o,t)=>{e.url.endsWith(".br")?o.setHeader("Content-Encoding","br"):e.url.endsWith(".gz")&&o.setHeader("Content-Encoding","gzip"),t()}),r.use((e,r,i)=>{try{const e=o.join(process.cwd(),"index.html"),n=t.readFileSync(e,"utf8"),i=s.load(n),a=[];i("script:not([src])").each((e,o)=>{const t=i(o).html();t&&t.trim()&&a.push(t)});a.map(e=>`'sha256-${c.createHash("sha256").update(e,"utf8").digest("base64")}'`);const l="*";r.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' ${l} data: blob: *; connect-src 'self' ${l} data: blob: ; style-src 'self' ${l} 'unsafe-inline' fonts.googleapis.com data: blob: *; font-src 'self' fonts.gstatic.com blob: data: *; media-src 'self' ${l} data: blob: *; frame-src 'none'; base-uri 'self'; worker-src 'self' blob: data: ;`)}catch(e){console.warn(n.red("Failed to set CSP header:"),e.message)}i()}),r.use(e.static(o.join(process.cwd()))),r.listen(p,()=>{const e=`${u}?minis_url=${`http://localhost:${p}`}&enable_log=1`;console.log(`you can access ${n.green.underline.bold(e)} to debug your game in browser...`);try{i(e)}catch(e){console.warn(n.red("Failed to open browser, you can access it manually"),e.message)}})}}(),G=T(H);function A(){if(U)return q;U=1;const e=d,o=a,t=f,{MINIS_MANIFEST_FILE_NAME:n}=O();function s(e,o){let t=o.find(o=>"folder"===o.type&&o.name===e[0]);return t||(t={type:"folder",name:e[0],children:[]},o.push(t)),e.length>1?s(e.slice(1),t.children):t}function r(e,n=e){const s={};return o.readdirSync(e).forEach(i=>{const c=t.join(e,i);if(o.statSync(c).isDirectory()){const e=r(c,n);Object.assign(s,e)}else if(""!==t.extname(i)&&i){const e="/"+t.relative(n,c).replace(/\\/g,"/");s[c]=e}}),s}return q={buildMinisManifest:async function(){try{const e=t.join(process.cwd()),i=[],c=r(e);Object.keys(c).filter(e=>!e.endsWith(".map")).forEach(e=>{const o=c[e].split("/").filter(Boolean),t=o.pop()||"";if(0===o.length)i.push({type:"file",name:t});else{s(o,i).children.push({type:"file",name:t})}}),o.writeFileSync(t.join(e,n),JSON.stringify({name:n,resource_list:i},null,2))}catch(o){console.error(e.red(`Error during debug process: ${o.message}`)),o instanceof Error&&o.stack&&console.error(e.red(`Stack trace: ${o.stack}`)),process.exit(1)}}}}var R=T(function(){if(F)return D;F=1;const e=a,o=y,t=f,n=d,s=b,r=c.createPromptModule(),{buildMinisManifest:i}=A();return D=async function(){try{const{zipName:c}=await r({type:"input",name:"zipName",default:"game",message:"Please input zip name"}),a=Date.now();console.log(n.bold.blue("start build your game, it will take a few minutes..."));e.readdirSync(process.cwd()).forEach(o=>{o.endsWith(".zip")&&e.unlinkSync(t.join(process.cwd(),o))}),await i();const l=s("zip",{zlib:{level:9}});l.on("error",function(e){throw e});const u=function(){const n=o.homedir(),s=["Desktop","桌面"];for(const o of s){const s=t.join(n,o);if(e.existsSync(s))return s}const r=t.join(n,"Desktop");return console.log(`未找到特定桌面文件夹,使用默认路径: ${r}`),r}(),d=t.join(u,`${c}.zip`);await l.pipe(e.createWriteStream(d)),await l.directory(t.resolve(process.cwd()),!1),await l.finalize(),console.log(n.yellow.bold(`build ${c}.zip success, you can find it in desktop, use time ${Date.now()-a} ms`)),process.exit(0)}catch(e){console.log(n.red(`auto build ${zipNameInput}.zip failed: ${e.message}, you should zip it manually`)),process.exit(1)}}}());class W{constructor(e){this.listeners=[],this.state=e}static getInstance(e){return W.instance||(W.instance=new W(e)),W.instance}getState(){return this.state}setState(e){this.state=Object.assign(Object.assign({},this.state),e),this.listeners.forEach(e=>e(this.state))}subscribe(e){return this.listeners.push(e),()=>{this.listeners=this.listeners.filter(o=>o!==e)}}reset(e){this.state=e,this.listeners.forEach(e=>e(this.state))}}const B=W.getInstance({clientServerPort:"",clientServerHost:"",clientKey:"",packages:{}}),V=9529,K=f.join(y.homedir(),"__TTMG__");function J({clientWsPort:e}){const o={ws_port:V,nodeWsPort:V,clientWsPort:e};return btoa(JSON.stringify(o))}let Q;function Y(){Q.close(()=>{console.log("Dev server closed"),Q=null,process.exit(0)})}const Z=f.join(y.homedir(),".ttmg-cli");function X(){const e=f.join(process.cwd(),"project.config.json");let o;try{const t=JSON.parse(a.readFileSync(e,"utf-8"));o=t.appid||t.appId}catch(e){o=""}if(o)return o;console.log(d.red.bold("No appid found in project.config.json, you should provide it in project.config.json")),process.exit(1)}function ee(){const e=X();return f.join(y.homedir(),"__TTMG__",e)}async function oe(){const e=ee();console.log(d.yellow.bold("Start compress game resource"));const o=f.join(y.homedir(),"__TTMG__","upload.zip");var t,n;await(t=e,n=o,new Promise((e,o)=>{const s=a.createWriteStream(n),r=b("zip",{zlib:{level:9}});s.on("close",()=>{e()}),s.on("error",e=>{o(e)}),r.on("error",e=>{o(e)}),r.pipe(s),r.directory(t,!1),r.finalize()})),console.log(d.green.bold("Compress game package resource success \n"));const s=await async function(e){const o=new M;o.append("file",a.createReadStream(e),"upload.zip");try{console.log(d.yellow.bold("Start upload resource to client"));const{clientServerHost:t,clientServerPort:n}=B.getState(),s=await k.post(`http://${t}:${n}/game/upload`,o,{headers:o.getHeaders()});return a.unlinkSync(e),s}catch(e){console.error("Upload resource to client failed:",e.message)}}(o);return{isSuccess:200===(null==s?void 0:s.status)}}f.join(Z,"config.json");const te=new class{constructor(){this.ws=new v.Server({port:V}),this.ws.on("connection",e=>{e.on("message",e=>{const o=JSON.parse(e.toString());console.log("Client Message",o);if("browser"===o.from){switch(o.method){case"connected":this.sendUploadStatus("start"),oe().then(e=>{e.isSuccess?(this.sendUploadStatus("success",{packages:B.getState().packages,isSuccess:e.isSuccess}),console.log(d.green.bold("Upload resource to client successfully!"))):(this.sendUploadStatus("error",{errMsg:e.errorMsg}),console.log(d.red.bold("Upload resource to client failed!")))}).catch(()=>{this.sendUploadStatus("error",{isSuccess:!1}),console.log(d.red.bold("Start upload resource to client failed!"))});break;case"closeLocalDebug":console.log("closeLocalDebug"),this.ws.close(),Y(),console.log("close server")}}else{switch(o.method){case"shareDevParams":console.log("shareDevParams",o);const e=o.payload;console.log("shareDevParams",e);const{host:t,port:n,wsPort:s}=e;B.setState({clientServerPort:n,clientServerHost:t});const r=`http://${t}:${n}?session=${J({clientWsPort:s})}`;I(r),console.log(d.bold.yellow(`Game debug is ready! Visit ${r} in your browser.`));break;case"checkPermissionFailed":console.log(d.red.bold("Check permission failed! Please authorize in client first."))}}})})}send(e){this.ws.clients.forEach(o=>{if(o.readyState===v.OPEN){const t=Object.assign(Object.assign({},e),{from:"nodeServer"});o.send(JSON.stringify(t))}})}close(){this.ws.close()}sendUploadStatus(e,o){this.send({method:"uploadStatus",status:e,payload:o})}};async function ne(){await async function(){console.log(d.yellow.bold("Start compile game for debug"));const e=process.cwd(),o=ee();a.existsSync(o)||a.mkdirSync(o,{recursive:!0});const{isSuccess:t,errorMsg:n,packages:s}=await _.debugPkgs({entry:e,output:o});t?(B.setState({packages:s}),console.log(d.green.bold("Compile game package success \n"))):(console.log(d.redBright("Build game package failed, Please check the error message below:")),console.log(d.redBright(n)),process.exit(1))}(),await async function(){Q&&Y();const e=p(),o=9528,t=S({dest:y.tmpdir()});return e.use(p.json()),e.use(p.urlencoded({extended:!0})),e.post("/game/env",async(e,o)=>{const t=e.body,{host:n,port:s,wsPort:r}=t;B.setState({clientServerPort:s,clientServerHost:n});const i=`http://${n}:${s}?session=${J({clientWsPort:r})}`;I(i),console.log(d.bold.yellow(`Game debug is ready! Visit ${i} in your browser.`)),o.json({code:0,msg:"ok",data:{devUrl:i}})}),e.post("/game/upload",t.single("file"),async(e,o)=>{o.json({code:0,msg:"ok",filename:e.file.filename,originalname:e.file.originalname,size:e.file.size,path:e.file.path})}),Q=e.listen(o,()=>{console.log(d.cyan.bold("Node devServer is running on port 9528"))}),{port:o,host:C()}}(),await async function(){const e=X(),o=f.join(K,e),t=C();console.log(d.green.bold("Tips:")),console.log(` 1. ${d.yellow.bold("Scan the QR code to start the client devServer.")}`),console.log(` 2. ${d.yellow.bold("Will auto upload compiled resource to client.")} ${d.bold(o)}`),console.log(` 3. ${d.yellow.bold("Debug your game in the browser.")}\n`);const n=`https://www.tiktok.com/ttmg/dev/${e}?host=${t}&port=9529`;j.generate(n,{small:!0},e=>{console.log(e)})}(),await async function(){let e=null;a.watch(process.cwd(),(o,t)=>{console.log(d.yellow("game resource change, restart to upload")),e&&clearTimeout(e),e=setTimeout(async()=>{te.sendUploadStatus("start"),oe().then(e=>{e.isSuccess?te.sendUploadStatus("success"):te.sendUploadStatus("error",{errMsg:e.errorMsg})}).catch(()=>{te.sendUploadStatus("error")}),e=null},500)})}()}var se="0.1.0-beta.8";const re=new i.Command;re.name("ttmg").description("TikTok Mini Games Command Line Tool").version(se,"-v, --version","显示版本号").option("dev","Debug TikTok Mini Games for Client").option("dev --h5","Debug TikTok Mini Games for Web"),re.option("--h5","H5 Mini Game").command("init").description("Initialize project").action(()=>{re.opts().h5?$():console.log("Native Mini Game initialize")}),re.option("--h5","H5 Mini Game").command("dev").description("Open browser dev environment").action(()=>{re.opts().h5?G():ne()}),re.option("--h5","H5 Mini Game").command("build").description("Bundle project").action(()=>{re.opts().h5?R():console.log("Native Mini Game bundle")}),re.command("login").description("User Dev Portal Account to Login").action(async()=>{console.log("will support soon")}),re.parse(process.argv);
2
+ 'use strict';
3
+
4
+ var commander = require('commander');
5
+ var require$$0 = require('inquirer');
6
+ var fs = require('fs');
7
+ var require$$1 = require('jsdom');
8
+ var require$$2 = require('prettier');
9
+ var chalk = require('chalk');
10
+ var express = require('express');
11
+ var path = require('path');
12
+ var require$$4 = require('cheerio');
13
+ var chromeLauncher = require('chrome-launcher');
14
+ var os = require('os');
15
+ var require$$6 = require('crypto');
16
+ var require$$5$1 = require('archiver');
17
+ var multer = require('multer');
18
+ var WebSocket = require('ws');
19
+ var got = require('got');
20
+ var FormData = require('form-data');
21
+ var qrcode = require('qrcode-terminal');
22
+ var ttmgPack = require('ttmg-pack');
23
+
24
+ function getDefaultExportFromCjs (x) {
25
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
26
+ }
27
+
28
+ function getAugmentedNamespace(n) {
29
+ if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
30
+ var f = n.default;
31
+ if (typeof f == "function") {
32
+ var a = function a () {
33
+ var isInstance = false;
34
+ try {
35
+ isInstance = this instanceof a;
36
+ } catch {}
37
+ if (isInstance) {
38
+ return Reflect.construct(f, arguments, this.constructor);
39
+ }
40
+ return f.apply(this, arguments);
41
+ };
42
+ a.prototype = f.prototype;
43
+ } else a = {};
44
+ Object.defineProperty(a, '__esModule', {value: true});
45
+ Object.keys(n).forEach(function (k) {
46
+ var d = Object.getOwnPropertyDescriptor(n, k);
47
+ Object.defineProperty(a, k, d.get ? d : {
48
+ enumerable: true,
49
+ get: function () {
50
+ return n[k];
51
+ }
52
+ });
53
+ });
54
+ return a;
55
+ }
56
+
57
+ var config;
58
+ var hasRequiredConfig;
59
+
60
+ function requireConfig () {
61
+ if (hasRequiredConfig) return config;
62
+ hasRequiredConfig = 1;
63
+ config = {
64
+ CONFIG_FILE_NAME: 'minigame.config.json',
65
+ SDK_URL: 'https://connect.tiktok-minis.com/game/sdk.js',
66
+ VCONSOLE_URL: 'https://connect.tiktok-minis.com/libs/vConsole.js',
67
+ VCONSOLE_INIT: `
68
+ if(typeof VConsole === 'function') {
69
+ window.vConsole = new VConsole();
70
+ }
71
+ `,
72
+ MINIS_MANIFEST_FILE_NAME: 'minis.manifest.json',
73
+ MINIS_RUNTIME_URL: 'https://www.tiktok.com/minigames/runtime',
74
+ };
75
+ return config;
76
+ }
77
+
78
+ var inject;
79
+ var hasRequiredInject;
80
+
81
+ function requireInject () {
82
+ if (hasRequiredInject) return inject;
83
+ hasRequiredInject = 1;
84
+ const fs$1 = fs;
85
+ const jsdom = require$$1;
86
+ const prettier = require$$2;
87
+ const chalk$1 = chalk;
88
+ const { JSDOM } = jsdom;
89
+ const {
90
+ CONFIG_FILE_NAME,
91
+ SDK_URL,
92
+ VCONSOLE_URL,
93
+ VCONSOLE_INIT,
94
+ } = requireConfig();
95
+ const CONFIG_PATH = `${process.cwd()}/${CONFIG_FILE_NAME}`;
96
+ const INDEX_HTML_PATH = `${process.cwd()}/index.html`;
97
+
98
+ function isSandbox(clientKey) {
99
+ /**
100
+ * sb 开头的 clientKey 都是 sandbox 环境
101
+ */
102
+ return clientKey.startsWith('sb');
103
+ }
104
+ // 判断是否是 TTMinis.game.init 的初始化脚本
105
+ function isTTMinisInitScript(script, clientKey) {
106
+ if (!script.innerHTML) return false;
107
+ return (
108
+ script.innerHTML.includes('TTMinis.game.init') &&
109
+ script.innerHTML.includes(clientKey)
110
+ );
111
+ }
112
+
113
+ async function injectScripts({ config, clientKey }) {
114
+ // 1. 检查 config 文件是否已存在
115
+ fs$1.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
116
+
117
+ // 2. 读取 index.html
118
+ if (!fs$1.existsSync(INDEX_HTML_PATH)) {
119
+ console.error('index.html does not exist');
120
+ return;
121
+ }
122
+ const indexHtml = fs$1.readFileSync(INDEX_HTML_PATH, 'utf8');
123
+ const dom = new JSDOM(indexHtml);
124
+ const { document } = dom.window;
125
+
126
+ // 3. head 标签
127
+ let head = document.querySelector('head');
128
+ if (!head) {
129
+ head = document.createElement('head');
130
+ document.documentElement.insertBefore(head, document.body);
131
+ }
132
+
133
+ // 4. 检查是否已注入 SDK
134
+ const scriptList = Array.from(document.querySelectorAll('script'));
135
+ const hasSDK = scriptList.some(
136
+ script => script.src && script.src.includes(SDK_URL),
137
+ );
138
+
139
+ // 5. 检查是否已注入 vConsole
140
+ const hasVConsole = scriptList.some(
141
+ script => script.src && script.src.includes('vConsole.js'),
142
+ );
143
+
144
+ let lastInitScript = null;
145
+ if (!hasSDK) {
146
+ // 插入 SDK 脚本
147
+ const sdkScript = document.createElement('script');
148
+ sdkScript.src = SDK_URL;
149
+ head.insertBefore(sdkScript, head.firstChild);
150
+
151
+ // 插入 SDK init 脚本
152
+ const initScript = document.createElement('script');
153
+ initScript.innerHTML = `
154
+ window.TTMinis = TTMinis;
155
+ TTMinis.game.init({
156
+ clientKey: "${clientKey}",
157
+ });
158
+ `;
159
+ head.insertBefore(initScript, sdkScript.nextSibling);
160
+ lastInitScript = initScript;
161
+ } else {
162
+ // 已经有 SDK,查找 TTMinis.game.init 脚本
163
+ const headScripts = Array.from(head.querySelectorAll('script'));
164
+ lastInitScript = headScripts.find(script =>
165
+ isTTMinisInitScript(script, clientKey),
166
+ );
167
+ if (lastInitScript) {
168
+ console.log('JS SDK 已接入,跳过 SDK 相关脚本注入');
169
+ } else {
170
+ // 没有 TTMinis.game.init,则查找最后一个 SDK 脚本
171
+ const sdkScripts = headScripts.filter(
172
+ script => script.src && script.src.includes(SDK_URL),
173
+ );
174
+ if (sdkScripts.length > 0) {
175
+ lastInitScript = sdkScripts[sdkScripts.length - 1];
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 只有 Sandbox 环境才需要注入 vConsole
182
+ */
183
+ if (isSandbox(clientKey)) {
184
+ console.log('Sandbox 环境,跳过 vConsole 相关脚本注入');
185
+ // 8. 插入 vConsole 相关脚本(如果需要)
186
+ if (!hasVConsole) {
187
+ // vConsole 相关脚本的插入点
188
+ let insertAfterNode = lastInitScript;
189
+ if (insertAfterNode) {
190
+ insertAfterNode = insertAfterNode.nextSibling;
191
+ } else {
192
+ insertAfterNode = head.firstChild;
193
+ }
194
+
195
+ // vConsole 源码
196
+ const vconsoleSourceScript = document.createElement('script');
197
+ vconsoleSourceScript.src = VCONSOLE_URL;
198
+ // vConsole 初始化
199
+ const vconsoleInitScript = document.createElement('script');
200
+ vconsoleInitScript.innerHTML = VCONSOLE_INIT;
201
+
202
+ head.insertBefore(vconsoleSourceScript, insertAfterNode);
203
+ head.insertBefore(vconsoleInitScript, insertAfterNode);
204
+ }
205
+ }
206
+
207
+ // 9. 格式化并写回 index.html
208
+ const formattedHtml = await prettier.format(dom.serialize(), {
209
+ parser: 'html',
210
+ });
211
+ fs$1.writeFileSync(INDEX_HTML_PATH, formattedHtml);
212
+ console.log(
213
+ chalk$1.green.bold(
214
+ 'TikTok H5 Mini Game initialization has been completed...',
215
+ ),
216
+ );
217
+ }
218
+
219
+ inject = injectScripts;
220
+ return inject;
221
+ }
222
+
223
+ var init;
224
+ var hasRequiredInit;
225
+
226
+ function requireInit () {
227
+ if (hasRequiredInit) return init;
228
+ hasRequiredInit = 1;
229
+ const inquirer = require$$0;
230
+ const inject = requireInject();
231
+
232
+ init = function init() {
233
+ const promptModule = inquirer.createPromptModule();
234
+ promptModule([
235
+ {
236
+ type: 'input',
237
+ name: 'clientKey',
238
+ message: 'Please input client key',
239
+ },
240
+ {
241
+ type: 'input',
242
+ name: 'devPort',
243
+ message: 'Please input dev port',
244
+ default: 9527,
245
+ },
246
+ ])
247
+ .then(async answers => {
248
+ const { clientKey, devPort } = answers;
249
+ const config = {
250
+ _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.`,
251
+ orientation: 'VERTICAL',
252
+ dev: {
253
+ port: devPort,
254
+ },
255
+ };
256
+ await inject({ clientKey, config });
257
+
258
+ process.exit(0);
259
+ })
260
+ .catch(() => {
261
+ process.exit(1);
262
+ });
263
+ };
264
+ return init;
265
+ }
266
+
267
+ var initExports = requireInit();
268
+ var index$2 = /*@__PURE__*/getDefaultExportFromCjs(initExports);
269
+
270
+ async function openUrl(url) {
271
+ try {
272
+ await chromeLauncher.launch({
273
+ startingUrl: url,
274
+ chromeFlags: [
275
+ '--auto-open-devtools-for-tabs', // 自动打开 DevTools
276
+ '--no-default-browser-check',
277
+ '--allow-insecure-localhost',
278
+ '--ignore-certificate-errors',
279
+ '--allow-running-insecure-content',
280
+ '--remote-allow-origins=*',
281
+ '--user-data-dir=/tmp/chrome-debug-profile',
282
+ ],
283
+ });
284
+ await new Promise(() => { });
285
+ }
286
+ catch (e) {
287
+ // const open = await import('open');
288
+ // await open.default(url);
289
+ }
290
+ }
291
+
292
+ function getDesktopPath() {
293
+ const homeDir = os.homedir();
294
+ // 常见桌面文件夹名
295
+ const desktopNames = ['Desktop', '桌面'];
296
+ for (const name of desktopNames) {
297
+ const desktopPath = path.join(homeDir, name);
298
+ if (fs.existsSync(desktopPath)) {
299
+ return desktopPath;
300
+ }
301
+ }
302
+ // 没找到就默认返回 Desktop
303
+ return path.join(homeDir, 'Desktop');
304
+ }
305
+
306
+ function centerQRCode(qrString) {
307
+ const terminalWidth = process.stdout.columns || 80; // 获取终端宽度,默认80
308
+ const lines = qrString.split('\n');
309
+ const qrWidth = lines.reduce((max, line) => Math.max(max, line.length), 0);
310
+ const padding = Math.floor((terminalWidth - qrWidth) / 3);
311
+ const padStr = ' '.repeat(padding > 0 ? padding : 0);
312
+ return lines.map(line => padStr + line).join('\n');
313
+ }
314
+
315
+ function getLocalIP() {
316
+ const networkInterfaces = os.networkInterfaces();
317
+ for (const interfaceName in networkInterfaces) {
318
+ const interfaceInfo = networkInterfaces[interfaceName];
319
+ if (!interfaceInfo)
320
+ continue;
321
+ for (const addressInfo of interfaceInfo) {
322
+ if (addressInfo.family === 'IPv4' && !addressInfo.internal) {
323
+ return addressInfo.address;
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ var libs = /*#__PURE__*/Object.freeze({
330
+ __proto__: null,
331
+ centerQRCode: centerQRCode,
332
+ getDesktopPath: getDesktopPath,
333
+ getLocalIP: getLocalIP,
334
+ openUrl: openUrl
335
+ });
336
+
337
+ var require$$5 = /*@__PURE__*/getAugmentedNamespace(libs);
338
+
339
+ var dev$1;
340
+ var hasRequiredDev;
341
+
342
+ function requireDev () {
343
+ if (hasRequiredDev) return dev$1;
344
+ hasRequiredDev = 1;
345
+ const express$1 = express;
346
+ const path$1 = path;
347
+ const fs$1 = fs;
348
+ const chalk$1 = chalk;
349
+ const cheerio = require$$4;
350
+ const app = express$1();
351
+ const { openUrl } = require$$5;
352
+ const crypto = require$$6;
353
+ // const open = require('open'); // 引入 open 包
354
+ const { CONFIG_FILE_NAME, MINIS_RUNTIME_URL } = requireConfig();
355
+
356
+ // 1. 检查配置文件是否存在
357
+ dev$1 = function dev() {
358
+ const configPath = path$1.join(process.cwd(), CONFIG_FILE_NAME);
359
+ if (!fs$1.existsSync(configPath)) {
360
+ console.log(
361
+ chalk$1.red.bold(
362
+ `${CONFIG_FILE_NAME} is not exist, please run minis game init first`,
363
+ ),
364
+ );
365
+ return;
366
+ }
367
+
368
+ // 2. 读取配置
369
+ const gameConfig = JSON.parse(fs$1.readFileSync(configPath, 'utf8'));
370
+ const devPort = gameConfig.dev?.port || 9527;
371
+
372
+ // 3. 打印开发前提示
373
+ console.log(
374
+ chalk$1.yellow.bold(
375
+ `⚠️ 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.`,
376
+ ),
377
+ );
378
+ console.log(
379
+ chalk$1.bold.blue(
380
+ '\n \n============== start dev your game, it will take a few seconds ============ \n \n',
381
+ ),
382
+ );
383
+
384
+ /**
385
+ * 支持 .br 文件, 支持 gzip
386
+ */
387
+
388
+ app.use((req, res, next) => {
389
+ if (req.url.endsWith('.br')) {
390
+ res.setHeader('Content-Encoding', 'br');
391
+ } else if (req.url.endsWith('.gz')) {
392
+ res.setHeader('Content-Encoding', 'gzip');
393
+ }
394
+ next();
395
+ });
396
+
397
+ /**
398
+ * 给所有的请求返回设置 CSP
399
+ */
400
+ app.use((req, res, next) => {
401
+ /**
402
+ * 计算 HTML 中的内联脚本生成 hash 插入 CSP 中
403
+ */
404
+ try {
405
+ // 1. 读取 HTML 文件内容
406
+ const htmlPath = path$1.join(process.cwd(), 'index.html');
407
+ const html = fs$1.readFileSync(htmlPath, 'utf8');
408
+
409
+ // 2. 用 cheerio 解析 HTML
410
+ const $ = cheerio.load(html);
411
+
412
+ // 3. 提取所有无 src 属性的内联 <script> 内容
413
+ const scripts = [];
414
+ $('script:not([src])').each((i, elem) => {
415
+ const content = $(elem).html();
416
+ if (content && content.trim()) {
417
+ scripts.push(content);
418
+ }
419
+ });
420
+
421
+ // 4. 计算每段脚本的 SHA-256 hash 并 base64 编码
422
+ const hashes = scripts.map(script => {
423
+ const hash = crypto
424
+ .createHash('sha256')
425
+ .update(script, 'utf8')
426
+ .digest('base64');
427
+ return `'sha256-${hash}'`;
428
+ });
429
+
430
+ // 开发者本地调试,信任的域名默认为 * 便于调试
431
+ const devTrustedDomain = '*';
432
+
433
+ res.setHeader(
434
+ 'Content-Security-Policy',
435
+ `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: ;`,
436
+ );
437
+ } catch (e) {
438
+ // 如果 index.html 不存在或有异常,CSP 头就不设置
439
+ console.warn(chalk$1.red('Failed to set CSP header:'), e.message);
440
+ }
441
+ next();
442
+ });
443
+
444
+ // 4. 静态资源服务
445
+ app.use(express$1.static(path$1.join(process.cwd())));
446
+
447
+ // 5. 启动服务并自动打开浏览器
448
+ app.listen(devPort, () => {
449
+ const gameUrl = `http://localhost:${devPort}`;
450
+ const devUrl = `${MINIS_RUNTIME_URL}?minis_url=${gameUrl}&enable_log=1`;
451
+
452
+ console.log(
453
+ `you can access ${chalk$1.green.underline.bold(
454
+ devUrl,
455
+ )} to debug your game in browser...`,
456
+ );
457
+ try {
458
+ // 自动打开浏览器,跨平台
459
+ openUrl(devUrl);
460
+ } catch (e) {
461
+ console.warn(
462
+ chalk$1.red('Failed to open browser, you can access it manually'),
463
+ e.message,
464
+ );
465
+ }
466
+ });
467
+ };
468
+ return dev$1;
469
+ }
470
+
471
+ var devExports = requireDev();
472
+ var index$1 = /*@__PURE__*/getDefaultExportFromCjs(devExports);
473
+
474
+ var manifest;
475
+ var hasRequiredManifest;
476
+
477
+ function requireManifest () {
478
+ if (hasRequiredManifest) return manifest;
479
+ hasRequiredManifest = 1;
480
+ const chalk$1 = chalk;
481
+ const fs$1 = fs;
482
+ const path$1 = path;
483
+ const { MINIS_MANIFEST_FILE_NAME } = requireConfig();
484
+ async function buildMinisManifest() {
485
+ try {
486
+ const buildPath = path$1.join(process.cwd());
487
+ const resourceList = [];
488
+ const allFiles = collectAllFiles(buildPath);
489
+
490
+ Object.keys(allFiles)
491
+ .filter(file => !file.endsWith('.map'))
492
+ .forEach(file => {
493
+ const relativeFilePath = allFiles[file];
494
+ const filePathArr = relativeFilePath.split('/').filter(Boolean); // Split filename
495
+ const fileName = filePathArr.pop() || ''; // Get filename
496
+ if (filePathArr.length === 0) {
497
+ resourceList.push({ type: 'file', name: fileName });
498
+ } else {
499
+ const folder = findOrCreateFolder(filePathArr, resourceList);
500
+ folder.children.push({ type: 'file', name: fileName });
501
+ }
502
+ });
503
+
504
+ fs$1.writeFileSync(
505
+ path$1.join(buildPath, MINIS_MANIFEST_FILE_NAME),
506
+ JSON.stringify(
507
+ { name: MINIS_MANIFEST_FILE_NAME, resource_list: resourceList },
508
+ null,
509
+ 2,
510
+ ),
511
+ );
512
+ } catch (error) {
513
+ console.error(chalk$1.red(`Error during debug process: ${error.message}`));
514
+ if (error instanceof Error && error.stack) {
515
+ console.error(chalk$1.red(`Stack trace: ${error.stack}`));
516
+ }
517
+ process.exit(1);
518
+ }
519
+ }
520
+
521
+ function findOrCreateFolder(pathArray, currentFolder) {
522
+ let folder = currentFolder.find(
523
+ item => item.type === 'folder' && item.name === pathArray[0],
524
+ );
525
+ if (!folder) {
526
+ folder = { type: 'folder', name: pathArray[0], children: [] };
527
+ currentFolder.push(folder);
528
+ }
529
+ if (pathArray.length > 1) {
530
+ return findOrCreateFolder(pathArray.slice(1), folder.children);
531
+ }
532
+ return folder;
533
+ }
534
+
535
+ function collectAllFiles(dir, baseDir = dir) {
536
+ const files = {};
537
+ fs$1.readdirSync(dir).forEach(file => {
538
+ const filePath = path$1.join(dir, file);
539
+ const fileStat = fs$1.statSync(filePath);
540
+ if (fileStat.isDirectory()) {
541
+ // Process subdirectories recursively
542
+ const subFiles = collectAllFiles(filePath, baseDir);
543
+ Object.assign(files, subFiles);
544
+ } else if (path$1.extname(file) !== '' && file) {
545
+ const relativePath =
546
+ '/' + path$1.relative(baseDir, filePath).replace(/\\/g, '/');
547
+ files[filePath] = relativePath;
548
+ }
549
+ });
550
+ return files;
551
+ }
552
+
553
+ manifest = {
554
+ buildMinisManifest,
555
+ };
556
+ return manifest;
557
+ }
558
+
559
+ var bundle;
560
+ var hasRequiredBundle;
561
+
562
+ function requireBundle () {
563
+ if (hasRequiredBundle) return bundle;
564
+ hasRequiredBundle = 1;
565
+ const fs$1 = fs;
566
+ const os$1 = os;
567
+ const path$1 = path;
568
+ const inquirer = require$$0;
569
+ const chalk$1 = chalk;
570
+ const archiver = require$$5$1;
571
+ const promptModule = inquirer.createPromptModule();
572
+ const { buildMinisManifest } = requireManifest();
573
+
574
+ bundle = async function build() {
575
+ try {
576
+ /**
577
+ * 用户输入打包后的游戏压缩包名称
578
+ */
579
+ const { zipName: zipNameInput } = await promptModule({
580
+ type: 'input',
581
+ name: 'zipName',
582
+ default: 'game',
583
+ message: 'Please input zip name',
584
+ });
585
+
586
+ const startTime = Date.now();
587
+ console.log(
588
+ chalk$1.bold.blue('start build your game, it will take a few minutes...'),
589
+ );
590
+
591
+ /**
592
+ * 移除掉当前根目录下的文件包
593
+ */
594
+
595
+ const files = fs$1.readdirSync(process.cwd());
596
+ files.forEach(file => {
597
+ if (file.endsWith('.zip')) {
598
+ fs$1.unlinkSync(path$1.join(process.cwd(), file));
599
+ }
600
+ });
601
+
602
+ await buildMinisManifest();
603
+ const archive = archiver('zip', {
604
+ zlib: { level: 9 }, // Sets the compression level.
605
+ });
606
+
607
+ archive.on('error', function (err) {
608
+ throw err;
609
+ });
610
+
611
+ /**
612
+ * 把当前文件压缩成 game-${dateStr}.zip,并添加在桌面
613
+ */
614
+ const desktopPath = getDesktopPath();
615
+ const zipPath = path$1.join(desktopPath, `${zipNameInput}.zip`);
616
+
617
+ await archive.pipe(fs$1.createWriteStream(zipPath));
618
+
619
+ await archive.directory(path$1.resolve(process.cwd()), false);
620
+
621
+ await archive.finalize();
622
+ console.log(
623
+ chalk$1.yellow.bold(
624
+ `build ${zipNameInput}.zip success, you can find it in desktop, use time ${
625
+ Date.now() - startTime
626
+ } ms`,
627
+ ),
628
+ );
629
+
630
+ process.exit(0);
631
+ } catch (error) {
632
+ console.log(
633
+ chalk$1.red(
634
+ `auto build ${zipNameInput}.zip failed: ${error.message}, you should zip it manually`,
635
+ ),
636
+ );
637
+ process.exit(1);
638
+ }
639
+ };
640
+
641
+ /**
642
+ * 获取当前操作系统的桌面路径,兼容 Windows 和 macOS。
643
+ * @returns {string} 桌面的绝对路径。
644
+ */
645
+ function getDesktopPath() {
646
+ // 1. 获取用户的主目录,这是所有平台通用的基础
647
+ // - Windows: C:\Users\<username>
648
+ // - macOS/Linux: /Users/<username>
649
+ const homeDir = os$1.homedir();
650
+ // 2. 定义可能的桌面文件夹名称列表
651
+ // - 'Desktop' 是英文系统的标准名称
652
+ // - '桌面' 是中文系统的标准名称
653
+ // 可以根据需要添加其他语言,例如 'Bureau' (法语), 'Schreibtisch' (德语) 等
654
+ const desktopNames = ['Desktop', '桌面'];
655
+ // 3. 遍历列表,检查哪个路径真实存在
656
+ for (const name of desktopNames) {
657
+ const possiblePath = path$1.join(homeDir, name);
658
+ // fs.existsSync 会同步检查文件或文件夹是否存在
659
+ if (fs$1.existsSync(possiblePath)) {
660
+ // 找到一个就立即返回,这是最可靠的桌面路径
661
+ return possiblePath;
662
+ }
663
+ }
664
+ // 4. 兜底策略 (Fallback)
665
+ // 如果上面的常见名称都没有找到(例如在一些极简或非标准的系统环境中),
666
+ // 我们就默认返回英文的 'Desktop' 路径。
667
+ // 这是一种安全的默认行为,因为即使文件夹不存在,程序后续创建文件时
668
+ // 也可以选择自动创建这个目录。
669
+ const defaultPath = path$1.join(homeDir, 'Desktop');
670
+ console.log(`未找到特定桌面文件夹,使用默认路径: ${defaultPath}`);
671
+ return defaultPath;
672
+ }
673
+ return bundle;
674
+ }
675
+
676
+ var bundleExports = requireBundle();
677
+ var index = /*@__PURE__*/getDefaultExportFromCjs(bundleExports);
678
+
679
+ class Store {
680
+ constructor(initialState) {
681
+ this.listeners = [];
682
+ this.state = initialState;
683
+ }
684
+ // 获取单例实例
685
+ static getInstance(initialState) {
686
+ if (!Store.instance) {
687
+ Store.instance = new Store(initialState);
688
+ }
689
+ return Store.instance;
690
+ }
691
+ // 获取状态
692
+ getState() {
693
+ return this.state;
694
+ }
695
+ // 设置状态
696
+ setState(newState) {
697
+ this.state = { ...this.state, ...newState };
698
+ this.listeners.forEach(listener => listener(this.state));
699
+ }
700
+ // 订阅状态变化
701
+ subscribe(listener) {
702
+ this.listeners.push(listener);
703
+ return () => {
704
+ this.listeners = this.listeners.filter(l => l !== listener);
705
+ };
706
+ }
707
+ // 重置状态
708
+ reset(newState) {
709
+ this.state = newState;
710
+ this.listeners.forEach(listener => listener(this.state));
711
+ }
712
+ }
713
+ const store = Store.getInstance({
714
+ clientServerPort: '',
715
+ clientServerHost: '',
716
+ clientKey: '',
717
+ packages: {},
718
+ });
719
+
720
+ const DEV_PORT = 9528;
721
+ const DEV_WS_PORT = 9529;
722
+ const OUTPUT_DIR = path.join(os.homedir(), '__TTMG__');
723
+
724
+ function getSessionId({ clientWsPort }) {
725
+ const config = {
726
+ ws_port: DEV_WS_PORT,
727
+ nodeWsPort: DEV_WS_PORT,
728
+ // http_port: DEV_PORT,
729
+ clientWsPort,
730
+ };
731
+ return btoa(JSON.stringify(config));
732
+ }
733
+
734
+ /**
735
+ * 把我的调试相关的数据,生成 base64 字符串
736
+ */
737
+ let server;
738
+ async function createServer() {
739
+ if (server) {
740
+ closeServer();
741
+ }
742
+ const app = express();
743
+ const port = DEV_PORT; // 你可以自定义端口
744
+ const upload = multer({ dest: os.tmpdir() }); // 上传到 uploads 目录
745
+ // 解析 JSON body
746
+ app.use(express.json());
747
+ // 解析 FormData body
748
+ app.use(express.urlencoded({ extended: true }));
749
+ /**
750
+ * TODO: 提供的接口供客户端进行调用,告诉 NodeServer 客户端的 host 和 port
751
+ */
752
+ app.post('/game/env', async (req, res) => {
753
+ // 获取请求体
754
+ const body = req.body;
755
+ const { host, port, wsPort } = body;
756
+ store.setState({
757
+ clientServerPort: port,
758
+ clientServerHost: host,
759
+ });
760
+ // 响应内容(可以根据实际业务返回需要的数据)
761
+ const devUrl = `http://${host}:${port}?session=${getSessionId({ clientWsPort: wsPort })}`;
762
+ openUrl(devUrl);
763
+ console.log(chalk.bold.yellow(`Game debug is ready! Visit ${devUrl} in your browser.`));
764
+ res.json({
765
+ code: 0,
766
+ msg: 'ok',
767
+ data: {
768
+ devUrl,
769
+ },
770
+ });
771
+ });
772
+ app.post('/game/upload', upload.single('file'), async (req, res) => {
773
+ // 文件信息在 req.file
774
+ res.json({
775
+ code: 0,
776
+ msg: 'ok',
777
+ filename: req.file.filename, // multer生成的临时文件名
778
+ originalname: req.file.originalname, // 上传时的原始文件名
779
+ size: req.file.size,
780
+ path: req.file.path,
781
+ });
782
+ });
783
+ // 启动服务
784
+ server = app.listen(port, () => {
785
+ console.log(chalk.cyan.bold(`Node devServer is running on port ${port}`));
786
+ });
787
+ return {
788
+ port,
789
+ host: getLocalIP(),
790
+ };
791
+ }
792
+ function closeServer() {
793
+ server.close(() => {
794
+ console.log('Dev server closed');
795
+ server = null;
796
+ process.exit(0);
797
+ });
798
+ }
799
+
800
+ const CONFIG_DIR = path.join(os.homedir(), '.ttmg-cli');
801
+ path.join(CONFIG_DIR, 'config.json');
802
+ /**
803
+ * 获取 Client Key(选择或输入)
804
+ */
805
+ function getClientKey() {
806
+ /**
807
+ * 读取 project.config.json 中的 appid/appId
808
+ */
809
+ const projectConfigPath = path.join(process.cwd(), 'project.config.json');
810
+ let clientKey;
811
+ try {
812
+ const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf-8'));
813
+ clientKey = projectConfig.appid || projectConfig.appId;
814
+ }
815
+ catch (e) {
816
+ clientKey = '';
817
+ }
818
+ if (clientKey) {
819
+ return clientKey;
820
+ }
821
+ else {
822
+ console.log(chalk.red.bold('No appid found in project.config.json, you should provide it in project.config.json'));
823
+ process.exit(1);
824
+ }
825
+ // const keys = readClientKeys();
826
+ // if (keys.length > 0) {
827
+ // // 有历史 key,展示选择框
828
+ // const { selectedKey } = await inquirer.prompt([
829
+ // {
830
+ // type: 'list',
831
+ // name: 'selectedKey',
832
+ // message: 'Please select your game id(client_key):',
833
+ // choices: [...keys, new inquirer.Separator(), 'Add new game id'],
834
+ // },
835
+ // ]);
836
+ // if (selectedKey === 'Add new game id') {
837
+ // // 输入新 key
838
+ // const { newKey } = await inquirer.prompt([
839
+ // {
840
+ // type: 'input',
841
+ // name: 'newKey',
842
+ // message: 'Please input your new game id(client_key):',
843
+ // validate: (input: string) => (input ? true : 'Please input game id'),
844
+ // },
845
+ // ]);
846
+ // saveClientKey(newKey);
847
+ // clientKey = newKey;
848
+ // } else {
849
+ // clientKey = selectedKey;
850
+ // }
851
+ // } else {
852
+ // // 没有历史 key,让用户输入
853
+ // const { newKey } = await inquirer.prompt([
854
+ // {
855
+ // type: 'input',
856
+ // name: 'newKey',
857
+ // message: 'Please input your game id(client_key):',
858
+ // validate: (input: string) => (input ? true : 'Please input game id'),
859
+ // },
860
+ // ]);
861
+ // saveClientKey(newKey);
862
+ // clientKey = newKey;
863
+ // }
864
+ // return clientKey;
865
+ }
866
+
867
+ function getOutputDir() {
868
+ const clientKey = getClientKey();
869
+ return path.join(os.homedir(), '__TTMG__', clientKey);
870
+ }
871
+
872
+ async function uploadGame() {
873
+ const outputDir = getOutputDir();
874
+ console.log(chalk.yellow.bold('Start compress game resource'));
875
+ const zipPath = path.join(os.homedir(), '__TTMG__', 'upload.zip');
876
+ await zipDirectory(outputDir, zipPath);
877
+ console.log(chalk.green.bold('Compress game package resource success \n'));
878
+ const res = await uploadZip(zipPath);
879
+ return {
880
+ isSuccess: res?.statusCode === 200,
881
+ };
882
+ }
883
+ function zipDirectory(sourceDir, outPath) {
884
+ return new Promise((resolve, reject) => {
885
+ const output = fs.createWriteStream(outPath); // 修正:传入输出路径
886
+ const archive = require$$5$1('zip', { zlib: { level: 9 } });
887
+ output.on('close', () => {
888
+ // console.log('Compress directory done');
889
+ resolve();
890
+ });
891
+ output.on('error', err => {
892
+ // console.error('Compress directory error:', err);
893
+ reject(err);
894
+ });
895
+ archive.on('error', err => {
896
+ reject(err);
897
+ });
898
+ archive.pipe(output);
899
+ archive.directory(sourceDir, false);
900
+ archive.finalize();
901
+ });
902
+ }
903
+ /**
904
+ * 上传 zip 文件到指定接口
905
+ * @param zipFilePath zip 文件路径
906
+ * @param uploadUrl 上传接口
907
+ */
908
+ // export async function uploadZip(zipPath: string) {
909
+ // const form = new FormData();
910
+ // form.append('file', fs.createReadStream(zipPath), 'upload.zip');
911
+ // try {
912
+ // console.log(chalk.yellow.bold('Start upload resource to client'));
913
+ // const { clientServerHost, clientServerPort } = store.getState();
914
+ // const res = await axios.post(
915
+ // `http://${clientServerHost}:${clientServerPort}/game/upload`,
916
+ // form,
917
+ // {
918
+ // headers: form.getHeaders(),
919
+ // },
920
+ // );
921
+ // fs.unlinkSync(zipPath);
922
+ // return res;
923
+ // } catch (err: any) {
924
+ // console.error('Upload resource to client failed:', err.message);
925
+ // }
926
+ // }
927
+ // 使用 require 语法,完全兼容 CommonJS
928
+ async function uploadZip(zipPath) {
929
+ const form = new FormData();
930
+ form.append('file', fs.createReadStream(zipPath), {
931
+ filename: 'upload.zip',
932
+ contentType: 'application/zip',
933
+ });
934
+ console.log(chalk.yellow.bold('Start upload resource to client'));
935
+ const { clientServerHost, clientServerPort } = store.getState();
936
+ const url = `http://${clientServerHost}:${clientServerPort}/game/upload`;
937
+ try {
938
+ // 1. 创建请求流
939
+ const stream = got.stream.post(url, {
940
+ body: form,
941
+ });
942
+ // 2. 监听上传进度 (这个回调是并行的,不影响封装)
943
+ stream.on('uploadProgress', progress => {
944
+ const percent = (progress.percent * 100).toFixed(1);
945
+ const transferred = progress.transferred;
946
+ const total = progress.total;
947
+ process.stdout.write(`\r${chalk.cyan('Uploading progress: ')}${chalk.green(percent + '%')}`);
948
+ // 生成标准的 百分比
949
+ wsServer?.sendUploadStatus('process', {
950
+ progress: `${percent}%`,
951
+ });
952
+ });
953
+ // 3. 【核心封装】将流的处理过程包装在 Promise 中
954
+ const response = await new Promise((resolve, reject) => {
955
+ // 用于拼接响应数据
956
+ const chunks = [];
957
+ // 当流传输数据时,收集数据块
958
+ stream.on('data', chunk => {
959
+ chunks.push(chunk);
960
+ });
961
+ // 当流成功结束时
962
+ stream.on('end', () => {
963
+ // 拼接所有数据块,并转换为字符串
964
+ // const responseBody = Buffer.concat(chunks).toString('utf-8');
965
+ // stream.response 在流结束后才可用
966
+ // 将完整的响应对象 resolve 出去
967
+ resolve({
968
+ statusCode: 200,
969
+ });
970
+ });
971
+ // 当流发生错误时
972
+ stream.on('error', err => {
973
+ // 将错误 reject 出去,这样外层的 try...catch 就能捕获到
974
+ reject(err);
975
+ });
976
+ });
977
+ // 4. 当 await 完成后,说明流已成功结束,可以安全地执行后续操作
978
+ process.stdout.write('\n'); // 换行,保持终端整洁
979
+ console.log(chalk.green.bold('✔ Upload completed successfully!'));
980
+ fs.unlinkSync(zipPath);
981
+ // 5. 返回从 Promise 中解析出的响应数据
982
+ return response;
983
+ }
984
+ catch (err) {
985
+ // 无论是请求错误还是流处理错误,都会在这里被捕获
986
+ process.stdout.write('\n');
987
+ if (err.response) {
988
+ console.error(chalk.red.bold('✖ Upload failed with server error:'), {
989
+ statusCode: err.response.statusCode,
990
+ body: err.response.body,
991
+ });
992
+ }
993
+ else {
994
+ console.error(chalk.red.bold('✖ Upload failed with error:'), err.message);
995
+ }
996
+ }
997
+ }
998
+
999
+ class WsServer {
1000
+ constructor() {
1001
+ this.ws = new WebSocket.Server({ port: DEV_WS_PORT });
1002
+ this.ws.on('connection', ws => {
1003
+ ws.on('message', message => {
1004
+ /** 客户端发送的消息 */
1005
+ const clientMessage = JSON.parse(message.toString());
1006
+ console.log('Client Message', clientMessage);
1007
+ const from = clientMessage.from;
1008
+ if (from === 'browser') {
1009
+ const method = clientMessage.method;
1010
+ switch (method) {
1011
+ case 'connected':
1012
+ this.sendUploadStatus('start');
1013
+ uploadGame()
1014
+ .then(res => {
1015
+ if (res.isSuccess) {
1016
+ this.sendUploadStatus('success', {
1017
+ packages: store.getState().packages,
1018
+ isSuccess: res.isSuccess,
1019
+ });
1020
+ }
1021
+ else {
1022
+ this.sendUploadStatus('error', {
1023
+ errMsg: res.errorMsg,
1024
+ });
1025
+ }
1026
+ })
1027
+ .catch(() => {
1028
+ this.sendUploadStatus('error', {
1029
+ isSuccess: false,
1030
+ });
1031
+ console.log(chalk.red.bold('Start upload resource to client failed!'));
1032
+ });
1033
+ break;
1034
+ case 'closeLocalDebug':
1035
+ console.log('closeLocalDebug');
1036
+ /**
1037
+ * 关闭调试服务
1038
+ */
1039
+ this.ws.close();
1040
+ closeServer();
1041
+ console.log('close server');
1042
+ break;
1043
+ }
1044
+ }
1045
+ else {
1046
+ const method = clientMessage.method;
1047
+ switch (method) {
1048
+ case 'shareDevParams':
1049
+ console.log('shareDevParams', clientMessage);
1050
+ const payload = clientMessage.payload;
1051
+ console.log('shareDevParams', payload);
1052
+ const { host, port, wsPort } = payload;
1053
+ store.setState({
1054
+ clientServerPort: port,
1055
+ clientServerHost: host,
1056
+ });
1057
+ // 响应内容(可以根据实际业务返回需要的数据)
1058
+ const devUrl = `http://${host}:${port}?session=${getSessionId({ clientWsPort: wsPort })}`;
1059
+ openUrl(devUrl);
1060
+ console.log(chalk.bold.yellow(`Game debug is ready! Visit ${devUrl} in your browser.`));
1061
+ break;
1062
+ // 鉴权失败
1063
+ case 'checkPermissionFailed':
1064
+ // 提醒使用客户端先完成测试用户授权,再重新扫描二维码开启调试服务
1065
+ console.log(chalk.red.bold('Check permission failed! Please authorize in client first.'));
1066
+ break;
1067
+ }
1068
+ }
1069
+ });
1070
+ });
1071
+ }
1072
+ send(params) {
1073
+ this.ws.clients.forEach(client => {
1074
+ if (client.readyState === WebSocket.OPEN) {
1075
+ const data = {
1076
+ ...params,
1077
+ from: 'nodeServer',
1078
+ };
1079
+ client.send(JSON.stringify(data));
1080
+ }
1081
+ });
1082
+ }
1083
+ close() {
1084
+ this.ws.close();
1085
+ }
1086
+ sendUploadStatus(status, payload) {
1087
+ this.send({
1088
+ method: 'uploadStatus',
1089
+ status,
1090
+ payload,
1091
+ });
1092
+ }
1093
+ }
1094
+ const wsServer = new WsServer();
1095
+
1096
+ async function watchChange() {
1097
+ let debounceTimer = null;
1098
+ fs.watch(process.cwd(), (eventType, filename) => {
1099
+ console.log(chalk.yellow('game resource change, restart to upload'));
1100
+ // 清除之前的定时器
1101
+ if (debounceTimer)
1102
+ clearTimeout(debounceTimer);
1103
+ // 重新设置定时器
1104
+ debounceTimer = setTimeout(async () => {
1105
+ wsServer.sendUploadStatus('start');
1106
+ uploadGame()
1107
+ .then((res) => {
1108
+ if (res.isSuccess) {
1109
+ wsServer.sendUploadStatus('success');
1110
+ }
1111
+ else {
1112
+ wsServer.sendUploadStatus('error', {
1113
+ errMsg: res.errorMsg,
1114
+ });
1115
+ }
1116
+ })
1117
+ .catch(() => {
1118
+ wsServer.sendUploadStatus('error');
1119
+ });
1120
+ debounceTimer = null;
1121
+ }, 500); // 500ms内只执行一次
1122
+ });
1123
+ }
1124
+
1125
+ async function showSchema() {
1126
+ const clientKey = getClientKey();
1127
+ const outputDir = path.join(OUTPUT_DIR, clientKey);
1128
+ const localIP = getLocalIP();
1129
+ console.log(chalk.green.bold('Tips:'));
1130
+ console.log(` 1. ${chalk.yellow.bold('Scan the QR code to start the client devServer.')}`);
1131
+ console.log(` 2. ${chalk.yellow.bold('Will auto upload compiled resource to client.')} ${chalk.bold(outputDir)}`);
1132
+ console.log(` 3. ${chalk.yellow.bold('Debug your game in the browser.')}\n`);
1133
+ const schema = `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${localIP}&port=${DEV_WS_PORT}`;
1134
+ qrcode.generate(schema, {
1135
+ small: true,
1136
+ }, qr => {
1137
+ console.log(qr);
1138
+ });
1139
+ }
1140
+
1141
+ async function prepareResource() {
1142
+ console.log(chalk.yellow.bold('Start compile game for debug'));
1143
+ const entryDir = process.cwd();
1144
+ const outputDir = getOutputDir();
1145
+ if (!fs.existsSync(outputDir)) {
1146
+ fs.mkdirSync(outputDir, { recursive: true });
1147
+ }
1148
+ const { isSuccess, errorMsg, packages } = await ttmgPack.debugPkgs({
1149
+ entry: entryDir,
1150
+ output: outputDir,
1151
+ });
1152
+ if (!isSuccess) {
1153
+ console.log(chalk.redBright('Build game package failed, Please check the error message below:'));
1154
+ console.log(chalk.redBright(errorMsg));
1155
+ process.exit(1);
1156
+ }
1157
+ else {
1158
+ store.setState({
1159
+ packages,
1160
+ });
1161
+ console.log(chalk.green.bold('Compile game package success \n'));
1162
+ }
1163
+ }
1164
+
1165
+ async function dev() {
1166
+ /**
1167
+ * 1. 准备游戏资源
1168
+ */
1169
+ await prepareResource();
1170
+ /**
1171
+ * 2. 创建本地调试服务
1172
+ */
1173
+ await createServer();
1174
+ /**
1175
+ * 3. 显示调试二维码信息,扫码启动客户端调试服务
1176
+ */
1177
+ await showSchema();
1178
+ /**
1179
+ * 4. 监听游戏资源变化
1180
+ */
1181
+ await watchChange();
1182
+ }
1183
+
1184
+ var version = "0.1.0-beta.9";
1185
+ var pkg = {
1186
+ version: version};
1187
+
1188
+ const program = new commander.Command();
1189
+ program
1190
+ .name('ttmg')
1191
+ .description('TikTok Mini Games Command Line Tool')
1192
+ .version(pkg.version, '-v, --version', '显示版本号')
1193
+ .option('dev', 'Debug TikTok Mini Games for Client')
1194
+ .option('dev --h5', 'Debug TikTok Mini Games for Web');
1195
+ program
1196
+ .option('--h5', 'H5 Mini Game')
1197
+ .command('init')
1198
+ .description('Initialize project')
1199
+ .action(() => {
1200
+ const options = program.opts(); // 获取 options
1201
+ if (options.h5) {
1202
+ index$2();
1203
+ }
1204
+ else {
1205
+ console.log('Native Mini Game initialize');
1206
+ }
1207
+ });
1208
+ /**
1209
+ * ttmg dev 命令
1210
+ */
1211
+ program
1212
+ .option('--h5', 'H5 Mini Game')
1213
+ .command('dev')
1214
+ .description('Open browser dev environment')
1215
+ .action(() => {
1216
+ const options = program.opts();
1217
+ if (options.h5) {
1218
+ index$1();
1219
+ }
1220
+ else {
1221
+ dev();
1222
+ }
1223
+ });
1224
+ /**
1225
+ * ttmg build 命令
1226
+ */
1227
+ program
1228
+ .option('--h5', 'H5 Mini Game')
1229
+ .command('build')
1230
+ .description('Bundle project')
1231
+ .action(() => {
1232
+ const options = program.opts(); // 获取 options
1233
+ if (options.h5) {
1234
+ index();
1235
+ }
1236
+ else {
1237
+ console.log('Native Mini Game bundle');
1238
+ }
1239
+ });
1240
+ program
1241
+ .command('login')
1242
+ .description('User Dev Portal Account to Login')
1243
+ .action(async () => {
1244
+ console.log('will support soon');
1245
+ });
1246
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -29,10 +29,12 @@
29
29
  "chalk": "^4.1.2",
30
30
  "cheerio": "^1.1.1",
31
31
  "chrome-launcher": "^1.2.0",
32
+ "chrome-remote-interface": "^0.33.3",
32
33
  "commander": "^11.1.0",
33
34
  "estraverse": "^5.3.0",
34
35
  "express": "^5.1.0",
35
36
  "form-data": "^4.0.4",
37
+ "got": "^11.8.5",
36
38
  "inquirer": "^12.7.0",
37
39
  "jsdom": "^26.1.0",
38
40
  "multer": "^2.0.2",