UNPKG

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