UNPKG

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