UNPKG

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