zmp-cli 3.15.0 → 3.15.1
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/.vscode/launch.json +9 -4
- package/index.js +2 -1
- package/package.json +4 -2
- package/start/generate-hr-config.js +60 -0
- package/start/index.js +182 -38
- package/utils/constants.js +5 -0
- package/utils/find-free-port.js +11 -0
- package/utils/tunnel.js +36 -0
package/.vscode/launch.json
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
"type": "pwa-node",
|
|
9
9
|
"request": "launch",
|
|
10
10
|
"name": "Launch Program",
|
|
11
|
-
"skipFiles": [
|
|
11
|
+
"skipFiles": [
|
|
12
|
+
"<node_internals>/**"
|
|
13
|
+
],
|
|
12
14
|
"program": "${workspaceFolder}/index.js",
|
|
13
|
-
"args": [
|
|
14
|
-
|
|
15
|
+
"args": [
|
|
16
|
+
"start",
|
|
17
|
+
"--device"
|
|
18
|
+
],
|
|
19
|
+
"cwd": "${workspaceFolder}/../zaui-coffee/",
|
|
15
20
|
"console": "integratedTerminal"
|
|
16
21
|
}
|
|
17
22
|
]
|
|
18
|
-
}
|
|
23
|
+
}
|
package/index.js
CHANGED
|
@@ -120,7 +120,7 @@ program
|
|
|
120
120
|
.option('-Z, --zalo-app', 'Preview on Zalo')
|
|
121
121
|
.option('-ios, --ios', 'Run on ios')
|
|
122
122
|
.option('-nF, --no-frame', 'Run without Zalo frame')
|
|
123
|
-
.option('-D, --
|
|
123
|
+
.option('-D, --device', 'Device mode')
|
|
124
124
|
.option('-M, --mode <m>', 'Env mode')
|
|
125
125
|
.description('Start a ZMP project')
|
|
126
126
|
.action(async (options) => {
|
|
@@ -161,6 +161,7 @@ program
|
|
|
161
161
|
(typeof options.frame === 'undefined' || options.frame === null)
|
|
162
162
|
? true
|
|
163
163
|
: options.frame,
|
|
164
|
+
deviceMode: (options && options.device) || false,
|
|
164
165
|
},
|
|
165
166
|
logger
|
|
166
167
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zmp-cli",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.1",
|
|
4
4
|
"description": "ZMP command line utility (CLI)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"body-parser": "^1.19.0",
|
|
47
47
|
"browser-sync": "^2.26.14",
|
|
48
48
|
"chalk": "^4.1.2",
|
|
49
|
-
"chii": "^1.
|
|
49
|
+
"chii": "^1.9.0",
|
|
50
50
|
"clear": "^0.1.0",
|
|
51
51
|
"cli-progress": "^3.9.0",
|
|
52
52
|
"clui": "^0.3.6",
|
|
@@ -65,8 +65,10 @@
|
|
|
65
65
|
"form-data": "^4.0.0",
|
|
66
66
|
"fs": "0.0.1-security",
|
|
67
67
|
"html-webpack-plugin": "^5.1.0",
|
|
68
|
+
"human-readable-ids": "^1.0.4",
|
|
68
69
|
"inquirer": "^7.3.3",
|
|
69
70
|
"jsonwebtoken": "^8.5.1",
|
|
71
|
+
"localtunnel": "^2.0.2",
|
|
70
72
|
"lodash": "^4.17.20",
|
|
71
73
|
"log-symbols": "^3.0.0",
|
|
72
74
|
"mime": "2.3.0",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { default: axios } = require('axios');
|
|
2
|
+
const DomParser = require('dom-parser');
|
|
3
|
+
|
|
4
|
+
async function generateHrFromIndex(url, bags = {}) {
|
|
5
|
+
const listCSS = [];
|
|
6
|
+
const listJS = [];
|
|
7
|
+
try {
|
|
8
|
+
const response = await axios({
|
|
9
|
+
url,
|
|
10
|
+
headers: {
|
|
11
|
+
Accept: 'text/html,application/xhtml+xml,application/xml',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
const html = response.data;
|
|
15
|
+
const parser = new DomParser();
|
|
16
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
17
|
+
|
|
18
|
+
const scripts = doc.getElementsByTagName('script');
|
|
19
|
+
scripts.forEach((script) => {
|
|
20
|
+
const src = script.getAttribute('src');
|
|
21
|
+
const type = script.getAttribute('type');
|
|
22
|
+
const skip = script.getAttribute('data-skip-hr-config');
|
|
23
|
+
const inline = script.innerHTML;
|
|
24
|
+
if (skip === null) {
|
|
25
|
+
listJS.push({
|
|
26
|
+
src,
|
|
27
|
+
type,
|
|
28
|
+
innerHTML: inline
|
|
29
|
+
? inline
|
|
30
|
+
.replaceAll('from "/', `from "${bags.exposedUrl}/`)
|
|
31
|
+
.replaceAll("from '/", `from '${bags.exposedUrl}/`)
|
|
32
|
+
: undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// override Vite HMR "full-reload" behavior
|
|
37
|
+
listJS.push({
|
|
38
|
+
src: `/studio.module.js`,
|
|
39
|
+
type: 'module',
|
|
40
|
+
});
|
|
41
|
+
listJS.push({
|
|
42
|
+
id: 'remote-debug-script',
|
|
43
|
+
innerHTML: `(function () {
|
|
44
|
+
var script = document.createElement('script');
|
|
45
|
+
script.src='${bags.chiiUrl}/target.js';
|
|
46
|
+
document.head.appendChild(script);
|
|
47
|
+
window.BACKUP_URL = window.location.href;
|
|
48
|
+
window.history.pushState(null, null, '/' + window.location.search);
|
|
49
|
+
})()`,
|
|
50
|
+
type: 'text/javascript',
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn(error);
|
|
54
|
+
}
|
|
55
|
+
return { listCSS, listJS };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
generateHrFromIndex,
|
|
60
|
+
};
|
package/start/index.js
CHANGED
|
@@ -7,11 +7,16 @@ const qrcode = require('qrcode-terminal');
|
|
|
7
7
|
const logSymbols = require('log-symbols');
|
|
8
8
|
const { createServer } = require('vite');
|
|
9
9
|
const chii = require('chii');
|
|
10
|
+
const { hri } = require('human-readable-ids');
|
|
10
11
|
|
|
11
12
|
const config = require('../config');
|
|
12
13
|
const envUtils = require('../utils/env');
|
|
13
14
|
const fs = require('../utils/fs-extra');
|
|
14
15
|
const fse = require('../utils/fs-extra');
|
|
16
|
+
const findFreePort = require('../utils/find-free-port');
|
|
17
|
+
const { openTunnel } = require('../utils/tunnel');
|
|
18
|
+
const { deviceModeSetup } = require('../utils/constants');
|
|
19
|
+
const { generateHrFromIndex } = require('./generate-hr-config');
|
|
15
20
|
|
|
16
21
|
const spinner = ora('Starting mini app...');
|
|
17
22
|
|
|
@@ -41,6 +46,7 @@ module.exports = async (options = {}, logger, { exitOnError = true } = {}) => {
|
|
|
41
46
|
const port = options.port;
|
|
42
47
|
const remoteDebugPort = port - 2;
|
|
43
48
|
const mode = options.mode || (previewOnZalo ? 'production' : 'development');
|
|
49
|
+
const deviceMode = options.deviceMode;
|
|
44
50
|
try {
|
|
45
51
|
if (previewOnZalo) {
|
|
46
52
|
const hrConfigPath = path.join(cwd, 'hr.config.json');
|
|
@@ -102,44 +108,165 @@ module.exports = async (options = {}, logger, { exitOnError = true } = {}) => {
|
|
|
102
108
|
viteConfig = 'vite.config.ts';
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
port:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
let server;
|
|
112
|
+
let subdomain;
|
|
113
|
+
let freePort;
|
|
114
|
+
if (deviceMode) {
|
|
115
|
+
// generate a subdomain for the tunnel
|
|
116
|
+
subdomain = hri.random();
|
|
117
|
+
// start the chii server
|
|
118
|
+
freePort = await findFreePort();
|
|
119
|
+
chii.start({
|
|
120
|
+
port: freePort,
|
|
121
|
+
});
|
|
122
|
+
// expose the chii server by using localtunnel
|
|
123
|
+
const chiiUrl = await openTunnel(freePort);
|
|
124
|
+
const remoteHost = `${subdomain}.${deviceModeSetup.TUNNEL_SERVER_HOST}`;
|
|
125
|
+
|
|
126
|
+
server = await createServer({
|
|
127
|
+
configFile: path.join(cwd, viteConfig),
|
|
128
|
+
root: cwd,
|
|
129
|
+
mode,
|
|
130
|
+
define: {
|
|
131
|
+
'process.env.NODE_ENV': JSON.stringify(mode),
|
|
132
|
+
'process.env.previewOnZalo': true,
|
|
133
|
+
},
|
|
134
|
+
server: {
|
|
135
|
+
port: port,
|
|
136
|
+
host: 'localhost',
|
|
137
|
+
origin: `https://${remoteHost}`,
|
|
138
|
+
base: `/zapps/${appId}/`,
|
|
139
|
+
hmr: {
|
|
140
|
+
host: remoteHost,
|
|
141
|
+
clientPort: 443,
|
|
142
|
+
protocol: 'wss',
|
|
143
|
+
},
|
|
144
|
+
https: false,
|
|
145
|
+
},
|
|
146
|
+
plugins: [
|
|
147
|
+
{
|
|
148
|
+
name: 'hr-config-plugin',
|
|
149
|
+
configureServer(server) {
|
|
150
|
+
const setupCors = (req, res) => {
|
|
151
|
+
if (
|
|
152
|
+
['https://h5.zdn.vn', 'zbrowser://h5.zdn.vn'].includes(
|
|
153
|
+
req.headers.origin
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
res.setHeader(
|
|
157
|
+
'Access-Control-Allow-Origin',
|
|
158
|
+
req.headers.origin
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
server.middlewares.use('/app-config.json', (req, res, next) => {
|
|
163
|
+
// there would be another originalUrl - app-config.json?module that would be dynamicly imported in js format
|
|
164
|
+
if (req.originalUrl === '/app-config.json') {
|
|
165
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
166
|
+
setupCors(req, res);
|
|
167
|
+
return res.end(
|
|
168
|
+
fs.readFileSync(path.join(cwd, 'app-config.json'))
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
return next();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
server.middlewares.use('/hr.config.json', async (req, res) => {
|
|
175
|
+
res.setHeader('Content-Type', 'application/json');
|
|
176
|
+
setupCors(req, res);
|
|
177
|
+
const hrConfig = await generateHrFromIndex(
|
|
178
|
+
`http://localhost:${app.httpServer.address().port}`,
|
|
179
|
+
{
|
|
180
|
+
chiiUrl,
|
|
181
|
+
exposedUrl: `https://${subdomain}.${deviceModeSetup.TUNNEL_SERVER_HOST}`,
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
res.end(JSON.stringify(hrConfig));
|
|
185
|
+
});
|
|
186
|
+
server.middlewares.use('/studio.module.js', async (req, res) => {
|
|
187
|
+
res.setHeader('Content-Type', 'text/javascript');
|
|
188
|
+
setupCors(req, res);
|
|
189
|
+
res.end(`
|
|
190
|
+
import { createHotContext } from "/@vite/client";
|
|
191
|
+
import.meta.hot = createHotContext("${subdomain}");
|
|
192
|
+
if (import.meta.hot) {
|
|
193
|
+
import.meta.hot.on("vite:beforeFullReload", () => {
|
|
194
|
+
window.location.href = window.BACKUP_URL;
|
|
195
|
+
ZJSBridge.callCustomAction("action.ma.menu.reload", {}, () => {});
|
|
196
|
+
throw "(skipping full reload)";
|
|
197
|
+
});
|
|
198
|
+
import.meta.hot.on('zmp:close', (data) => {
|
|
199
|
+
ZJSBridge.callCustomAction("action.window.close", {}, () => {});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
let shownTip = false;
|
|
204
|
+
window.onerror = function (message, url, line, column, error) {
|
|
205
|
+
if (!error) {
|
|
206
|
+
console.error(
|
|
207
|
+
"An uncaught error has occurred, but the detailed error message cannot be displayed due to the cross-origin policy."
|
|
208
|
+
);
|
|
209
|
+
if (!shownTip) {
|
|
210
|
+
console.warn(
|
|
211
|
+
"To view the detailed error message, you can try one of the following methods:\\n\\n1. Wrap your code in a try-catch block, use .catch with Promises, or use <ErrorBoundary> in React to catch the detailed error object.\\n2. Try to reproduce the issue using a simulator."
|
|
212
|
+
);
|
|
213
|
+
shownTip = true;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
console.error("[" + error.name + "] " + error.message);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(error);
|
|
221
|
+
}
|
|
222
|
+
`);
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
server = await createServer({
|
|
230
|
+
configFile: path.join(cwd, viteConfig),
|
|
231
|
+
root: cwd,
|
|
232
|
+
mode,
|
|
233
|
+
define: {
|
|
234
|
+
'process.env.NODE_ENV': JSON.stringify(mode),
|
|
235
|
+
'process.env.previewOnZalo': previewOnZalo,
|
|
236
|
+
},
|
|
237
|
+
server: {
|
|
238
|
+
port: usingFrame ? port - 1 : port,
|
|
239
|
+
...(previewOnZalo ? publicServer : localServer),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
118
243
|
const app = await server.listen();
|
|
119
244
|
|
|
120
245
|
if (!previewOnZalo) {
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
246
|
+
if (!deviceMode) {
|
|
247
|
+
if (usingFrame) {
|
|
248
|
+
//run frame server
|
|
249
|
+
const serverFrame = await createServer({
|
|
250
|
+
// any valid user config options, plus `mode` and `configFile`
|
|
251
|
+
configFile: false,
|
|
252
|
+
root: __dirname + '/frame',
|
|
253
|
+
server: {
|
|
254
|
+
port: app.httpServer.address().port + 1,
|
|
255
|
+
strictPort: true,
|
|
256
|
+
open: true,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
spinner.stop();
|
|
260
|
+
await serverFrame.listen();
|
|
261
|
+
const info = serverFrame.config.logger.info;
|
|
262
|
+
info(chalk.green(`Zalo Mini App dev server is running at:\n`));
|
|
263
|
+
serverFrame.printUrls();
|
|
264
|
+
} else {
|
|
265
|
+
spinner.stop();
|
|
266
|
+
const info = server.config.logger.info;
|
|
267
|
+
info(chalk.green(`Zalo Mini App dev server is running at:\n`));
|
|
268
|
+
server.printUrls();
|
|
269
|
+
}
|
|
143
270
|
}
|
|
144
271
|
} else {
|
|
145
272
|
try {
|
|
@@ -171,10 +298,9 @@ module.exports = async (options = {}, logger, { exitOnError = true } = {}) => {
|
|
|
171
298
|
}
|
|
172
299
|
|
|
173
300
|
spinner.stop();
|
|
174
|
-
return await new Promise(() => {
|
|
175
|
-
const previewOnZaloURL = `https://zalo.me/app/link/zapps/${appId}/?env=TESTING_LOCAL&clientIp=http://${host}:${app.config.server.port}`;
|
|
176
|
-
|
|
301
|
+
return await new Promise(async () => {
|
|
177
302
|
if (previewOnZalo) {
|
|
303
|
+
const previewOnZaloURL = `https://zalo.me/app/link/zapps/${appId}/?env=TESTING_LOCAL&clientIp=http://${host}:${app.config.server.port}`;
|
|
178
304
|
qrcode.generate(previewOnZaloURL, { small: true }, function (qrcode) {
|
|
179
305
|
logger.text(
|
|
180
306
|
chalk.green(
|
|
@@ -227,6 +353,24 @@ module.exports = async (options = {}, logger, { exitOnError = true } = {}) => {
|
|
|
227
353
|
}
|
|
228
354
|
});
|
|
229
355
|
}
|
|
356
|
+
if (deviceMode) {
|
|
357
|
+
const port = app.httpServer.address().port;
|
|
358
|
+
const remoteUrl = await openTunnel(port, subdomain);
|
|
359
|
+
const deviceModeUrl = `https://zalo.me/app/link/zapps/${appId}/?env=TESTING_LOCAL&clientIp=${remoteUrl}`;
|
|
360
|
+
qrcode.generate(deviceModeUrl, { small: true }, function (qrcode) {
|
|
361
|
+
const qrCode = `${logSymbols.info} ${chalk.bold(
|
|
362
|
+
`Scan the QR code with Zalo app:\n${qrcode}`
|
|
363
|
+
)}`;
|
|
364
|
+
logger.text(qrCode);
|
|
365
|
+
logger.text(
|
|
366
|
+
`${logSymbols.info} ${chalk.bold(
|
|
367
|
+
`To inspect your app, open: http://localhost:${freePort} in ${chalk.green(
|
|
368
|
+
`Google Chrome`
|
|
369
|
+
)} or a ${chalk.blue(`Chromium-based`)} browser.`
|
|
370
|
+
)}`
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
230
374
|
});
|
|
231
375
|
} catch (err) {
|
|
232
376
|
logger.statusError('Error starting project');
|
package/utils/constants.js
CHANGED
package/utils/tunnel.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const localtunnel = require('localtunnel');
|
|
2
|
+
const { deviceModeSetup } = require('./constants');
|
|
3
|
+
|
|
4
|
+
const openedTunnels = {};
|
|
5
|
+
|
|
6
|
+
async function openTunnel(port, subdomain) {
|
|
7
|
+
const tunnel = await localtunnel({
|
|
8
|
+
port,
|
|
9
|
+
host: `https://${deviceModeSetup.TUNNEL_SERVER_HOST}`,
|
|
10
|
+
subdomain,
|
|
11
|
+
});
|
|
12
|
+
openedTunnels[tunnel.url] = tunnel;
|
|
13
|
+
return tunnel.url;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function closeTunnel(url) {
|
|
17
|
+
const tunnel = openedTunnels[url];
|
|
18
|
+
if (tunnel) {
|
|
19
|
+
tunnel.close();
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function closeAllTunnels() {
|
|
26
|
+
const res = await Promise.all(
|
|
27
|
+
Object.keys(openedTunnels).map((key) => closeTunnel(key))
|
|
28
|
+
);
|
|
29
|
+
return res.reduce((total, status) => total + (status ? 1 : 0), 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
openTunnel,
|
|
34
|
+
closeTunnel,
|
|
35
|
+
closeAllTunnels,
|
|
36
|
+
};
|