UNPKG

27 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3const fs = require('fs-extra');
4const path = require('path');
5const program = require('commander');
6const pm2 = require('pm2');
7const chalk = require('chalk');
8const npmRunScript = require('npm-run-script');
9const opn = require('open');
10
11const before = require('./_before');
12
13const contentWaiting = require('../defaults/content-waiting');
14const {
15 keyFileProjectConfigTempFull,
16 keyFileProjectConfigTempPortionServer,
17 keyFileProjectConfigTempPortionClient,
18 keyFileProjectConfigTempPortionOtherClient,
19 filenameWebpackDevServerPortTemp,
20 filenameBuilding
21 // filenameBuildFail,
22 // filenameDll, filenameDllManifest,
23} = require('../defaults/before-build');
24
25const checkFileUpdate = require('../libs/check-file-change');
26const removeTempBuild = require('../libs/remove-temp-build');
27const removeTempProjectConfig = require('../libs/remove-temp-project-config');
28const validateConfig = require('../libs/validate-config');
29const validateConfigDist = require('../libs/validate-config-dist');
30const getDirDevTmp = require('../libs/get-dir-dev-tmp');
31const getDirDevCache = require('../libs/get-dir-dev-cache');
32const getDirTemp = require('../libs/get-dir-tmp');
33
34const __ = require('../utils/translate');
35const sleep = require('../utils/sleep');
36const spinner = require('../utils/spinner');
37// const readBuildConfigFile = require('../utils/read-build-config-file')
38const getAppType = require('../utils/get-app-type');
39const setEnvFromCommand = require('../utils/set-env-from-command');
40const getChunkmapPath = require('../utils/get-chunkmap-path');
41const initNodeEnv = require('../utils/init-node-env');
42const getCwd = require('../utils/get-cwd');
43const getPathnameDevServerStart = require('../utils/get-pathname-dev-server-start');
44const getLogMsg = require('../libs/get-log-msg');
45const log = require('../libs/log');
46// const terminate = require('../utils/terminate');
47
48const kootWebpackBuildVendorDll = require('koot-webpack/build-vendor-dll');
49
50let exiting = false;
51
52program
53 .version(require('../package').version, '-v, --version')
54 .usage('[options]')
55 .option('-c, --client', 'Set STAGE to CLIENT')
56 .option('-s, --server', 'Set STAGE to SERVER')
57 .option('-g, --global', 'Connect to global PM2')
58 .option('--stage <stage>', 'Set STAGE')
59 .option(
60 '--dest <destination-path>',
61 'Set destination directory (for temporary files)'
62 )
63 .option('--config <config-file-path>', 'Set config file')
64 .option('--type <project-type>', 'Set project type')
65 .option('--port <port>', 'Set server port')
66 .option('--no-open', "Don't open browser automatically")
67 .option('--no-dll', "Don't use Webpack's DLL plugin")
68 .option('--koot-test', 'Koot test mode')
69 .option('--koot-development', 'Koot development mode')
70 .parse(process.argv);
71
72/**
73 * 进入开发环境
74 * ---
75 * **同构 (isomorphic)**
76 * 1. 启动 PM2 进程: `webpack-dev-server` (STAGE: client)
77 * 2. 启动 PM2 进程: `webpack` (watch mode) (STAGE: server)
78 * 3. 启动 PM2 进程: `[打包结果]/server/index.js`
79 * 4. 启动 PM2 进程: `ReactApp/server/index-dev.js`
80 * ---
81 * **单页面应用 (SPA)**
82 * - 强制设置 STAGE 为 client,并启动 webpack-dev-server
83 *
84 */
85const run = async () => {
86 process.env.WEBPACK_BUILD_ENV = 'dev';
87
88 // 清除所有临时配置文件
89 await removeTempProjectConfig();
90 // 清理临时目录
91 await before(program);
92
93 // 清空 log
94 process.stdout.write('\x1B[2J\x1B[0f');
95
96 const {
97 client,
98 server,
99 stage: _stage,
100 dest,
101 config,
102 type,
103 global = false,
104 open = true,
105 port,
106 dll = true,
107 kootTest = false,
108 kootDevelopment = false
109 } = program;
110
111 initNodeEnv();
112 setEnvFromCommand({
113 config,
114 type,
115 port
116 });
117
118 let stage = (() => {
119 if (_stage) return _stage;
120 if (client) return 'client';
121 if (server) return 'server';
122
123 // false - 同构项目的完整开发环境
124 return false;
125 })();
126
127 /** @type {String} build 命令的附加参数 */
128 const buildCmdArgs =
129 '--env dev' +
130 (typeof dest === 'string' ? ` --dest ${dest}` : '') +
131 (typeof config === 'string' ? ` --config ${config}` : '') +
132 (typeof type === 'string' ? ` --type ${type}` : '') +
133 (kootTest ? ` --koot-test` : '') +
134 (kootDevelopment ? ` --koot-development` : '') +
135 ' --koot-dev';
136
137 // ========================================================================
138 //
139 // 准备项目配置和相关变量
140 //
141 // ========================================================================
142
143 // 确保关键目录存在
144 const cwd = getCwd();
145 const dirDevTemp = getDirDevTmp(cwd);
146 await fs.ensureDir(dirDevTemp);
147 await fs.emptyDir(dirDevTemp);
148 const dirCache = getDirDevCache();
149 await fs.ensureDir(dirCache);
150 await fs.emptyDir(dirCache);
151
152 // 验证、读取项目配置信息
153 const kootConfig = await validateConfig();
154
155 // 如果在命令中设置了 dest,强制修改配置中的 dist
156 if (dest) kootConfig.dist = validateConfigDist(dest);
157
158 const {
159 dist,
160 // port: configPort,
161 devPort,
162 [keyFileProjectConfigTempFull]: fileProjectConfigTempFull,
163 [keyFileProjectConfigTempPortionServer]: fileProjectConfigTempPortionServer,
164 [keyFileProjectConfigTempPortionClient]: fileProjectConfigTempPortionClient,
165 [keyFileProjectConfigTempPortionOtherClient]: fileProjectConfigTempPortionOtherClient
166 } = kootConfig;
167 const [devMemoryAllocationClient, devMemoryAllocationServer] = (() => {
168 const { devMemoryAllocation } = kootConfig;
169 if (!devMemoryAllocation) return [undefined, undefined];
170 if (typeof devMemoryAllocation === 'object') {
171 return [devMemoryAllocation.client, devMemoryAllocation.server];
172 }
173 if (isNaN(devMemoryAllocation)) return [undefined, undefined];
174 return [parseInt(devMemoryAllocation), parseInt(devMemoryAllocation)];
175 })();
176 const appType = await getAppType();
177 const packageInfo = await fs.readJson(path.resolve(cwd, 'package.json'));
178 const { name } = packageInfo;
179
180 // 清理目标目录
181 await fs.ensureDir(dist);
182 await fs.emptyDir(dist);
183 await fs.ensureDir(path.resolve(dist, 'public'));
184 await fs.ensureDir(path.resolve(dist, 'server'));
185
186 /** @type {Array} 正在运行的进程/服务列表 */
187 const processes = [];
188
189 /** @type {Boolean} 全局等待提示 */
190 const waitingSpinner = false;
191
192 // 清理遗留的临时文件
193 await removeTempBuild(dist);
194
195 // 如果有临时项目配置��件,更改环境变量
196 if (fileProjectConfigTempFull)
197 process.env.KOOT_PROJECT_CONFIG_FULL_PATHNAME = fileProjectConfigTempFull;
198 if (fileProjectConfigTempPortionServer)
199 process.env.KOOT_PROJECT_CONFIG_PORTION_SERVER_PATHNAME = fileProjectConfigTempPortionServer;
200 if (fileProjectConfigTempPortionClient)
201 process.env.KOOT_PROJECT_CONFIG_PORTION_CLIENT_PATHNAME = fileProjectConfigTempPortionClient;
202 if (fileProjectConfigTempPortionOtherClient)
203 process.env.KOOT_PROJECT_CONFIG_PORTION_OTHER_CLIENT_PATHNAME = fileProjectConfigTempPortionOtherClient;
204
205 // 如果为 SPA,强制设置 STAGE
206 if (process.env.WEBPACK_BUILD_TYPE === 'spa') {
207 process.env.WEBPACK_BUILD_STAGE = 'client';
208 stage = 'client';
209 }
210
211 // 如果配置中存在 port,修改环境变量
212 // if (typeof port === 'undefined' && typeof configPort !== 'undefined')
213 // process.env.SERVER_PORT = getPort(configPort, 'dev')
214 process.env.SERVER_PORT = devPort;
215
216 // 设置其他环境变量
217 process.env.KOOT_DEV_START_TIME = Date.now();
218 if (typeof kootConfig.devServer === 'object')
219 process.env.KOOT_DEV_WDS_EXTEND_CONFIG = JSON.stringify(
220 kootConfig.devServer
221 );
222
223 // 等待一段时间,确保某些硬盘操作的完成
224 await sleep(1000);
225
226 // ========================================================================
227 //
228 // 进程关闭行为
229 //
230 // ========================================================================
231 const removeAllExitListeners = () => {
232 process.removeListener('exit', exitHandler);
233 process.removeListener('SIGINT', exitHandler);
234 process.removeListener('SIGUSR1', exitHandler);
235 process.removeListener('SIGUSR2', exitHandler);
236 process.removeListener('uncaughtException', exitHandler);
237 };
238 const exitHandler = async (options = {}) => {
239 if (exiting) return;
240
241 exiting = true;
242
243 let { silent = false } = options;
244 const { error = false } = options;
245
246 if (error) silent = true;
247
248 const PromiseAll = Promise.allSettled
249 ? Promise.allSettled.bind(Promise)
250 : Promise.all.bind(Promise);
251
252 if (Array.isArray(processes) && processes.length) {
253 if (waitingSpinner) waitingSpinner.stop();
254 // eslint-disable-next-line no-console
255 if (!silent) console.log(' ');
256
257 // if (!silent)
258 // console.log(
259 // '\n\n\n' +
260 // chalk.redBright(
261 // '!! Please wait for killing processes !!'
262 // ) +
263 // '\n\n'
264 // );
265 const kill = p => {
266 // if (!silent) {
267 // console.log(
268 // `TERMINATING: ${p.pm2_env.pm_id} | ${p.pid} | ${p.name}`
269 // );
270 // }
271 return new Promise((resolve, reject) => {
272 // console.log(JSON.stringify(p));
273 // console.log(pm2, pm2.delete);
274 pm2.delete(p.pm2_env.pm_id, (err, proc) => {
275 // console.log('err', err);
276 // console.log('proc', proc.map(p => p.status));
277 if (err) return reject(err);
278 // processes.splice(processes.indexOf(p), 1);
279 resolve(proc);
280 });
281 });
282 };
283 // for (const p of processes) {
284 // await kill(p);
285 // }
286 await PromiseAll(processes.map(kill)).catch(console.error);
287 pm2.disconnect();
288 // w.stop()
289 }
290
291 // console.log(
292 // '> status',
293 // processes.map(p => {
294 // const { pm2_env, stderr, stdio, stdout, stdin, ...proc } = p;
295 // console.log(proc);
296 // return proc.status;
297 // })
298 // );
299 // await sleep(1000);
300
301 try {
302 await PromiseAll([
303 removeTempProjectConfig(),
304 removeTempBuild(dist),
305 fs.emptyDir(getDirDevTmp(cwd)),
306 // 清理临时目录
307 fs.remove(getDirTemp())
308 ]);
309 } catch (e) {}
310
311 removeAllExitListeners();
312
313 try {
314 // if (!silent)
315 // console.log(
316 // '\n\n\n' + chalk.redBright('!! TERMINATED !!') + '\n\n'
317 // );
318 if (process.send) {
319 process.send('Koot dev mode exit successfully');
320 }
321 try {
322 process.kill(process.pid);
323 } catch (e) {}
324 if (!error) {
325 // eslint-disable-next-line no-console
326 console.log(
327 '\n\n\n' +
328 chalk.cyanBright('Press CTRL+C again to exit.') +
329 '\n\n'
330 );
331 }
332 process.exit(1);
333 } catch (e) {
334 console.error(e);
335 }
336
337 // exiting = false;
338 };
339 // 在脚本进程关闭/结束时,同时关闭打开的 PM2 进程
340 process.stdin.resume();
341 // do something when app is closing
342 process.on('exit', exitHandler);
343 // catches ctrl+c event
344 process.on('SIGINT', exitHandler);
345 // catches "kill pid" (for example: nodemon restart)
346 process.on('SIGUSR1', exitHandler);
347 process.on('SIGUSR2', exitHandler);
348 // catches uncaught exceptions
349 process.on('uncaughtException', exitHandler);
350
351 // ========================================================================
352 //
353 // 如果开启了 Webpack DLL 插件,此时执行 DLL 打包
354 //
355 // ========================================================================
356 if (dll && process.env.WEBPACK_BUILD_STAGE !== 'server') {
357 const msg = getLogMsg(false, 'dev', __('dev.build_dll'));
358 const waiting = spinner(msg + '...');
359 let error;
360 let result;
361
362 // DLL 打包
363 try {
364 if (stage) {
365 process.env.WEBPACK_BUILD_STAGE = stage;
366 result = await kootWebpackBuildVendorDll(kootConfig);
367 } else {
368 const stageCurrent = process.env.WEBPACK_BUILD_STAGE;
369
370 process.env.WEBPACK_BUILD_STAGE = 'client';
371 result = await kootWebpackBuildVendorDll(kootConfig);
372 await sleep(500);
373 process.env.WEBPACK_BUILD_STAGE = 'server';
374 result = await kootWebpackBuildVendorDll(kootConfig);
375
376 process.env.WEBPACK_BUILD_STAGE = stageCurrent;
377 }
378 } catch (e) {
379 waiting.stop();
380 spinner(msg).fail();
381 if (
382 result &&
383 Array.isArray(result.errors) &&
384 result.errors.length
385 ) {
386 error = result.errors;
387 result.errors.forEach(e => console.error(e));
388 } else {
389 error = e;
390 console.error(e);
391 }
392 process.exit();
393 }
394
395 if (error) return;
396
397 await sleep(500);
398 // console.log('result', result)
399 // console.log(111)
400 // return
401
402 waiting.stop();
403 spinner(msg).succeed();
404 }
405
406 // ========================================================================
407 //
408 // 如果设置了 stage,仅运行该 stage
409 //
410 // ========================================================================
411 if (stage) {
412 const cmd = `koot-build --stage ${stage} ${buildCmdArgs}`;
413 const child = npmRunScript(cmd, {});
414 child.once('error', error => {
415 // eslint-disable-next-line no-console
416 console.trace(error);
417 process.exit(1);
418 });
419 child.once('exit', async (/*exitCode*/) => {
420 // console.trace('exit in', exitCode)
421 // process.exit(exitCode)
422 });
423
424 // SPA 开发环境
425 if (process.env.WEBPACK_BUILD_TYPE === 'spa') {
426 // 等待 filenameBuilding 文件删除
427 let flagCreated = false;
428 const fileFlagBuilding = path.resolve(dist, filenameBuilding);
429 await new Promise(resolve => {
430 const wait = () =>
431 setTimeout(() => {
432 if (!flagCreated) {
433 flagCreated = fs.existsSync(fileFlagBuilding);
434 return wait();
435 }
436 if (!fs.existsSync(fileFlagBuilding)) return resolve();
437 wait();
438 }, 1000);
439 wait();
440 });
441
442 // console.log(' ')
443
444 await new Promise(resolve => {
445 setTimeout(() => {
446 log('success', 'dev', __('dev.spa_success'));
447 // eslint-disable-next-line no-console
448 console.log(
449 ' @ ' +
450 chalk.green(
451 `http://localhost:${process.env.SERVER_PORT}/`
452 )
453 );
454 // eslint-disable-next-line no-console
455 console.log(' ');
456 // eslint-disable-next-line no-console
457 console.log('------------------------------');
458 // eslint-disable-next-line no-console
459 console.log(' ');
460 resolve();
461 }, 500);
462 });
463
464 if (open) openBrowserPage();
465 }
466
467 return;
468 }
469
470 // ========================================================================
471 //
472 // 没有设置 STAGE,表示同构项目的完整开发环境,开启多个进程
473 //
474 // ========================================================================
475 // spinner(
476 // chalk.yellowBright('[koot/build] ')
477 // + __('build.build_start', {
478 // type: chalk.cyanBright(appType),
479 // stage: chalk.green('client'),
480 // env: chalk.green('dev'),
481 // })
482 // )
483
484 const pathChunkmap = getChunkmapPath(dist);
485 const pathServerJS = path.resolve(dist, 'server/index.js');
486 const pathServerStartFlag = getPathnameDevServerStart();
487
488 // 根据 stage 开启 PM2 进程
489 const start = stage =>
490 new Promise(async (resolve, reject) => {
491 // console.log(`starting ${stage}`)
492
493 const pathLogOut = path.resolve(getDirDevTmp(cwd), `${stage}.log`);
494 const pathLogErr = path.resolve(
495 getDirDevTmp(cwd),
496 `${stage}-error.log`
497 );
498 if (fs.existsSync(pathLogOut)) await fs.remove(pathLogOut);
499 if (fs.existsSync(pathLogErr)) await fs.remove(pathLogErr);
500 await fs.ensureFile(pathLogOut);
501 await fs.ensureFile(pathLogErr);
502
503 const config = {
504 name: `${stage}-${name}`,
505 script: path.resolve(__dirname, './build.js'),
506 args: `--stage ${stage} ${buildCmdArgs}`,
507 cwd: cwd,
508 output: pathLogOut,
509 error: pathLogErr,
510 autorestart: true
511 };
512
513 switch (stage) {
514 case 'client': {
515 if (devMemoryAllocationClient)
516 config.node_args = `--max-old-space-size=${devMemoryAllocationClient}`;
517 break;
518 }
519 case 'server': {
520 if (devMemoryAllocationServer)
521 config.node_args = `--max-old-space-size=${devMemoryAllocationServer}`;
522 break;
523 }
524 case 'run': {
525 Object.assign(config, {
526 script: pathServerJS,
527 watch: path.dirname(pathServerJS),
528 ignore_watch: [
529 '.server-start',
530 'node_modules',
531 config.output,
532 config.error
533 ],
534 watch_options: {
535 cwd: path.dirname(pathServerJS)
536 // usePolling: true
537 }
538 // autorestart: true,
539 });
540 // console.log(config);
541 delete config.args;
542 // console.log(config)
543 // await fs.writeJson(
544 // path.resolve(__dirname, '../1.json'),
545 // config,
546 // {
547 // spaces: 4
548 // }
549 // )
550 break;
551 }
552 case 'main': {
553 const mainScript = path.resolve(
554 __dirname,
555 '../ReactApp/server/index-dev.js'
556 );
557 Object.assign(config, {
558 script: mainScript,
559 watch: path.dirname(mainScript)
560 // env: {
561 // DEBUG: 'koa-mount'
562 // }
563 });
564 delete config.args;
565 break;
566 }
567 default: {
568 }
569 }
570
571 // console.log(config)
572 // processes.push(config.name)
573 pm2.start(config, (err, proc) => {
574 // console.log(err)
575 if (err) return reject(err);
576 proc.forEach(p => {
577 processes.push({
578 ...p,
579 name: config.name,
580 pid: p.pid || p.process.pid
581 });
582 });
583 // console.log(JSON.stringify(proc))
584 // fs.writeJsonSync(path.resolve(__dirname, '../2.json'), proc, {
585 // spaces: 4
586 // });
587 resolve(proc);
588 });
589 });
590
591 // 启动过程结束
592 const complete = () => {
593 npmRunScript(`pm2 logs`);
594 if (open) return opn(`http://localhost:${process.env.SERVER_PORT}/`);
595 };
596
597 // 遇到错误
598 const encounterError = e => {
599 const error = e instanceof Error ? e : new Error(e);
600 exitHandler({ error: true });
601 throw error;
602 };
603
604 // 连接 PM2
605 // console.log('noDaemon', !global)
606 try {
607 pm2.connect(!global, async err => {
608 if (err) {
609 // console.error(err)
610 process.exit(2);
611 }
612
613 // eslint-disable-next-line no-console
614 console.log(
615 ` ` +
616 chalk.yellowBright('[koot/build] ') +
617 __('build.build_start', {
618 type: chalk.cyanBright(__(`appType.${appType}`)),
619 stage: chalk.green('client'),
620 env: chalk.green('dev')
621 })
622 );
623
624 // 清空 chunkmap 文件
625 await fs.ensureFile(pathChunkmap);
626 await fs.writeFile(pathChunkmap, contentWaiting);
627
628 // 清空 server 打包结果文件
629 await fs.ensureFile(pathServerJS);
630 await fs.writeFile(pathServerJS, contentWaiting);
631
632 // 清空服务器启动成功标识文件
633 await fs.ensureFile(pathServerStartFlag);
634 await fs.writeFile(pathServerStartFlag, contentWaiting);
635
636 // 启动 client webpack-dev-server
637 /*const processClient = */ await start('client');
638
639 // 监视 chunkmap 文件,如果修改,进入下一步
640 // await Promise.race([
641 await checkFileUpdate(pathChunkmap, contentWaiting);
642 // checkFileUpdate(path.resolve(getDirDevTmp(cwd), 'client-error.log'), '')
643 // .then(encounterError)
644 // ])
645 // waitingSpinner.succeed()
646 // eslint-disable-next-line no-console
647 console.log(
648 chalk.green('√ ') +
649 chalk.yellowBright('[koot/build] ') +
650 __('build.build_complete', {
651 type: chalk.cyanBright(__(`appType.${appType}`)),
652 stage: chalk.green('client'),
653 env: chalk.green('dev')
654 })
655 );
656 // console.log(processClient[0].process, processClient[0].pid)
657 // console.log(
658 // ` [${}]`
659 // )
660
661 // 启动 server webpack
662 // waitingSpinner = spinner(
663 // chalk.yellowBright('[koot/build] ')
664 // + __('build.build_start', {
665 // type: chalk.cyanBright(appType),
666 // stage: chalk.green('server'),
667 // env: chalk.green('dev'),
668 // })
669 // )
670 // eslint-disable-next-line no-console
671 console.log(
672 ` ` +
673 chalk.yellowBright('[koot/build] ') +
674 __('build.build_start', {
675 type: chalk.cyanBright(__(`appType.${appType}`)),
676 stage: chalk.green('server'),
677 env: chalk.green('dev')
678 })
679 );
680 await start('server');
681
682 // 监视 server.js 文件,如果修改,进入下一步
683 await checkFileUpdate(pathServerJS, contentWaiting);
684 // waitingSpinner.succeed()
685
686 // 执行
687 // waitingSpinner = spinner(
688 // chalk.yellowBright('[koot/build] ')
689 // + 'waiting...'
690 // )
691
692 await sleep(500);
693 // eslint-disable-next-line no-console
694 console.log(
695 chalk.green('√ ') +
696 chalk.yellowBright('[koot/build] ') +
697 __('build.build_complete', {
698 type: chalk.cyanBright(__(`appType.${appType}`)),
699 stage: chalk.green('server'),
700 env: chalk.green('dev')
701 })
702 );
703
704 // 启动服务器
705 await start('run');
706
707 // 监视服务器启动标识文件,如果修改,进入下一步
708 const errServerRun = await checkFileUpdate(
709 pathServerStartFlag,
710 contentWaiting
711 );
712
713 // 移除临时文件
714 await fs.remove(
715 path.resolve(
716 getDirDevTmp(cwd),
717 filenameWebpackDevServerPortTemp
718 )
719 );
720
721 // waitingSpinner.stop()
722 // waitingSpinner = undefined
723
724 /** @type {Object} 服务器相关信息 */
725 let infosServer;
726 try {
727 infosServer = JSON.parse(errServerRun);
728 } catch (e) {}
729
730 if (
731 typeof infosServer !== 'object' &&
732 errServerRun !== ' ' &&
733 errServerRun
734 ) {
735 // 出错
736 // eslint-disable-next-line no-console
737 console.log(' ');
738 // eslint-disable-next-line no-console
739 console.log(chalk.redBright(errServerRun));
740 // eslint-disable-next-line no-console
741 console.log(' ');
742 return await exitHandler({
743 silent: true
744 });
745 }
746
747 await start('main');
748 await checkFileUpdate(pathServerStartFlag, contentWaiting);
749
750 return complete();
751 });
752 } catch (e) {
753 encounterError(e);
754 }
755};
756
757const openBrowserPage = () => {
758 return opn(`http://localhost:${process.env.SERVER_PORT}/`);
759};
760
761run().catch(err => {
762 console.error(err);
763});