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.
@@ -8,11 +8,16 @@
8
8
  "type": "pwa-node",
9
9
  "request": "launch",
10
10
  "name": "Launch Program",
11
- "skipFiles": ["<node_internals>/**"],
11
+ "skipFiles": [
12
+ "<node_internals>/**"
13
+ ],
12
14
  "program": "${workspaceFolder}/index.js",
13
- "args": ["sync-config", "--root-element", "#__next", "out/index.html"],
14
- "cwd": "${workspaceFolder}/../next-mini-app/",
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, --dev', 'Development environment')
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.0",
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.6.2",
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
- const server = await createServer({
106
- configFile: path.join(cwd, viteConfig),
107
- root: cwd,
108
- mode,
109
- define: {
110
- 'process.env.NODE_ENV': JSON.stringify(mode),
111
- 'process.env.previewOnZalo': previewOnZalo,
112
- },
113
- server: {
114
- port: usingFrame ? port - 1 : port,
115
- ...(previewOnZalo ? publicServer : localServer),
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 (usingFrame) {
122
- //run frame server
123
- const serverFrame = await createServer({
124
- // any valid user config options, plus `mode` and `configFile`
125
- configFile: false,
126
- root: __dirname + '/frame',
127
- server: {
128
- port: app.httpServer.address().port + 1,
129
- strictPort: true,
130
- open: true,
131
- },
132
- });
133
- spinner.stop();
134
- await serverFrame.listen();
135
- const info = serverFrame.config.logger.info;
136
- info(chalk.green(`Zalo Mini App dev server is running at:\n`));
137
- serverFrame.printUrls();
138
- } else {
139
- spinner.stop();
140
- const info = server.config.logger.info;
141
- info(chalk.green(`Zalo Mini App dev server is running at:\n`));
142
- server.printUrls();
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');
@@ -9,7 +9,12 @@ const projectFramework = {
9
9
  VUE: 'vue',
10
10
  };
11
11
 
12
+ const deviceModeSetup = {
13
+ TUNNEL_SERVER_HOST: 'mini.123c.vn',
14
+ };
15
+
12
16
  module.exports = {
13
17
  versionStatus,
14
18
  projectFramework,
19
+ deviceModeSetup,
15
20
  };
@@ -0,0 +1,11 @@
1
+ const net = require('net');
2
+
3
+ module.exports = async function findFreePort() {
4
+ return new Promise((res) => {
5
+ const srv = net.createServer();
6
+ srv.listen(0, () => {
7
+ const port = srv.address().port;
8
+ srv.close(() => res(port));
9
+ });
10
+ });
11
+ };
@@ -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
+ };