UNPKG

8.29 kBJavaScriptView Raw
1'use strict'
2
3// 确保在文件首部设置环境变量
4process.env.BABEL_ENV = 'production'
5process.env.NODE_ENV = 'production'
6
7process.on('unhandledRejection', err => {
8 throw err
9})
10
11const fs = require('fs-extra')
12const chalk = require('chalk')
13const path = require('path')
14const ora = require('ora')
15const webpack = require('webpack')
16const getEntry = require('../lib/entry')
17const { bumpProjectVersion, isInstalled } = require('../lib/utils')
18const { cliBadge } = require('@mara/devkit')
19const config = require('../config')
20const getBuildContext = require('../config/context')
21const { TARGET, DEPLOY_ENV } = require('../config/const')
22const paths = config.paths
23const getWebpackConfig = require('../webpack/webpack.prod.conf')
24const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages')
25const { hybridDevPublish, testDeploy } = require('../lib/hybrid')
26const printBuildError = require('../lib/printBuildError')
27const {
28 getLastBuildSize,
29 printBuildAssets,
30 getBuildSizeOfFileMap
31} = require('../lib/buildReporter')
32const prehandleConfig = require('../lib/prehandleConfig')
33const isHybridMode = config.hybrid && config.target === TARGET.APP
34
35const { name: projectName, version: latestVersion } = require(config.paths
36 .packageJson)
37// hybrid 模式下 ftp 发布将自动更新 package version
38// 此变量记录更新后的版本号
39let currentVersion = latestVersion
40
41// These sizes are pretty large. We'll warn for bundles exceeding them.
42const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024
43const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024
44
45const spinner = ora('Building for production...')
46
47// entryInput: {entry, ftpBranch, entryArgs}
48async function createContext(entryInput) {
49 const isHybridPublish =
50 config.ftp.hybridPublish && entryInput.ftpBranch !== null
51 const enableAutoVersion = config.ftp.hybridAutoVersion
52 const shouldBumpVersion = enableAutoVersion && isHybridMode && isHybridPublish
53
54 // hybrid dev 发布模式下版本号自动递增
55 if (shouldBumpVersion) {
56 // e.g. v1.2.3-1
57 const { stdout } = bumpProjectVersion('prerelease')
58
59 // 记录最新版本
60 currentVersion = stdout.replace(/^v/, '')
61 }
62
63 return getBuildContext({
64 version: currentVersion,
65 view: entryInput.entry
66 })
67
68 // return { context, ...entryInput }
69}
70
71async function clean(dist) {
72 return fs.emptyDir(dist)
73}
74
75function build(context) {
76 let webpackConfig = getWebpackConfig(context, spinner)
77
78 webpackConfig = prehandleConfig({
79 command: 'build',
80 webpackConfig,
81 entry: context.entry
82 })
83
84 const compiler = webpack(webpackConfig)
85
86 return new Promise((resolve, reject) => {
87 compiler.run((err, stats) => {
88 let messages
89 spinner.stop()
90
91 if (err) {
92 if (!err.message) return reject(err)
93
94 messages = formatWebpackMessages({
95 errors: [err.message],
96 warnings: []
97 })
98 } else {
99 messages = formatWebpackMessages(
100 stats.toJson({ all: false, warnings: true, errors: true })
101 )
102 }
103
104 if (messages.errors.length) {
105 // Only keep the first error. Others are often indicative
106 // of the same problem, but confuse the reader with noise.
107 messages.errors.length = 1
108
109 return reject(new Error(messages.errors.join('\n\n')))
110 }
111
112 if (
113 process.env.CI &&
114 (typeof process.env.CI !== 'string' ||
115 process.env.CI.toLowerCase() !== 'false') &&
116 messages.warnings.length
117 ) {
118 console.log(
119 chalk.yellow(
120 '\nTreating warnings as errors because process.env.CI = true.\n' +
121 'Most CI servers set it automatically.\n'
122 )
123 )
124
125 return reject(new Error(messages.warnings.join('\n\n')))
126 }
127
128 const tinifyOriginSizes = getBuildSizeOfFileMap(compiler._tinifySourceMap)
129
130 return resolve({
131 stats,
132 sizes: tinifyOriginSizes,
133 publicPath: webpackConfig.output.publicPath,
134 outputPath: webpackConfig.output.path,
135 warnings: messages.warnings
136 })
137 })
138 })
139}
140
141function printResult(
142 { stats, sizes, publicPath, outputPath, warnings },
143 context,
144 preBuildSize
145) {
146 const result = stats.toJson({
147 hash: false,
148 chunks: false,
149 modules: false,
150 chunkModules: false
151 })
152
153 if (warnings.length) {
154 console.log(chalk.yellow('Compiled with warnings:\n'))
155 console.log(warnings.join('\n\n'))
156 // add new line
157 console.log()
158 }
159
160 let buildTime = result.time
161
162 if (buildTime < 1000) {
163 buildTime += 'ms'
164 } else {
165 buildTime = buildTime / 1000 + 's'
166 }
167
168 console.log(chalk.green(`Compiled successfully in ${buildTime}\n`))
169 console.log('File sizes after gzip:\n')
170
171 result.assets['__dist'] = outputPath
172 preBuildSize.sizes = Object.assign({}, preBuildSize.sizes, sizes)
173
174 printBuildAssets(
175 // view 为数组
176 { view: [result.assets] },
177 preBuildSize,
178 WARN_AFTER_BUNDLE_GZIP_SIZE,
179 WARN_AFTER_CHUNK_GZIP_SIZE
180 )
181
182 // just new line
183 console.log()
184 const targetBadge = cliBadge('target', config.target)
185 const envBadge = cliBadge(
186 'env',
187 config.deployEnv,
188 config.deployEnv === DEPLOY_ENV.ONLINE ? 'info' : 'warning'
189 )
190 console.log(`${targetBadge} ${envBadge}`)
191
192 console.log()
193 console.log(
194 `The ${chalk.cyan(
195 'dist/' + context.entry
196 )} folder is ready to be deployed.\n`
197 )
198
199 if (publicPath === '/') {
200 console.log(
201 chalk.yellow(
202 `The app is built assuming that it will be deployed at the root of a domain.`
203 )
204 )
205 console.log(
206 chalk.yellow(
207 `If you intend to deploy it under a subpath, update the ${chalk.green(
208 'publicPath'
209 )} option in your project config (${chalk.cyan(
210 `marauder.config.js`
211 )}).\n`
212 )
213 )
214 }
215}
216
217async function ftpUpload(entryInput, context) {
218 if (entryInput.ftpBranch === null) return ''
219
220 const remotePath = await require('../lib/ftp').uploadDir({
221 project: projectName,
222 version: context.version,
223 view: entryInput.entry,
224 namespace: entryInput.ftpBranch,
225 target: config.target
226 })
227
228 return remotePath
229}
230
231async function deploy({ entry, entryArgs }, remotePath) {
232 // hybrid deplpy 需提供 hybrid 配置
233 // 并且为 app 模式
234 if (isHybridMode && config.ftp.hybridPublish && remotePath) {
235 await hybridDevPublish(entry, remotePath, currentVersion)
236 } else if (entryArgs.test !== null) {
237 await testDeploy(entry, currentVersion, entryArgs.test)
238 }
239}
240
241// finally fn
242function done() {
243 const date = new Date()
244 const hour = date.getHours()
245
246 if (config.marax.inspire || hour >= 21) {
247 const { inspire } = require('@mara/devkit')
248 const quote = inspire.random()
249
250 console.log(chalk.magenta('☕️ ' + quote))
251 }
252}
253
254function printError(err) {
255 // 构建中途报错将直接被 error 捕获
256 // 这里确保 spinner 被及时关闭
257 spinner.stop()
258
259 if (currentVersion !== latestVersion) {
260 // 回滚自动设置的版本号
261 bumpProjectVersion(latestVersion)
262 }
263
264 console.log(chalk.red('\n🕳 Failed to compile.\n'))
265 printBuildError(err)
266 process.exit(1)
267}
268
269async function loadHook(argv, context) {
270 const hookName = argv.hook
271
272 if (hookName && isInstalled(`../hooks/${hookName}`)) {
273 const mod = require(`../hooks/${hookName}`)
274
275 console.log(`Execute hook - ${hookName}...\n`)
276
277 if (typeof mod === 'function') await mod(argv, context)
278 } else if (hookName !== undefined) {
279 console.log(chalk.red('Can not find hook', hookName))
280 }
281}
282
283async function run(argv) {
284 // Make sure to force cancel
285 ;['SIGINT', 'SIGTERM'].forEach(sig => {
286 process.on(sig, () => {
287 process.exit()
288 })
289 })
290
291 const entryInput = await getEntry(argv)
292 const dist = path.join(paths.dist, entryInput.entry)
293
294 spinner.start()
295
296 const context = await createContext(entryInput)
297 const preBuildSize = await getLastBuildSize(dist)
298
299 await clean(dist)
300
301 const buildResult = await build(context)
302 printResult(buildResult, context, preBuildSize)
303
304 await loadHook(argv, context)
305
306 const remotePath = await ftpUpload(entryInput, context)
307 await deploy(entryInput, remotePath)
308}
309
310module.exports = async function runBuild(argv) {
311 try {
312 await run(argv).then(done)
313 } catch (e) {
314 printError(e)
315 }
316}