UNPKG

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