UNPKG

7.93 kBJavaScriptView Raw
1/* eslint-disable unicorn/no-process-exit */
2const util = require('util')
3const fs = require('fs')
4const url = require('url')
5const chalk = require('chalk')
6const notifier = require('node-notifier')
7const co = require('co')
8const stripAnsi = require('strip-ansi')
9const tildify = require('tildify')
10const address = require('address')
11const merge = require('lodash.merge')
12const copy = require('clipboardy')
13const opn = require('opn')
14const buildConfigChain = require('babel-core/lib/transformation/file/options/build-config-chain')
15const LoadExternalConfig = require('poi-load-config')
16const loadPoiConfig = require('poi-load-config/poi')
17const AppError = require('../lib/app-error')
18const { cwd, ownDir, inferHTML, readPkg } = require('../lib/utils')
19const poi = require('../lib')
20const terminal = require('../lib/terminal-utils')
21const logger = require('../lib/logger')
22
23const unspecifiedAddress = host => host === '0.0.0.0' || host === '::'
24
25module.exports = co.wrap(function * (cliOptions) {
26 console.log(`> Running in ${cliOptions.mode} mode`)
27 if (!process.env.NODE_ENV) {
28 // env could be `production` `development` `test`
29 process.env.NODE_ENV = cliOptions.mode === 'watch' ? 'development' : cliOptions.mode
30 }
31
32 let { path: configPath, config = {} } = yield loadPoiConfig({ config: cliOptions.config })
33
34 if (configPath) {
35 console.log(`> Using external Poi config file`)
36 console.log(chalk.dim(`> location: "${tildify(configPath)}"`))
37 config = handleConfig(config, cliOptions)
38 } else if (cliOptions.config) {
39 throw new AppError('Config file was not found!')
40 }
41
42 const options = merge(config, cliOptions)
43
44 const clear = () => options.clear !== false && terminal.clear()
45
46 const printStats = stats => {
47 clear()
48 if (stats.hasErrors() || stats.hasWarnings()) {
49 console.log(stats.toString('errors-only').trim())
50 process.exitCode = 1
51 } else {
52 console.log(stats.toString({
53 colors: true,
54 chunks: false,
55 modules: false,
56 children: false,
57 version: false,
58 hash: false,
59 timings: false
60 }))
61 process.exitCode = 0
62 }
63 }
64
65 let copied
66 let lanIP
67
68 const printOutro = (stats, host, port) => {
69 console.log()
70 if (stats.hasErrors()) {
71 logger.error('Compiled with Errors!')
72 } else if (stats.hasWarnings()) {
73 logger.warn('Compiled with Warnings!')
74 } else {
75 if (options.mode === 'development') {
76 const isUnspecifiedAddress = unspecifiedAddress(host)
77 const localURL = url.format({
78 protocol: 'http',
79 hostname: isUnspecifiedAddress ? 'localhost' : host,
80 port
81 })
82 if (copied) {
83 console.log(chalk.bold(`> Open ${localURL}`))
84 } else {
85 copied = true
86 try {
87 copy.writeSync(localURL)
88 console.log(chalk.bold(`> Open ${localURL}`), chalk.dim('(copied!)'))
89 } catch (err) {
90 console.log(chalk.bold(`> Open ${localURL}`))
91 }
92 }
93 if (isUnspecifiedAddress) {
94 const lanURL = url.format({
95 protocol: 'http',
96 hostname: lanIP || (lanIP = address.ip()),
97 port
98 })
99 console.log(chalk.dim(`> On Your Network: ${lanURL}`))
100 }
101 console.log()
102 }
103 logger.success(`Build ${stats.hash.slice(0, 6)} finished in ${stats.endTime - stats.startTime} ms!`)
104 }
105 console.log()
106 }
107
108 const loadExternalConfig = new LoadExternalConfig({ cwd: options.cwd })
109
110 if (options.babel === undefined) {
111 const { useConfig, file } = yield loadExternalConfig.babel(buildConfigChain)
112 if (useConfig) {
113 console.log('> Using external babel configuration')
114 console.log(chalk.dim(`> location: "${tildify(file)}"`))
115 options.babel = {
116 cacheDirectory: true,
117 babelrc: true
118 }
119 } else {
120 options.babel = {
121 cacheDirectory: true,
122 babelrc: false
123 }
124 }
125 if (options.babel.babelrc === false) {
126 // Use our default preset when no babelrc was found
127 options.babel.presets = [
128 [require.resolve('babel-preset-vue-app'), { useBuiltIns: true }]
129 ]
130 }
131 }
132
133 if (options.postcss === undefined) {
134 const postcssConfig = yield loadExternalConfig.postcss()
135 if (postcssConfig.file) {
136 console.log('> Using extenal postcss configuration')
137 console.log(chalk.dim(`> location: "${tildify(postcssConfig.file)}"`))
138 options.postcss = postcssConfig
139 }
140 }
141
142 if (options.html === undefined) {
143 console.log(`> Using inferred value from package.json for HTML file`)
144 options.html = inferHTML(options)
145 }
146
147 if (options.entry === undefined) {
148 const mainField = readPkg().main
149 if (mainField) {
150 console.log(`> Using main field in package.json as entry point`)
151 options.entry = mainField
152 }
153 }
154
155 if (options.homepage === undefined && options.mode === 'production') {
156 options.homepage = readPkg().homepage
157 }
158
159 const { browserslist = ['ie > 8', 'last 2 versions'] } = readPkg()
160
161 options.autoprefixer = Object.assign({
162 browsers: browserslist
163 }, options.autoprefixer)
164
165 deleteExtraOptions(options, [
166 '_',
167 '$0',
168 'inspectOptions',
169 'inspect-options',
170 'v',
171 'version',
172 'h',
173 'help'
174 ])
175
176 if (cliOptions.inspectOptions) {
177 console.log('> Options:', util.inspect(options, { colors: true, depth: null }))
178 }
179
180 const app = poi(options)
181
182 console.log(`> Bundling with Webpack ${require('webpack/package.json').version}`)
183
184 if (options.mode === 'production') {
185 clear()
186 console.log('> Creating an optimized production build:\n')
187 const stats = yield app.build()
188 printStats(stats)
189 printOutro(stats)
190 if (options.generateStats) {
191 const statsFile = cwd(options.cwd, typeof options.generateStats === 'string' ? options.generateStats : 'stats.json')
192 console.log('> Generating webpack stats file')
193 fs.writeFileSync(statsFile, JSON.stringify(stats.toJson()), 'utf8')
194 console.log(chalk.dim(`> location: "${tildify(statsFile)}"`))
195 }
196 } else if (options.mode === 'watch') {
197 yield app.watch()
198 app.once('compile-done', () => {
199 console.log()
200 })
201 app.on('compile-done', stats => {
202 printStats(stats)
203 printOutro(stats)
204 })
205 } else if (options.mode === 'development') {
206 const { server, host, port } = yield app.dev()
207
208 server.listen(port, host)
209 .on('error', err => {
210 if (err.code === 'EADDRINUSE') {
211 return handleError(new AppError(`Port ${port} is already in use.\n\nYou can use another one by adding \`--port <port>\` or set it in config file.`))
212 }
213 handleError(err)
214 })
215
216 app.once('compile-done', () => {
217 if (options.open) {
218 opn(url.format({
219 protocol: 'http',
220 hostname: unspecifiedAddress(host) ? 'localhost' : host,
221 port
222 }))
223 }
224 })
225
226 app.on('compile-done', stats => {
227 printStats(stats)
228 printOutro(stats, host, port)
229 })
230 } else if (options.mode === 'test') {
231 app.test().catch(handleError)
232 }
233})
234
235module.exports.handleError = handleError
236
237function handleError(err) {
238 console.log()
239 if (err.name === 'AppError') {
240 console.error(chalk.red(err.message))
241 } else {
242 console.error(err.stack.trim())
243 }
244 notifier.notify({
245 title: 'Poi: error!',
246 message: stripAnsi(err.stack).replace(/^\s+/gm, ''),
247 icon: ownDir('bin/error.png')
248 })
249 console.log()
250 logger.error('Failed to start!')
251 console.log()
252 process.exit(1)
253}
254
255function handleConfig(config, options) {
256 if (typeof config === 'function') {
257 config = config(options, require)
258 }
259
260 config = merge(config, config[options.mode])
261
262 delete config.development
263 delete config.production
264 delete config.watch
265 delete config.test
266
267 return config
268}
269
270function deleteExtraOptions(obj, arr) {
271 arr.forEach(k => delete obj[k])
272}