1 |
|
2 | const fs = require('fs')
|
3 | const url = require('url')
|
4 | const chalk = require('chalk')
|
5 | const notifier = require('node-notifier')
|
6 | const co = require('co')
|
7 | const stripAnsi = require('strip-ansi')
|
8 | const tildify = require('tildify')
|
9 | const address = require('address')
|
10 | const merge = require('lodash.merge')
|
11 | const findBabelConfig = require('babel-load-config')
|
12 | const findPostcssConfig = require('postcss-load-config')
|
13 | const copy = require('clipboardy')
|
14 | const opn = require('opn')
|
15 | const AppError = require('../lib/app-error')
|
16 | const { cwd, ownDir, inferHTML, readPkg, parsePresets } = require('../lib/utils')
|
17 | const loadConfig = require('../lib/load-config')
|
18 | const poi = require('../lib')
|
19 | const terminal = require('../lib/terminal-utils')
|
20 | const logger = require('../lib/logger')
|
21 |
|
22 | const unspecifiedAddress = host => host === '0.0.0.0' || host === '::'
|
23 |
|
24 | const loadPostCSSConfig = co.wrap(function * () {
|
25 | let defaultPostcssOptions = {}
|
26 | try {
|
27 | defaultPostcssOptions = yield findPostcssConfig({}, null, { argv: false })
|
28 | .then(res => {
|
29 | console.log('> Using extenal postcss configuration')
|
30 | console.log(chalk.dim(`> location: "${tildify(res.file)}"`))
|
31 | return res
|
32 | })
|
33 | } catch (err) {
|
34 | if (err.message.indexOf('No PostCSS Config found') === -1) {
|
35 | throw err
|
36 | }
|
37 | }
|
38 |
|
39 | return defaultPostcssOptions
|
40 | })
|
41 |
|
42 | const loadBabelConfig = function () {
|
43 | const defaultBabelOptions = {
|
44 | babelrc: true,
|
45 | cacheDirectory: true
|
46 | }
|
47 |
|
48 | const externalBabelConfig = findBabelConfig(process.cwd())
|
49 | if (externalBabelConfig) {
|
50 | console.log('> Using external babel configuration')
|
51 | console.log(chalk.dim(`> location: "${tildify(externalBabelConfig.loc)}"`))
|
52 |
|
53 |
|
54 |
|
55 | const { options } = externalBabelConfig
|
56 | defaultBabelOptions.babelrc = options.babelrc !== false
|
57 | } else {
|
58 | defaultBabelOptions.babelrc = false
|
59 | }
|
60 |
|
61 |
|
62 | if (!defaultBabelOptions.babelrc) {
|
63 | defaultBabelOptions.presets = [
|
64 | [require.resolve('babel-preset-vue-app'), {
|
65 | useBuiltIns: true
|
66 | }]
|
67 | ]
|
68 | }
|
69 |
|
70 | return defaultBabelOptions
|
71 | }
|
72 |
|
73 | module.exports = co.wrap(function * (cliOptions) {
|
74 | console.log(`> Running in ${cliOptions.mode} mode`)
|
75 |
|
76 | const config = yield loadConfig(cliOptions)
|
77 | const options = merge(config, cliOptions)
|
78 |
|
79 | const clear = () => options.clear !== false && terminal.clear()
|
80 |
|
81 | const printStats = stats => {
|
82 | clear()
|
83 | if (stats.hasErrors() || stats.hasWarnings()) {
|
84 | console.log(stats.toString('errors-only').trim())
|
85 | process.exitCode = 1
|
86 | } else {
|
87 | console.log(stats.toString({
|
88 | colors: true,
|
89 | chunks: false,
|
90 | modules: false,
|
91 | children: false,
|
92 | version: false,
|
93 | hash: false,
|
94 | timings: false
|
95 | }))
|
96 | process.exitCode = 0
|
97 | }
|
98 | }
|
99 |
|
100 | let copied
|
101 | let lanIP
|
102 |
|
103 | const printOutro = (stats, host, port) => {
|
104 | console.log()
|
105 | if (stats.hasErrors()) {
|
106 | logger.error('Compiled with Errors!')
|
107 | } else if (stats.hasWarnings()) {
|
108 | logger.warn('Compiled with Warnings!')
|
109 | } else {
|
110 | if (options.mode === 'development') {
|
111 | const isUnspecifiedAddress = unspecifiedAddress(host)
|
112 | const localURL = url.format({
|
113 | protocol: 'http',
|
114 | hostname: isUnspecifiedAddress ? 'localhost' : host,
|
115 | port
|
116 | })
|
117 | if (copied) {
|
118 | console.log(chalk.bold(`> Open ${localURL}`))
|
119 | } else {
|
120 | copied = true
|
121 | try {
|
122 | copy.writeSync(localURL)
|
123 | console.log(chalk.bold(`> Open ${localURL}`), chalk.dim('(copied!)'))
|
124 | } catch (err) {
|
125 | console.log(chalk.bold(`> Open ${localURL}`))
|
126 | }
|
127 | }
|
128 | if (isUnspecifiedAddress) {
|
129 | const lanURL = url.format({
|
130 | protocol: 'http',
|
131 | hostname: lanIP || (lanIP = address.ip()),
|
132 | port
|
133 | })
|
134 | console.log(chalk.dim(`> On Your Network: ${lanURL}`))
|
135 | }
|
136 | console.log()
|
137 | }
|
138 | logger.success(`Build ${stats.hash.slice(0, 6)} finished in ${stats.endTime - stats.startTime} ms!`)
|
139 | }
|
140 | console.log()
|
141 | }
|
142 |
|
143 | if (options.presets) {
|
144 | options.presets = parsePresets(options.presets)
|
145 | }
|
146 |
|
147 | if (options.babel === undefined) {
|
148 | options.babel = loadBabelConfig()
|
149 | }
|
150 |
|
151 | if (options.postcss === undefined) {
|
152 | options.postcss = yield loadPostCSSConfig()
|
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 app = poi(options)
|
173 |
|
174 | console.log(`> Bundling with Webpack ${require('webpack/package.json').version}`)
|
175 |
|
176 | if (options.mode === 'production') {
|
177 | clear()
|
178 | console.log('> Creating an optimized production build:\n')
|
179 | const stats = yield app.build()
|
180 | printStats(stats)
|
181 | printOutro(stats)
|
182 | if (options.generateStats) {
|
183 | const statsFile = cwd(options.cwd, typeof options.generateStats === 'string' ? options.generateStats : 'stats.json')
|
184 | console.log('> Generating webpack stats file')
|
185 | fs.writeFileSync(statsFile, JSON.stringify(stats.toJson()), 'utf8')
|
186 | console.log(chalk.dim(`> location: "${tildify(statsFile)}"`))
|
187 | }
|
188 | } else if (options.mode === 'watch') {
|
189 | yield app.watch()
|
190 | app.once('compile-done', () => {
|
191 | console.log()
|
192 | })
|
193 | app.on('compile-done', stats => {
|
194 | printStats(stats)
|
195 | printOutro(stats)
|
196 | })
|
197 | } else if (options.mode === 'development') {
|
198 | const { server, host, port } = yield app.dev()
|
199 |
|
200 | server.listen(port, host)
|
201 | .on('error', err => {
|
202 | if (err.code === 'EADDRINUSE') {
|
203 | 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.`))
|
204 | }
|
205 | handleError(err)
|
206 | })
|
207 |
|
208 | app.once('compile-done', () => {
|
209 | if (options.open) {
|
210 | opn(url.format({
|
211 | protocol: 'http',
|
212 | hostname: unspecifiedAddress(host) ? 'localhost' : host,
|
213 | port
|
214 | }))
|
215 | }
|
216 | })
|
217 |
|
218 | app.on('compile-done', stats => {
|
219 | printStats(stats)
|
220 | printOutro(stats, host, port)
|
221 | })
|
222 | } else if (options.mode === 'test') {
|
223 | app.test().catch(handleError)
|
224 | }
|
225 | })
|
226 |
|
227 | module.exports.handleError = handleError
|
228 |
|
229 | function handleError(err) {
|
230 | console.log()
|
231 | if (err.name === 'AppError') {
|
232 | console.error(chalk.red(err.message))
|
233 | } else {
|
234 | console.error(err.stack.trim())
|
235 | }
|
236 | notifier.notify({
|
237 | title: 'Poi: error!',
|
238 | message: stripAnsi(err.stack).replace(/^\s+/gm, ''),
|
239 | icon: ownDir('bin/error.png')
|
240 | })
|
241 | console.log()
|
242 | logger.error('Failed to start!')
|
243 | console.log()
|
244 | process.exit(1)
|
245 | }
|