UNPKG

13.5 kBJavaScriptView Raw
1'use strict'
2
3const fs = require('fs')
4const path = require('path')
5const webpack = require('webpack')
6const merge = require('webpack-merge')
7const chalk = require('chalk')
8const { localIp, isObject } = require('@mara/devkit')
9const TerserPlugin = require('terser-webpack-plugin')
10const CopyWebpackPlugin = require('copy-webpack-plugin')
11const HtmlWebpackPlugin = require('html-webpack-plugin')
12const safePostCssParser = require('postcss-safe-parser')
13const MiniCssExtractPlugin = require('mini-css-extract-plugin')
14const moduleDependency = require('sinamfe-webpack-module_dependency')
15const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
16const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin')
17const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin')
18
19// const { HybridCommonPlugin } = require('../lib/hybrid')
20const { banner, rootPath, getEntryPoints } = require('../lib/utils')
21const BuildProgressPlugin = require('../lib/BuildProgressPlugin')
22const InlineUmdHtmlPlugin = require('../lib/InlineUmdHtmlPlugin')
23const { GLOB, VIEWS_DIR, DLL_DIR, TARGET } = require('../config/const')
24const ManifestPlugin = require('../lib/hybrid/ManifestPlugin')
25const BuildJsonPlugin = require('../lib/BuildJsonPlugin')
26const ZenJsPlugin = require('../lib/ZenJsPlugin')
27const config = require('../config')
28
29const shouldUseSourceMap = !!config.build.sourceMap
30
31/**
32 * 生成生产配置
33 * @param {String} context 构建上下文
34 * @param {String} spinner loading
35 * @return {Object} webpack 配置对象
36 */
37module.exports = function(context, spinner) {
38 const entry = context.entry
39 const distPageDir = `${config.paths.dist}/${entry}`
40 const baseWebpackConfig = require('./webpack.base.conf')(context, 'build')
41 const htmlTemplatePath = `${config.paths.views}/${entry}/index.html`
42 const hasHtml = fs.existsSync(htmlTemplatePath)
43 const servantEntry = getEntryPoints(
44 `${VIEWS_DIR}/${entry}/${GLOB.SERVANT_ENTRY}`
45 )
46 const debugLabel = config.debug ? '.debug' : ''
47 const isHybridMode = context.target === TARGET.APP
48 const shouldUseZenJs = config.compiler.zenJs && context.target != TARGET.APP
49
50 // 优先取外部注入的 version
51 const buildVersion =
52 context.version || require(config.paths.packageJson).version
53
54 // https://github.com/survivejs/webpack-merge
55 const webpackConfig = merge(baseWebpackConfig, {
56 mode: 'production',
57 // 在第一个错误出错时抛出,而不是无视错误
58 bail: true,
59 devtool: shouldUseSourceMap ? 'source-map' : false,
60 // merge base.config entry
61 entry: servantEntry,
62 output: {
63 path: distPageDir,
64 publicPath: context.publicPath,
65 // 保持传统,非 debug 的 main js 添加 min 后缀
66 filename: config.hash.main
67 ? `static/js/[name].[contenthash:8]${debugLabel}.js`
68 : `static/js/[name]${debugLabel || '.min'}.js`,
69 chunkFilename: config.hash.chunk
70 ? `static/js/[name].[contenthash:8].chunk${debugLabel}.js`
71 : `static/js/[name].chunk${debugLabel}.js`,
72 // Point sourcemap entres to original disk location (format as URL on Windows)
73 devtoolModuleFilenameTemplate: info =>
74 path
75 .relative(config.paths.src, info.absoluteResourcePath)
76 .replace(/\\/g, '/')
77 },
78 // 放在单独的 prod.conf 中,保持 base.conf 通用性
79 optimization: {
80 minimize: config.debug !== true,
81 minimizer: [
82 new TerserPlugin({
83 terserOptions: {
84 parse: {
85 // we want terser to parse ecma 8 code. However, we don't want it
86 // to apply any minfication steps that turns valid ecma 5 code
87 // into invalid ecma 5 code. This is why the 'compress' and 'output'
88 // sections only apply transformations that are ecma 5 safe
89 // https://github.com/facebook/create-react-app/pull/4234
90 ecma: 8
91 },
92 compress: {
93 ecma: 5,
94 warnings: false,
95 // Disabled because of an issue with Uglify breaking seemingly valid code:
96 // https://github.com/facebook/create-react-app/issues/2376
97 // Pending further investigation:
98 // https://github.com/mishoo/UglifyJS2/issues/2011
99 comparisons: false,
100 // Disabled because of an issue with Terser breaking valid code:
101 // https://github.com/facebook/create-react-app/issues/5250
102 // Pending futher investigation:
103 // https://github.com/terser-js/terser/issues/120
104 inline: 2
105 },
106 mangle: {
107 safari10: true
108 },
109 output: {
110 ecma: 5,
111 comments: false,
112 // Turned on because emoji and regex is not minified properly using default
113 // https://github.com/facebook/create-react-app/issues/2488
114 ascii_only: true
115 }
116 },
117 // Use multi-process parallel running to improve the build speed
118 // Default number of concurrent runs: os.cpus().length - 1
119 parallel: true,
120 // Enable file caching
121 cache: true,
122 sourceMap: shouldUseSourceMap
123 }),
124 new OptimizeCSSAssetsPlugin({
125 cssProcessorOptions: {
126 parser: safePostCssParser,
127 map: shouldUseSourceMap
128 ? {
129 // `inline: false` forces the sourcemap to be output into a
130 // separate file
131 inline: false,
132 // `annotation: true` appends the sourceMappingURL to the end of
133 // the css file, helping the browser find the sourcemap
134 annotation: true
135 }
136 : false
137 },
138 canPrint: false // 不显示通知
139 })
140 ],
141 // Keep the runtime chunk seperated to enable long term caching
142 // https://twitter.com/wSokra/status/969679223278505985
143 // set false until https://github.com/webpack/webpack/issues/6598 be resolved
144 runtimeChunk: false
145 },
146 plugins: [
147 // 由于 base.conf 会被外部引用,在一些情况下不需要 ProgressPlugin
148 // 因此独立放在 prod.conf 中
149 spinner &&
150 new BuildProgressPlugin({
151 spinner,
152 name: 'Building',
153 type: config.marax.progress
154 }),
155 hasHtml &&
156 new HtmlWebpackPlugin({
157 // prod 模式以 index.html 命名输出
158 filename: 'index.html',
159 // 每个html的模版,这里多个页面使用同一个模版
160 template: htmlTemplatePath,
161 // 自动将引用插入html
162 inject: true,
163 minify: {
164 removeRedundantAttributes: true,
165 useShortDoctype: true,
166 removeEmptyAttributes: true,
167 removeStyleLinkTypeAttributes: true,
168 keepClosingSlash: true,
169 minifyJS: true,
170 minifyCSS: true,
171 minifyURLs: true
172 }
173 }),
174 hasHtml && new InlineUmdHtmlPlugin(HtmlWebpackPlugin),
175 hasHtml && shouldUseZenJs && new ZenJsPlugin(HtmlWebpackPlugin),
176 hasHtml &&
177 new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),
178 hasHtml &&
179 new InterpolateHtmlPlugin(HtmlWebpackPlugin, context.buildEnv.raw),
180 new MiniCssExtractPlugin({
181 // 保持传统,非 debug 的 main css 添加 min 后缀
182 filename: config.hash.main
183 ? `static/css/[name].[contenthash:8]${debugLabel}.css`
184 : `static/css/[name]${debugLabel || '.min'}.css`,
185 chunkFilename: config.hash.chunk
186 ? `static/css/[name].[contenthash:8].chunk${debugLabel}.css`
187 : `static/css/[name].chunk${debugLabel}.css`
188 }),
189 // hybrid 共享包
190 // 创建 maraContext
191 // new HybridCommonPlugin(),
192
193 // 【争议】:lib 模式禁用依赖分析?
194 new moduleDependency({
195 emitError: config.compiler.checkDuplicatePackage
196 }),
197 new webpack.BannerPlugin({
198 banner: banner(buildVersion, context.target), // 其值为字符串,将作为注释存在
199 entryOnly: false // 如果值为 true,将只在入口 chunks 文件中添加
200 }),
201 new BuildJsonPlugin({
202 debug: config.debug,
203 target: context.target,
204 env: config.deployEnv,
205 version: buildVersion,
206 marax: require(config.paths.maraxPackageJson).version
207 }),
208 new ManifestPlugin({
209 entry,
210 version: context.version,
211 target: context.target
212 }),
213 ...copyPublicFiles(entry, distPageDir)
214 ].filter(Boolean)
215 })
216
217 // 预加载
218 if (config.prerender) {
219 const PrerenderSPAPlugin = require('prerender-html-plugin')
220 const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
221
222 new PrerenderSPAPlugin({
223 // 生成文件的路径,也可以与webpakc打包的一致。
224 // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
225 entry: `${entry}`,
226
227 staticDir: path.join(rootPath(`dist`), `${entry}`),
228
229 outputDir: path.join(rootPath(`dist`), `${entry}`),
230
231 // 对应自己的路由文件,比如index有参数,就需要写成 /index/param1。
232 routes: ['/'],
233
234 // 这个很重要,如果没有配置这段,也不会进行预编译
235 renderer: new Renderer({
236 inject: {
237 foo: 'bar'
238 },
239 headless: false,
240 // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
241 renderAfterDocumentEvent: 'render-event'
242 })
243 })
244 }
245
246 const vendorConf = config.vendor || []
247 if (Object.keys(vendorConf).length) {
248 if (isObject(vendorConf) && !vendorConf.libs) {
249 console.log(
250 chalk.yellow(
251 'Build skip, vendor.libs is undefined. Please check marauder.config.js'
252 )
253 )
254 process.exit(0)
255 }
256
257 let manifest = ''
258 // 为多页面准备,生成 xxx_vender 文件夹
259 const namespace = config.vendor.name ? `${config.vendor.name}_` : ''
260
261 try {
262 manifest = require(`${config.paths.dll}/${namespace}manifest.json`)
263 } catch (err) {
264 console.log(
265 chalk.yellow(
266 `${DLL_DIR}/${namespace}manifest.json 未生成,请执行 npm run dll\n`
267 )
268 )
269 process.exit(1)
270 }
271
272 webpackConfig.plugins.push(
273 new webpack.DllReferencePlugin({
274 manifest: manifest
275 })
276 )
277 }
278
279 // bundle 大小分析
280 if (config.build.report || config.build.writeStatsJson) {
281 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
282
283 webpackConfig.plugins.push(
284 new BundleAnalyzerPlugin({
285 logLevel: 'warn',
286 analyzerHost: localIp(),
287 defaultSizes: 'gzip',
288 analyzerMode: config.build.report ? 'server' : 'disabled',
289 statsFilename: 'build-stats.json',
290 generateStatsFile: config.build.writeStatsJson
291 })
292 )
293 }
294
295 // @TODO publish npm module
296 // 生成serviceworker
297 // if (config.sw) {
298 // const webpackWS = require('@mfelib/webpack-create-serviceworker')
299 // const swConfig = config.sw_config || {}
300 // webpackConfig.plugins.push(new webpackWS(swConfig))
301 // }
302
303 // 重要:确保 zip plugin 在插件列表末尾
304 if (isHybridMode) {
305 const ZipPlugin = require('zip-webpack-plugin')
306 webpackConfig.plugins.push(
307 new ZipPlugin({
308 // OPTIONAL: defaults to the Webpack output filename (above) or,
309 // if not present, the basename of the path
310 filename: entry,
311 extension: 'php',
312 // OPTIONAL: defaults to including everything
313 // can be a string, a RegExp, or an array of strings and RegExps
314 // include: [/\.js$/],
315 // OPTIONAL: defaults to excluding nothing
316 // can be a string, a RegExp, or an array of strings and RegExps
317 // if a file matches both include and exclude, exclude takes precedence
318 exclude: [
319 /__MACOSX$/,
320 /.DS_Store$/,
321 /dependencyGraph.json$/,
322 /build.json$/,
323 /js.map$/,
324 /css.map$/
325 ],
326 // yazl Options
327 // OPTIONAL: see https://github.com/thejoshwolfe/yazl#addfilerealpath-metadatapath-options
328 fileOptions: {
329 mtime: new Date(),
330 mode: 0o100664,
331 compress: true,
332 forceZip64Format: false
333 },
334 // OPTIONAL: see https://github.com/thejoshwolfe/yazl#endoptions-finalsizecallback
335 zipOptions: {
336 forceZip64Format: false
337 }
338 })
339 )
340 }
341
342 return webpackConfig
343}
344
345function copyPublicFiles(entry, distPageDir) {
346 const localPublicDir = rootPath(`${config.paths.views}/${entry}/public`)
347 const plugins = []
348
349 function getCopyOption(src) {
350 return {
351 from: src,
352 // 放置于根路径
353 to: distPageDir,
354 // 忽略 manifest.json
355 // 交由 maraManifestPlugin 处理
356 ignore: ['.*', 'manifest.json']
357 }
358 }
359
360 // 全局 public
361 if (fs.existsSync(config.paths.public)) {
362 plugins.push(new CopyWebpackPlugin([getCopyOption(config.paths.public)]))
363 }
364
365 // 页面级 public,能够覆盖全局 public
366 if (fs.existsSync(localPublicDir)) {
367 plugins.push(new CopyWebpackPlugin([getCopyOption(localPublicDir)]))
368 }
369
370 return plugins
371}