1 | 'use strict'
|
2 |
|
3 |
|
4 | process.env.BABEL_ENV = 'production'
|
5 | process.env.NODE_ENV = 'production'
|
6 |
|
7 | process.on('unhandledRejection', err => {
|
8 | throw err
|
9 | })
|
10 |
|
11 | const fs = require('fs-extra')
|
12 | const chalk = require('chalk')
|
13 | const path = require('path')
|
14 | const ora = require('ora')
|
15 | const webpack = require('webpack')
|
16 | const getEntry = require('../lib/entry')
|
17 | const { bumpProjectVersion, isInstalled } = require('../lib/utils')
|
18 | const { cliBadge } = require('@mara/devkit')
|
19 | const config = require('../config')
|
20 | const getBuildContext = require('../config/context')
|
21 | const { TARGET, DEPLOY_ENV } = require('../config/const')
|
22 | const paths = config.paths
|
23 | const getWebpackConfig = require('../webpack/webpack.prod.conf')
|
24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages')
|
25 | const { hybridDevPublish, testDeploy } = require('../lib/hybrid')
|
26 | const printBuildError = require('../lib/printBuildError')
|
27 | const {
|
28 | getLastBuildSize,
|
29 | printBuildAssets,
|
30 | getBuildSizeOfFileMap
|
31 | } = require('../lib/buildReporter')
|
32 | const prehandleConfig = require('../lib/prehandleConfig')
|
33 | const isHybridMode = config.hybrid && config.target === TARGET.APP
|
34 |
|
35 | const { name: projectName, version: latestVersion } = require(config.paths
|
36 | .packageJson)
|
37 |
|
38 |
|
39 | let currentVersion = latestVersion
|
40 |
|
41 |
|
42 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024
|
43 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024
|
44 |
|
45 | const spinner = ora('Building for production...')
|
46 |
|
47 |
|
48 | async 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 |
|
55 | if (shouldBumpVersion) {
|
56 |
|
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 |
|
69 | }
|
70 |
|
71 | async function clean(dist) {
|
72 | return fs.emptyDir(dist)
|
73 | }
|
74 |
|
75 | function 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 |
|
106 |
|
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 |
|
141 | function 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 |
|
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 |
|
176 | { view: [result.assets] },
|
177 | preBuildSize,
|
178 | WARN_AFTER_BUNDLE_GZIP_SIZE,
|
179 | WARN_AFTER_CHUNK_GZIP_SIZE
|
180 | )
|
181 |
|
182 |
|
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 |
|
217 | async 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 |
|
231 | async function deploy({ entry, entryArgs }, remotePath) {
|
232 |
|
233 |
|
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 |
|
242 | function 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 |
|
254 | function printError(err) {
|
255 |
|
256 |
|
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 |
|
269 | async 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 |
|
283 | async function run(argv) {
|
284 |
|
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 |
|
310 | module.exports = async function runBuild(argv) {
|
311 | try {
|
312 | await run(argv).then(done)
|
313 | } catch (e) {
|
314 | printError(e)
|
315 | }
|
316 | }
|