1 |
|
2 | const util = require('util')
|
3 | const fs = require('fs')
|
4 | const url = require('url')
|
5 | const chalk = require('chalk')
|
6 | const notifier = require('node-notifier')
|
7 | const co = require('co')
|
8 | const stripAnsi = require('strip-ansi')
|
9 | const tildify = require('tildify')
|
10 | const address = require('address')
|
11 | const merge = require('lodash.merge')
|
12 | const copy = require('clipboardy')
|
13 | const opn = require('opn')
|
14 | const buildConfigChain = require('babel-core/lib/transformation/file/options/build-config-chain')
|
15 | const LoadExternalConfig = require('poi-load-config')
|
16 | const loadPoiConfig = require('poi-load-config/poi')
|
17 | const AppError = require('../lib/app-error')
|
18 | const { cwd, ownDir, inferHTML, readPkg } = require('../lib/utils')
|
19 | const poi = require('../lib')
|
20 | const terminal = require('../lib/terminal-utils')
|
21 | const logger = require('../lib/logger')
|
22 |
|
23 | const unspecifiedAddress = host => host === '0.0.0.0' || host === '::'
|
24 |
|
25 | module.exports = co.wrap(function * (cliOptions) {
|
26 | console.log(`> Running in ${cliOptions.mode} mode`)
|
27 | if (!process.env.NODE_ENV) {
|
28 |
|
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 |
|
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 |
|
235 | module.exports.handleError = handleError
|
236 |
|
237 | function 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 |
|
255 | function 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 |
|
270 | function deleteExtraOptions(obj, arr) {
|
271 | arr.forEach(k => delete obj[k])
|
272 | }
|