UNPKG

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