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 | const handleWebpackErrors = require('../lib/webpack/handle-errors')
|
23 |
|
24 | const unspecifiedAddress = host => host === '0.0.0.0' || host === '::'
|
25 |
|
26 | module.exports = co.wrap(function * (cliOptions) {
|
27 | console.log(`> Running in ${cliOptions.mode} mode`)
|
28 | if (!process.env.NODE_ENV) {
|
29 |
|
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 |
|
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 |
|
248 | module.exports.handleError = handleError
|
249 |
|
250 | function 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 |
|
268 | function 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 |
|
283 | function deleteExtraOptions(obj, arr) {
|
284 | arr.forEach(k => delete obj[k])
|
285 | }
|