UNPKG

10.8 kBJavaScriptView Raw
1const
2 path = require('path'),
3 webpack = require('webpack'),
4 WebpackChain = require('webpack-chain'),
5 VueLoaderPlugin = require('vue-loader/lib/plugin'),
6 WebpackProgress = require('./plugin.progress'),
7 BootDefaultExport = require('./plugin.boot-default-export')
8
9const
10 appPaths = require('../app-paths'),
11 injectStyleRules = require('./inject.style-rules')
12
13module.exports = function (cfg, configName) {
14 const
15 chain = new WebpackChain(),
16 needsHash = !cfg.ctx.dev && !['electron', 'cordova'].includes(cfg.ctx.modeName),
17 fileHash = needsHash ? '.[hash:8]' : '',
18 chunkHash = needsHash ? '.[contenthash:8]' : '',
19 resolveModules = [
20 'node_modules',
21 appPaths.resolve.app('node_modules'),
22 appPaths.resolve.cli('node_modules')
23 ]
24
25 chain.entry('app').add(appPaths.resolve.app('.quasar/client-entry.js'))
26 chain.mode(cfg.ctx.dev ? 'development' : 'production')
27 chain.devtool(cfg.build.sourceMap ? cfg.build.devtool : false)
28
29 if (cfg.ctx.prod || cfg.ctx.mode.ssr) {
30 chain.output
31 .path(
32 cfg.ctx.mode.ssr
33 ? path.join(cfg.build.distDir, 'www')
34 : cfg.build.distDir
35 )
36 .publicPath(cfg.build.publicPath)
37 .filename(`js/[name]${fileHash}.js`)
38 .chunkFilename(`js/[name]${chunkHash}.js`)
39 }
40
41 chain.resolve.symlinks(false)
42
43 chain.resolve.extensions
44 .merge([ '.js', '.vue', '.json' ])
45
46 chain.resolve.modules
47 .merge(resolveModules)
48
49 chain.resolve.alias
50 .merge({
51 src: appPaths.srcDir,
52 app: appPaths.appDir,
53 components: appPaths.resolve.src(`components`),
54 layouts: appPaths.resolve.src(`layouts`),
55 pages: appPaths.resolve.src(`pages`),
56 assets: appPaths.resolve.src(`assets`),
57 boot: appPaths.resolve.src(`boot`)
58 })
59
60 if (cfg.framework.all === true) {
61 chain.resolve.alias.set('quasar$', 'quasar/dist/quasar.esm.js')
62 }
63 if (cfg.build.vueCompiler) {
64 chain.resolve.alias.set('vue$', 'vue/dist/vue.esm.js')
65 }
66
67 chain.resolveLoader.modules
68 .merge(resolveModules)
69
70 chain.module.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)
71
72 chain.module.rule('vue')
73 .test(/\.vue$/)
74 .use('vue-loader')
75 .loader('vue-loader')
76 .options({
77 productionMode: cfg.ctx.prod,
78 compilerOptions: {
79 preserveWhitespace: false
80 },
81 transformAssetUrls: {
82 video: 'src',
83 source: 'src',
84 img: 'src',
85 image: 'xlink:href'
86 }
87 })
88
89 chain.module.rule('babel')
90 .test(/\.jsx?$/)
91 .exclude
92 .add(filepath => {
93 // always transpile js(x) in Vue files
94 if (/\.vue\.jsx?$/.test(filepath)) {
95 return false
96 }
97
98 if (filepath.match(/[\\/]node_modules[\\/]quasar[\\/]/)) {
99 if (configName === 'Server') {
100 // transpile only if not from 'quasar/dist' folder
101 if (!filepath.match(/[\\/]node_modules[\\/]quasar[\\/]dist/)) {
102 return false
103 }
104 }
105 else {
106 // always transpile Quasar
107 return false
108 }
109 }
110
111 if (cfg.build.transpileDependencies.some(dep => filepath.match(dep))) {
112 return false
113 }
114
115 // Don't transpile anything else in node_modules
116 return /[\\/]node_modules[\\/]/.test(filepath)
117 })
118 .end()
119 .use('babel-loader')
120 .loader('babel-loader')
121 .options({
122 extends: appPaths.resolve.app('babel.config.js'),
123 plugins: cfg.framework.all !== true && configName !== 'Server' ? [
124 [
125 'transform-imports', {
126 quasar: {
127 transform: `quasar/dist/babel-transforms/imports.js`,
128 preventFullImport: true
129 }
130 }
131 ]
132 ] : []
133 })
134
135 chain.module.rule('images')
136 .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
137 .use('url-loader')
138 .loader('url-loader')
139 .options({
140 limit: 10000,
141 name: `img/[name]${fileHash}.[ext]`
142 })
143
144 chain.module.rule('fonts')
145 .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/)
146 .use('url-loader')
147 .loader('url-loader')
148 .options({
149 limit: 10000,
150 name: `fonts/[name]${fileHash}.[ext]`
151 })
152
153 chain.module.rule('media')
154 .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
155 .use('url-loader')
156 .loader('url-loader')
157 .options({
158 limit: 10000,
159 name: `media/[name]${fileHash}.[ext]`
160 })
161
162 injectStyleRules(chain, {
163 rtl: cfg.build.rtl,
164 sourceMap: cfg.build.sourceMap,
165 extract: cfg.build.extractCSS,
166 minify: cfg.build.minify,
167 stylusLoaderOptions: cfg.build.stylusLoaderOptions,
168 sassLoaderOptions: cfg.build.sassLoaderOptions,
169 scssLoaderOptions: cfg.build.scssLoaderOptions,
170 lessLoaderOptions: cfg.build.lessLoaderOptions
171 })
172
173 chain.plugin('vue-loader')
174 .use(VueLoaderPlugin)
175
176 chain.plugin('define')
177 .use(webpack.DefinePlugin, [ cfg.build.env ])
178
179 if (cfg.build.showProgress) {
180 chain.plugin('progress')
181 .use(WebpackProgress, [{ name: configName }])
182 }
183
184 chain.plugin('boot-default-export')
185 .use(BootDefaultExport)
186
187 chain.performance
188 .hints(false)
189 .maxAssetSize(500000)
190
191 // DEVELOPMENT build
192 if (cfg.ctx.dev) {
193 const
194 FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'),
195 { devCompilationSuccess } = require('../helpers/banner')
196
197 chain.optimization
198 .noEmitOnErrors(true)
199
200 chain.plugin('friendly-errors')
201 .use(FriendlyErrorsPlugin, [{
202 clearConsole: true,
203 compilationSuccessInfo: ['spa', 'pwa', 'ssr'].includes(cfg.ctx.modeName)
204 ? { notes: [ devCompilationSuccess(cfg.ctx, cfg.build.APP_URL, appPaths.appDir) ] }
205 : undefined
206 }])
207 }
208 // PRODUCTION build
209 else {
210 // keep module.id stable when vendor modules does not change
211 chain.plugin('hashed-module-ids')
212 .use(webpack.HashedModuleIdsPlugin, [{
213 hashDigest: 'hex'
214 }])
215
216 // keep chunk ids stable so async chunks have consistent hash
217 const hash = require('hash-sum')
218 chain
219 .plugin('named-chunks')
220 .use(webpack.NamedChunksPlugin, [
221 chunk => chunk.name || hash(
222 Array.from(chunk.modulesIterable, m => m.id).join('_')
223 )
224 ])
225
226 if (configName !== 'Server') {
227 const
228 add = cfg.vendor.add,
229 rem = cfg.vendor.remove,
230 regex = /[\\/]node_modules[\\/]/
231
232 chain.optimization
233 .splitChunks({
234 cacheGroups: {
235 vendors: {
236 name: 'vendor',
237 chunks: 'initial',
238 priority: -10,
239 // a module is extracted into the vendor chunk if...
240 test: add.length > 0 || rem.length > 0
241 ? module => {
242 if (module.resource) {
243 if (add.length > 0 && add.test(module.resource)) { return true }
244 if (rem.length > 0 && rem.test(module.resource)) { return false }
245 }
246 return regex.test(module.resource)
247 }
248 : module => regex.test(module.resource)
249 },
250 common: {
251 name: `chunk-common`,
252 minChunks: 2,
253 priority: -20,
254 chunks: 'initial',
255 reuseExistingChunk: true
256 }
257 }
258 })
259
260 // extract webpack runtime and module manifest to its own file in order to
261 // prevent vendor hash from being updated whenever app bundle is updated
262 if (cfg.build.webpackManifest) {
263 chain.optimization.runtimeChunk('single')
264 }
265
266 // copy statics to dist folder
267 const CopyWebpackPlugin = require('copy-webpack-plugin')
268 chain.plugin('copy-webpack')
269 .use(CopyWebpackPlugin, [
270 [{
271 from: appPaths.resolve.src('statics'),
272 to: 'statics',
273 ignore: ['.*'],
274 ignore: ['.*'].concat(
275 // avoid useless files to be copied
276 ['electron', 'proton', 'cordova', 'capacitor'].includes(cfg.ctx.modeName)
277 ? [ 'icons/*', 'app-logo-128x128.png' ]
278 : []
279 )
280 }]
281 ])
282 }
283
284 // Scope hoisting ala Rollupjs
285 if (cfg.build.scopeHoisting) {
286 chain.optimization
287 .concatenateModules(true)
288 }
289
290 if (cfg.ctx.debug) {
291 // reset default webpack 4 minimizer
292 chain.optimization.minimizers.delete('js')
293 }
294 else if (cfg.build.minify) {
295 const TerserPlugin = require('terser-webpack-plugin')
296
297 chain.optimization
298 .minimizer('js')
299 .use(TerserPlugin, [{
300 terserOptions: cfg.build.uglifyOptions,
301 cache: true,
302 parallel: true,
303 sourceMap: cfg.build.sourceMap
304 }])
305 }
306
307 // configure CSS extraction & optimize
308 if (cfg.build.extractCSS) {
309 const MiniCssExtractPlugin = require('mini-css-extract-plugin')
310
311 // extract css into its own file
312 chain.plugin('mini-css-extract')
313 .use(MiniCssExtractPlugin, [{
314 filename: 'css/[name].[contenthash:8].css'
315 }])
316
317 // dedupe & minify CSS (only if extracted)
318 if (cfg.build.minify) {
319 const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
320
321 const cssProcessorOptions = {
322 parser: require('postcss-safe-parser'),
323 autoprefixer: { disable: true }
324 }
325 if (cfg.build.sourceMap) {
326 cssProcessorOptions.map = { inline: false }
327 }
328
329 // We are using this plugin so that possible
330 // duplicated CSS = require(different components) can be deduped.
331 chain.plugin('optimize-css')
332 .use(OptimizeCSSPlugin, [{
333 canPrint: false,
334 cssProcessor: require('cssnano'),
335 cssProcessorOptions,
336 cssProcessorPluginOptions: {
337 preset: ['default', {
338 mergeLonghand: false,
339 convertValues: false,
340 cssDeclarationSorter: false,
341 reduceTransforms: false
342 }]
343 }
344 }])
345 }
346 }
347
348 if (configName !== 'Server') {
349 // also produce a gzipped version
350 if (cfg.build.gzip) {
351 const CompressionWebpackPlugin = require('compression-webpack-plugin')
352 chain.plugin('compress-webpack')
353 .use(CompressionWebpackPlugin, [ cfg.build.gzip ])
354 }
355
356 if (cfg.build.analyze) {
357 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
358 chain.plugin('bundle-analyzer')
359 .use(BundleAnalyzerPlugin, [ Object.assign({}, cfg.build.analyze) ])
360 }
361 }
362 }
363
364 return chain
365}