UNPKG

26.6 kBJavaScriptView Raw
1const fs = require('fs');
2const fse = require('fs-extra');
3const path = require('path');
4const http = require('http');
5const net = require('net');
6const uuid = require('uuid');
7const chalk = require('chalk');
8const Gauge = require('gauge');
9const crypto = require('crypto');
10const chokidar = require('chokidar');
11const notifier = require('node-notifier');
12const ipv4 = require('internal-ip').v4.sync();
13const child_process = require('child_process');
14const utils = require('../utils');
15const builder = require('./builder');
16const config = require('../../config');
17const ansiHtml = require('./ansiHtml');
18const mine = require('../utils/mine').types;
19
20let socketAlready = false;
21let socketTimeout = null;
22let socketClients = [];
23let fileMd5Lists = {};
24
25module.exports = {
26 /**
27 * 获取未使用接口
28 * @param port
29 * @param callback
30 */
31 portIsOccupied(port, callback) {
32 const server = net.createServer().listen(port);
33 server.on('listening', () => {
34 server.close();
35 callback(null, port);
36 });
37 server.on('error', (err) => {
38 if (err.code === 'EADDRINUSE') {
39 this.portIsOccupied(port + 1, callback);
40 } else {
41 callback(err)
42 }
43 });
44 },
45
46 /**
47 * 获取首页url
48 * @param dirName
49 * @returns {string}
50 */
51 getHostIndexUrl(dirName) {
52 let indexName = 'index.js';
53 let homePage = utils.getObject(require(path.resolve('eeui.config')), 'homePage').trim();
54 if (utils.count(homePage) > 0) {
55 if (utils.leftExists(homePage, "http://") || utils.leftExists(homePage, "https://") || utils.leftExists(homePage, "ftp://") || utils.leftExists(homePage, "file://")) {
56 return homePage;
57 }
58 let lastUrl = homePage.substring(homePage.lastIndexOf("/"), homePage.length);
59 if (!utils.strExists(lastUrl, ".")) {
60 homePage += ".js";
61 }
62 indexName = homePage;
63 }
64 return dirName + "/" + indexName;
65 },
66
67 /**
68 * 格式化url参数
69 * @param url
70 * @returns {{}}
71 */
72 urlParamets(url) {
73 let arr;
74 if (utils.strExists(url, "?")) {
75 arr = utils.getMiddle(url, "?", null).split("&");
76 }else{
77 arr = utils.getMiddle(url, "#", null).split("&");
78 }
79 let params = {};
80 for (let i = 0; i < arr.length; i++) {
81 let data = arr[i].split("=");
82 if (data.length === 2) {
83 params[data[0]] = data[1];
84 }
85 }
86 return params;
87 },
88
89 /**
90 * 创建网络访问服务
91 * @param contentBase
92 * @param port
93 */
94 createServer(contentBase, port) {
95 http.createServer((req, res) => {
96 let url = req.url;
97 let file = contentBase + url.split('?').shift();
98 let params = this.urlParamets(url);
99 let suffixName = file.split('.').pop();
100 let stats = utils.pathType(file);
101 switch (stats) {
102 case 1:
103 res.writeHead(200, {'content-type': (mine[suffixName] || "text/plain; charset=utf-8")});
104 if (params.preload === 'preload') {
105 res.write(JSON.stringify({
106 'appboards': utils.getAllAppboards(config.sourceDir),
107 'body': fs.readFileSync(file, 'utf8'),
108 }));
109 res.end();
110 } else {
111 fs.createReadStream(file).pipe(res);
112 }
113 break;
114
115 case 2:
116 this.errorServer(res, 405);
117 break;
118
119 default:
120 this.errorServer(res, 404);
121 break;
122 }
123 }).listen(port);
124 },
125
126 /**
127 * 生成错误js
128 * @param res
129 * @param errorCode
130 * @param errorMsg
131 * @returns {string}
132 */
133 errorServer(res, errorCode, errorMsg) {
134 if (res === true) {
135 let data = fs.readFileSync(path.resolve(__dirname, 'error.js'), 'utf8');
136 data += "";
137 if (errorCode) {
138 data = data.replace('你访问的页面出错了!', '你访问的页面出错了! (' + errorCode + ')')
139 }
140 if (errorMsg) {
141 data = data.replace('var errorMsg=decodeURIComponent("");', 'var errorMsg=decodeURIComponent("' + encodeURIComponent(errorMsg.replace(new RegExp(path.resolve(__dirname, '../../'), 'g'), '')) + '");')
142 }
143 return data;
144 }
145 fs.readFile(path.resolve(__dirname, 'error.js'), (err, data) => {
146 if (err) {
147 res.writeHead(404, { 'content-type': 'text/html' });
148 res.write('<h1>404错误</h1><p>你要找的页面不存在</p>');
149 res.end();
150 } else {
151 data += "";
152 if (errorCode) {
153 data = data.replace('你访问的页面出错了!', '你访问的页面出错了! (' + errorCode + ')')
154 }
155 if (errorMsg) {
156 data = data.replace('var errorMsg=decodeURIComponent("");', 'var errorMsg=decodeURIComponent("' + encodeURIComponent(errorMsg.replace(new RegExp(path.resolve(__dirname, '../../'), 'g'), '')) + '");')
157 }
158 res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
159 res.write(data);
160 res.end();
161 }
162 });
163 },
164
165 /**
166 * 完整代码
167 * @param assetsByChunkName
168 */
169 completeCode(assetsByChunkName) {
170 utils.each(assetsByChunkName, (key, value) => {
171 let assetPath = path.resolve(config.distDir, config.sourcePagesDir, value);
172 let assetContent = fs.readFileSync(assetPath, 'utf8');
173 let isEdit = false;
174 if (!/^\/\/\s*{\s*"framework"\s*:\s*"Vue"\s*}/.exec(assetContent)) {
175 assetContent = `// { "framework": "Vue"} \n` + assetContent;
176 isEdit = true;
177 }
178 if (/((\s|{|\[|\(|,|;)console)\.(debug|log|info|warn|error)\((.*?)\)/.exec(assetContent)) {
179 assetContent = utils.replaceEeuiLog(assetContent);
180 isEdit = true;
181 }
182 if (/\.(requireModule|isRegisteredModule)\((['"])(eeui\/.*)\2\)/.exec(assetContent)) {
183 assetContent = utils.replaceModule(assetContent);
184 isEdit = true;
185 }
186 if (isEdit) {
187 fs.writeFileSync(assetPath, assetContent);
188 }
189 });
190 },
191
192 /**
193 * 复制其他文件
194 * @param originDir
195 * @param newDir
196 * @param containAppboardDir
197 */
198 copyOtherFile(originDir, newDir, containAppboardDir) {
199 let lists = fs.readdirSync(originDir);
200 let appboardDir = path.resolve(config.sourceDir, 'appboard');
201 lists.some((item) => {
202 if (!utils.execPath(item)) {
203 return false;
204 }
205 let originPath = path.resolve(originDir, item);
206 let newPath = path.resolve(newDir, item);
207 let stats = utils.pathType(originPath);
208 if (stats === 1) {
209 if (utils.leftExists(originPath, appboardDir)) {
210 if (containAppboardDir === true) {
211 let originContent = fs.readFileSync(originPath, 'utf8');
212 fse.outputFileSync(newPath, utils.replaceModule(utils.replaceEeuiLog(originContent)));
213 }
214 } else {
215 fse.copySync(originPath, newPath);
216 }
217 } else if (stats === 2) {
218 this.copyOtherFile(originPath, newPath, containAppboardDir)
219 }
220 });
221 },
222
223 /**
224 * 复制文件(md5判断文件不一致才复制)
225 * @param originPath
226 * @param newPath
227 * @param callback
228 */
229 copyFileMd5(originPath, newPath, callback) {
230 let stream = fs.createReadStream(originPath);
231 let md5sum = crypto.createHash('md5');
232 stream.on('data', (chunk) => {
233 md5sum.update(chunk);
234 });
235 stream.on('end', () => {
236 let str = md5sum.digest("hex").toUpperCase();
237 if (fileMd5Lists[newPath] !== str) {
238 fileMd5Lists[newPath] = str;
239 fse.copy(originPath, newPath, callback);
240 }
241 });
242 },
243
244 /**
245 * babel处理appboard内文件
246 * @param task
247 */
248 appboardGulpBabel(task) {
249 let gulpBin = path.resolve('node_modules/gulp/bin/gulp.js');
250 let gulpFile = path.resolve('gulpfile.js');
251 if (!fs.existsSync(gulpBin) || !fs.existsSync(gulpFile)) {
252 return false;
253 }
254 //
255 try {
256 child_process.execSync("node " + gulpBin + " --gulpfile " + gulpFile + " " + (task || "default"), {encoding: 'utf8'});
257 return true;
258 } catch (e) {
259 return false;
260 }
261 },
262
263 /**
264 * 复制编译文件至app资源目录
265 * @param host
266 * @param port
267 * @param socketPort
268 * @param removeBundlejs
269 */
270 syncFolderAndWebSocket(host, port, socketPort, removeBundlejs) {
271 let isSocket = !!(host && socketPort);
272 let hostUrl = 'http://' + host + ':' + port + "/";
273 //
274 let jsonData = require(path.resolve('eeui.config'));
275 jsonData.socketHost = host ? host : '';
276 jsonData.socketPort = socketPort ? socketPort : '';
277 jsonData.socketHome = isSocket ? this.getHostIndexUrl(hostUrl + config.sourcePagesDir) : '';
278 //
279 let random = Math.random();
280 let deviceIds = {};
281 //
282 let copyJsEvent = (originDir, newDir, rootDir) => {
283 let lists = fs.readdirSync(originDir);
284 lists.some((item) => {
285 if (!utils.execPath(item)) {
286 return false;
287 }
288 let originPath = path.resolve(originDir, item);
289 let newPath = path.resolve(newDir, item);
290 let stats = utils.pathType(originPath);
291 if (stats === 1) {
292 this.copyFileMd5(originPath, newPath, (err) => {
293 if (err || !socketAlready) {
294 return;
295 }
296 socketClients.some((client) => {
297 let deviceKey = client.deviceId + hostUrl + rootDir + item;
298 if (client.ws.readyState !== 2 && deviceIds[deviceKey] !== random) {
299 deviceIds[deviceKey] = random;
300 setTimeout(() => {
301 utils.sendWebSocket(client.ws, client.version, {
302 type: "RELOADPAGE",
303 value: hostUrl + rootDir + item,
304 });
305 }, 300);
306 }
307 });
308 });
309 } else if (stats === 2) {
310 copyJsEvent(originPath, newPath, (rootDir || "") + item + "/")
311 }
312 });
313 };
314 //syncFiles
315 let mainPath = path.resolve('platforms/android/eeuiApp/app/src/main/assets/eeui');
316 let bundlejsPath = path.resolve('platforms/ios/eeuiApp/bundlejs/eeui');
317 if (removeBundlejs) {
318 fse.removeSync(mainPath);
319 fse.removeSync(bundlejsPath);
320 fse.outputFile(path.resolve(mainPath, 'config.json'), JSON.stringify(jsonData, null, "\t"));
321 fse.outputFile(path.resolve(bundlejsPath, 'config.json'), JSON.stringify(jsonData, null, "\t"));
322 }
323 copyJsEvent(path.resolve(config.distDir), mainPath);
324 copyJsEvent(path.resolve(config.distDir), bundlejsPath);
325 //WebSocket
326 if (isSocket) {
327 if (socketAlready === false) {
328 socketAlready = true;
329 let WebSocketServer = require('ws').Server,
330 wss = new WebSocketServer({port: socketPort});
331 wss.on('connection', (ws, info) => {
332 let deviceId = uuid.v4();
333 let mode = utils.getQueryString(info.url, "mode");
334 let version = utils.runNum(utils.getQueryString(info.url, "version"));
335 socketClients.push({deviceId, ws, version});
336 ws.on('close', () => {
337 socketClients.some((socketItem, i) => {
338 if (socketItem.deviceId === deviceId) {
339 socketClients.splice(i, 1);
340 return true;
341 }
342 });
343 });
344 //
345 switch (mode) {
346 case "initialize":
347 utils.sendWebSocket(ws, version, {
348 type: "HOMEPAGE",
349 value: this.getHostIndexUrl(hostUrl + config.sourcePagesDir),
350 appboards: utils.getAllAppboards(config.sourceDir)
351 });
352 break;
353
354 case "back":
355 utils.sendWebSocket(ws, version, {
356 type: "HOMEPAGEBACK",
357 value: this.getHostIndexUrl(hostUrl + config.sourcePagesDir),
358 appboards: utils.getAllAppboards(config.sourceDir)
359 });
360 break;
361
362 case "reconnect":
363 utils.sendWebSocket(ws, version, {
364 type: "RECONNECT",
365 value: this.getHostIndexUrl(hostUrl + config.sourcePagesDir),
366 appboards: utils.getAllAppboards(config.sourceDir)
367 });
368 break;
369 }
370 });
371 }
372 notifier.notify({
373 title: 'WiFi真机同步',
374 message: jsonData.socketHost + ':' + jsonData.socketPort,
375 contentImage: path.join(__dirname, 'logo.png')
376 });
377 socketTimeout && clearInterval(socketTimeout);
378 socketTimeout = setTimeout(() => {
379 let msg = '';
380 msg+= chalk.bgGreen.bold.black(`【WiFI真机同步】`);
381 msg+= chalk.bgGreen.black(`IP地址: `);
382 msg+= chalk.bgGreen.bold.black.underline(`${jsonData.socketHost}`);
383 msg+= chalk.bgGreen.black(`、端口号: `);
384 msg+= chalk.bgGreen.bold.black.underline(`${jsonData.socketPort}`);
385 console.log(); console.log(msg); console.log();
386 }, 200);
387 } else {
388 child_process.fork(path.join(__dirname, 'buildNotify.js'));
389 }
390 },
391
392 /**
393 * 打包build目录
394 */
395 compressBuildDir() {
396 let zipName = "build-" + utils.formatDate("YmdHis");
397 let expand = require("../utils/expand");
398 if (expand.androidGradle("versionName")) {
399 zipName += "-";
400 zipName += expand.androidGradle("versionName");
401 if (expand.androidGradle("versionCode")) {
402 zipName += "-";
403 zipName += expand.androidGradle("versionCode");
404 }
405 }
406 let zipPackPath = path.resolve(config.zipPackDir);
407 utils.mkdirsSync(zipPackPath);
408 utils.zipCompress({
409 output: zipPackPath + "/" + zipName + ".zip",
410 entry: [{
411 type: 'dir',
412 path: path.resolve(config.distDir)
413 }]
414 });
415 },
416
417 /**
418 * 开发模式
419 * @param isOnce
420 * @returns {*}
421 */
422 dev(isOnce) {
423 let gauge = new Gauge();
424 let progress = 0;
425 let options = {
426 ext: 'vue',
427 watch: !isOnce,
428 minimize: false,
429 devtool: false,
430 mode: 'development',
431 onProgress: (complete, action) => {
432 if (complete > progress) {
433 progress = complete;
434 } else {
435 complete = progress;
436 }
437 gauge.show(action, complete);
438 }
439 };
440 if (fs.existsSync(path.resolve('babel.config.js'))) {
441 options.babelOptions = require(path.resolve('babel.config.js'));
442 } else if (fs.existsSync(path.resolve('.babelrc'))) {
443 options.babelOptions = utils.jsonParse(fs.readFileSync(path.resolve('.babelrc'), 'utf8'));
444 }
445 //
446 let serverStatus = 0;
447 let socketPort = config.port;
448 let serverPort = config.port_socket;
449 let buildCallback = (error, output, info) => {
450 gauge.hide();
451 if (error) {
452 console.log(chalk.red('Build Failed!'));
453 utils.each(typeof error == 'object' ? error : [error], (index, item) => {
454 console.error(item);
455 });
456 utils.each(info.assetsByChunkName, (key, value) => {
457 fs.writeFileSync(path.resolve(config.distDir, config.sourcePagesDir, value), this.errorServer(true, 500, ansiHtml.toHtml(error)));
458 });
459 } else {
460 console.log('Build completed!');
461 console.log(output.toString());
462 //
463 this.completeCode(info.assetsByChunkName);
464 if (options.watch) {
465 if (serverStatus === 0) {
466 serverStatus = 1;
467 this.portIsOccupied(serverPort, (err, port) => {
468 if (err) throw err;
469 this.portIsOccupied(socketPort, (err, sPort) => {
470 if (err) throw err;
471 serverPort = port;
472 socketPort = sPort;
473 this.createServer(path.resolve(config.distDir), serverPort);
474 this.copyOtherFile(path.resolve(config.sourceDir), path.resolve(config.distDir), true);
475 this.appboardGulpBabel('appboard-dev');
476 this.syncFolderAndWebSocket(ipv4, serverPort, socketPort, true);
477 serverStatus = 200;
478 });
479 });
480 }
481 } else {
482 this.copyOtherFile(path.resolve(config.sourceDir), path.resolve(config.distDir), true);
483 this.appboardGulpBabel('appboard-dev');
484 this.syncFolderAndWebSocket(null, null, null, true);
485 }
486 }
487 if (serverStatus === 200) {
488 this.copyOtherFile(path.resolve(config.sourceDir), path.resolve(config.distDir), false);
489 this.syncFolderAndWebSocket(ipv4, serverPort, socketPort, false);
490 }
491 };
492 //
493 fse.removeSync(path.resolve(config.distDir));
494 let mBuilder = new builder(`${config.sourceDir}/${config.sourcePagesDir}`, `${config.distDir}/${config.sourcePagesDir}`, options).build(buildCallback);
495 //
496 if (options.watch) {
497 //监听appboard文件变化
498 let watchListener = (filePath, content) => {
499 if (utils.leftExists(filePath, "appboard/") && utils.rightExists(filePath, ".js") && socketAlready) {
500 content = utils.replaceModule(utils.replaceEeuiLog(content));
501 socketClients.some((client) => {
502 if (client.ws.readyState !== 2) {
503 utils.sendWebSocket(client.ws, client.version, {
504 type: "REFRESH",
505 appboards: [{
506 path: filePath,
507 content: content,
508 }],
509 });
510 }
511 });
512 }
513 };
514 //监听文件变化
515 (() => {
516 let appboardDir = path.resolve(config.sourceDir, 'appboard'),
517 sourceDir = config.sourceDir,
518 distDir = config.distDir,
519 sourcePath,
520 sourceName;
521 chokidar.watch(config.sourceDir, {
522 ignored: /[\/\\]\./,
523 persistent: true
524 }).on('all', (event, filePath) => {
525 if (serverStatus !== 200) {
526 return;
527 }
528 sourcePath = path.resolve(filePath);
529 sourceName = path.relative(path.resolve(sourceDir), filePath);
530 if (/^win/.test(process.platform)) {
531 filePath = filePath.replace(/\\/g, "/");
532 sourceName = sourceName.replace(/\\/g, "/");
533 }
534 //
535 if (utils.rightExists(filePath, ".vue")) {
536 if (utils.leftExists(filePath, "src/pages/")) {
537 let fileName = path.relative(path.resolve("src/pages/"), filePath).replace(/\.\w+$/, '');
538 if (event === "add") {
539 mBuilder.insertEntry({
540 fileName: fileName,
541 sourcePath: sourcePath + "?entry=true"
542 });
543 } else if (event === "unlink") {
544 mBuilder.removeEntry({
545 fileName: fileName
546 });
547 }
548 }
549 } else if (utils.execPath(sourcePath)) {
550 let distPath = path.resolve(distDir, sourceName);
551 if (["add", "change"].indexOf(event) !== -1) {
552 let sourceContent = fs.readFileSync(sourcePath, 'utf8');
553 if (utils.leftExists(sourcePath, appboardDir)) {
554 fse.outputFileSync(distPath, utils.replaceModule(utils.replaceEeuiLog(sourceContent)));
555 this.appboardGulpBabel('appboard-dev --filePath ' + distPath);
556 } else {
557 fse.copySync(sourcePath, distPath);
558 }
559 watchListener(sourceName, sourceContent);
560 } else if (event === "unlink") {
561 fse.removeSync(path.resolve(sourceDir, '/../platforms/android/eeuiApp/app/src/main/assets/eeui', sourceName));
562 fse.removeSync(path.resolve(sourceDir, '/../platforms/ios/eeuiApp/bundlejs/eeui', sourceName));
563 fse.removeSync(distPath);
564 watchListener(sourceName, "");
565 }
566 if (["change", "unlink"].indexOf(event) !== -1) {
567 mBuilder.webpackInvalidate();
568 }
569 }
570 });
571 //监听eeui.config配置文件
572 chokidar.watch(path.resolve(sourceDir, '/../eeui.config.js'), {
573 ignored: /[\/\\]\./,
574 persistent: true
575 }).on('change', (s) => {
576 if (serverStatus !== 200) {
577 return;
578 }
579 notifier.notify({
580 title: 'eeui.config.js',
581 message: "修改的内容需要重编译运行App才生效。",
582 contentImage: path.join(__dirname, 'logo.png')
583 });
584 logger.warn("检测到配置文件[eeui.config.js]已变化,修改的内容可能需要重新编译运行App才起效。");
585 logger.sep();
586 utils.syncConfigToPlatforms();
587 });
588 })();
589 }
590 //
591 return mBuilder;
592 },
593
594 /**
595 * 编译模式
596 * @param noZip
597 */
598 build(noZip) {
599 let gauge = new Gauge();
600 let progress = 0;
601 let options = {
602 ext: 'vue',
603 watch: false,
604 minimize: true,
605 devtool: false,
606 mode: 'production',
607 onProgress: (complete, action) => {
608 if (complete > progress) {
609 progress = complete;
610 } else {
611 complete = progress;
612 }
613 gauge.show(action, complete);
614 }
615 };
616 if (fs.existsSync(path.resolve('babel.config.js'))) {
617 options.babelOptions = require(path.resolve('babel.config.js'));
618 } else if (fs.existsSync(path.resolve('.babelrc'))) {
619 options.babelOptions = utils.jsonParse(fs.readFileSync(path.resolve('.babelrc'), 'utf8'));
620 }
621 //
622 let buildCallback = (error, output, info) => {
623 gauge.hide();
624 if (error) {
625 console.log(chalk.red('Build Failed!'));
626 utils.each(typeof error == 'object' ? error : [error], (index, item) => {
627 console.error(item);
628 });
629 } else {
630 console.log('Build completed!');
631 console.log(output.toString());
632 //
633 this.completeCode(info.assetsByChunkName);
634 this.copyOtherFile(path.resolve(config.sourceDir), path.resolve(config.distDir), true);
635 this.appboardGulpBabel('appboard-build');
636 this.syncFolderAndWebSocket(null, null, null, true);
637 if (noZip !== true) {
638 this.compressBuildDir();
639 }
640 }
641 };
642 fse.removeSync(path.resolve(config.distDir));
643 return new builder(`${config.sourceDir}/${config.sourcePagesDir}`, `${config.distDir}/${config.sourcePagesDir}`, options).build(buildCallback);
644 }
645};
\No newline at end of file