@quasar/app
Version:
Quasar Framework local CLI
381 lines (331 loc) • 11.3 kB
JavaScript
const path = require('path')
const webpack = require('webpack')
const WebpackChain = require('webpack-chain')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const WebpackProgress = require('./plugin.progress')
const BootDefaultExport = require('./plugin.boot-default-export')
const appPaths = require('../app-paths')
const injectStyleRules = require('./inject.style-rules')
module.exports = function (cfg, configName) {
const chain = new WebpackChain()
const needsHash = !cfg.ctx.dev && !['electron', 'cordova', 'capacitor'].includes(cfg.ctx.modeName)
const fileHash = needsHash ? '.[hash:8]' : ''
const chunkHash = needsHash ? '.[contenthash:8]' : ''
const resolveModules = [
'node_modules',
appPaths.resolve.app('node_modules'),
appPaths.resolve.cli('node_modules')
]
if (configName === 'Capacitor') {
// need to also look into /src-capacitor
// for deps like @capacitor/core
resolveModules.push(
appPaths.resolve.capacitor('node_modules')
)
}
chain.entry('app').add(appPaths.resolve.app('.quasar/client-entry.js'))
chain.mode(cfg.ctx.dev ? 'development' : 'production')
chain.devtool(cfg.build.sourceMap ? cfg.build.devtool : false)
if (cfg.ctx.prod || cfg.ctx.mode.ssr) {
chain.output
.path(
cfg.ctx.mode.ssr
? path.join(cfg.build.distDir, 'www')
: cfg.build.distDir
)
.publicPath(cfg.build.publicPath)
.filename(`js/[name]${fileHash}.js`)
.chunkFilename(`js/[name]${chunkHash}.js`)
}
chain.resolve.symlinks(false)
chain.resolve.extensions
.merge([ '.js', '.vue', '.json' ])
chain.resolve.modules
.merge(resolveModules)
chain.resolve.alias
.merge({
src: appPaths.srcDir,
app: appPaths.appDir,
components: appPaths.resolve.src(`components`),
layouts: appPaths.resolve.src(`layouts`),
pages: appPaths.resolve.src(`pages`),
assets: appPaths.resolve.src(`assets`),
boot: appPaths.resolve.src(`boot`)
})
if (cfg.framework.all === true) {
chain.resolve.alias.set('quasar$', 'quasar/dist/quasar.esm.js')
}
if (cfg.build.vueCompiler) {
chain.resolve.alias.set('vue$', 'vue/dist/vue.esm.js')
}
chain.resolveLoader.modules
.merge(resolveModules)
chain.module.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)
const vueRule = chain.module.rule('vue')
.test(/\.vue$/)
if (cfg.framework.all === 'auto') {
vueRule.use('quasar-auto-import')
.loader(path.join(__dirname, 'loader.auto-import.js'))
.options(cfg.framework.autoImportComponentCase)
}
vueRule.use('vue-loader')
.loader('vue-loader')
.options({
productionMode: cfg.ctx.prod,
compilerOptions: {
preserveWhitespace: false
},
transformAssetUrls: cfg.build.transformAssetUrls
})
chain.module.rule('babel')
.test(/\.jsx?$/)
.exclude
.add(filepath => {
// always transpile js(x) in Vue files
if (/\.vue\.jsx?$/.test(filepath)) {
return false
}
if (filepath.match(/[\\/]node_modules[\\/]quasar[\\/]/)) {
if (configName === 'Server') {
// transpile only if not from 'quasar/dist' folder
if (!filepath.match(/[\\/]node_modules[\\/]quasar[\\/]dist/)) {
return false
}
}
else {
// always transpile Quasar
return false
}
}
if (cfg.build.transpileDependencies.some(dep => filepath.match(dep))) {
return false
}
// Don't transpile anything else in node_modules
return /[\\/]node_modules[\\/]/.test(filepath)
})
.end()
.use('babel-loader')
.loader('babel-loader')
.options({
extends: appPaths.resolve.app('babel.config.js'),
plugins: cfg.framework.all !== true && configName !== 'Server' ? [
[
'transform-imports', {
quasar: {
transform: `quasar/dist/babel-transforms/imports.js`,
preventFullImport: true
}
}
]
] : []
})
chain.module.rule('images')
.test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.options({
esModule: false,
limit: 10000,
name: `img/[name]${fileHash}.[ext]`
})
chain.module.rule('fonts')
.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.options({
esModule: false,
limit: 10000,
name: `fonts/[name]${fileHash}.[ext]`
})
chain.module.rule('media')
.test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.options({
esModule: false,
limit: 10000,
name: `media/[name]${fileHash}.[ext]`
})
injectStyleRules(chain, {
rtl: cfg.build.rtl,
sourceMap: cfg.build.sourceMap,
extract: cfg.build.extractCSS,
serverExtract: configName === 'Server' && cfg.build.extractCSS,
minify: cfg.build.minify,
stylusLoaderOptions: cfg.build.stylusLoaderOptions,
sassLoaderOptions: cfg.build.sassLoaderOptions,
scssLoaderOptions: cfg.build.scssLoaderOptions,
lessLoaderOptions: cfg.build.lessLoaderOptions
})
chain.plugin('vue-loader')
.use(VueLoaderPlugin)
chain.plugin('define')
.use(webpack.DefinePlugin, [ cfg.build.env ])
if (cfg.build.showProgress) {
chain.plugin('progress')
.use(WebpackProgress, [{ name: configName }])
}
chain.plugin('boot-default-export')
.use(BootDefaultExport)
chain.performance
.hints(false)
.maxAssetSize(500000)
if (configName !== 'Server' && cfg.vendor.disable !== true) {
const { add, remove } = cfg.vendor
const regex = /[\\/]node_modules[\\/]/
chain.optimization
.splitChunks({
cacheGroups: {
vendors: {
name: 'vendor',
chunks: 'all',
priority: -10,
// a module is extracted into the vendor chunk if...
test: add !== void 0 || remove !== void 0
? module => {
if (module.resource) {
if (add !== void 0 && add.test(module.resource)) { return true }
if (remove !== void 0 && remove.test(module.resource)) { return false }
}
return regex.test(module.resource)
}
: module => regex.test(module.resource)
},
common: {
name: `chunk-common`,
minChunks: 2,
priority: -20,
chunks: 'all',
reuseExistingChunk: true
}
}
})
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
if (cfg.build.webpackManifest) {
chain.optimization.runtimeChunk('single')
}
}
// DEVELOPMENT build
if (cfg.ctx.dev) {
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const { devCompilationSuccess } = require('../helpers/banner')
chain.optimization
.noEmitOnErrors(true)
chain.plugin('friendly-errors')
.use(FriendlyErrorsPlugin, [{
clearConsole: true,
compilationSuccessInfo: ['spa', 'pwa', 'ssr'].includes(cfg.ctx.modeName)
? { notes: [ devCompilationSuccess(cfg.ctx, cfg.build.APP_URL, appPaths.appDir) ] }
: undefined
}])
}
// PRODUCTION build
else {
// keep module.id stable when vendor modules does not change
chain.plugin('hashed-module-ids')
.use(webpack.HashedModuleIdsPlugin, [{
hashDigest: 'hex'
}])
// keep chunk ids stable so async chunks have consistent hash
const hash = require('hash-sum')
chain
.plugin('named-chunks')
.use(webpack.NamedChunksPlugin, [
chunk => chunk.name || hash(
Array.from(chunk.modulesIterable, m => m.id).join('_')
)
])
if (configName !== 'Server') {
// copy statics to dist folder
const CopyWebpackPlugin = require('copy-webpack-plugin')
chain.plugin('copy-webpack')
.use(CopyWebpackPlugin, [
[{
from: appPaths.resolve.src('statics'),
to: 'statics',
ignore: ['.*'],
ignore: ['.*'].concat(
// avoid useless files to be copied
['electron', 'cordova', 'capacitor'].includes(cfg.ctx.modeName)
? [ 'icons/*', 'app-logo-128x128.png' ]
: []
)
}]
])
}
// Scope hoisting ala Rollupjs
if (cfg.build.scopeHoisting) {
chain.optimization
.concatenateModules(true)
}
if (cfg.ctx.debug) {
// reset default webpack 4 minimizer
chain.optimization.minimizers.delete('js')
// also:
chain.optimization.minimize(false)
}
else if (cfg.build.minify) {
const TerserPlugin = require('terser-webpack-plugin')
chain.optimization
.minimizer('js')
.use(TerserPlugin, [{
terserOptions: cfg.build.uglifyOptions,
extractComments: false,
cache: true,
parallel: true,
sourceMap: cfg.build.sourceMap
}])
}
// configure CSS extraction & optimize
if (configName !== 'Server' && cfg.build.extractCSS) {
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// extract css into its own file
chain.plugin('mini-css-extract')
.use(MiniCssExtractPlugin, [{
filename: 'css/[name].[contenthash:8].css'
}])
// dedupe & minify CSS (only if extracted)
if (cfg.build.minify) {
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const cssProcessorOptions = {
parser: require('postcss-safe-parser'),
autoprefixer: { disable: true }
}
if (cfg.build.sourceMap) {
cssProcessorOptions.map = { inline: false }
}
// We are using this plugin so that possible
// duplicated CSS = require(different components) can be deduped.
chain.plugin('optimize-css')
.use(OptimizeCSSPlugin, [{
canPrint: false,
cssProcessor: require('cssnano'),
cssProcessorOptions,
cssProcessorPluginOptions: {
preset: ['default', {
mergeLonghand: false,
convertValues: false,
cssDeclarationSorter: false,
reduceTransforms: false
}]
}
}])
}
}
if (configName !== 'Server') {
// also produce a gzipped version
if (cfg.build.gzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
chain.plugin('compress-webpack')
.use(CompressionWebpackPlugin, [ cfg.build.gzip ])
}
if (cfg.build.analyze) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
chain.plugin('bundle-analyzer')
.use(BundleAnalyzerPlugin, [ Object.assign({}, cfg.build.analyze) ])
}
}
}
return chain
}