UNPKG

@quasar/app-webpack

Version:

Quasar Framework App CLI with Webpack

555 lines (472 loc) 16.9 kB
const { join } = require('node:path') const webpack = require('webpack') const { merge } = require('webpack-merge') const WebpackChain = require('webpack-5-chain') const { VueLoaderPlugin } = require('vue-loader') const { cliPkg } = require('./utils/cli-runtime.js') const { getBuildSystemDefine } = require('./utils/env.js') const { log } = require('./utils/logger.js') const { injectStyleRules } = require('./utils/inject-style-rules.js') const { WebpackProgressPlugin } = require('./plugins/webpack.progress.js') const { BootDefaultExportPlugin } = require('./plugins/webpack.boot-default-export.js') const cliPkgDependencies = Object.keys(cliPkg.dependencies || {}) const nodeModulesRegex = /[\\/]node_modules[\\/]/ function getDependenciesRegex (list) { const deps = list.map(dep => { // eslint-disable-line array-callback-return if (typeof dep === 'string') { return join('node_modules', dep, '/') .replace(/\\/g, '[\\\\/]') // windows support } if (dep instanceof RegExp) { return dep.source } }).filter(e => e) return new RegExp(deps.join('|')) } function getRawDefine (rootDefines, compileId) { if (compileId === 'webpack-ssr-server') { return { ...rootDefines, __QUASAR_SSR_SERVER__: true } } if (compileId === 'webpack-ssr-client') { return { ...rootDefines, __QUASAR_SSR_CLIENT__: true } } return rootDefines } module.exports.createWebpackChain = async function createWebpackChain (quasarConf, { compileId, threadName }) { const { ctx } = quasarConf const { appPaths, cacheProxy } = ctx const chain = new WebpackChain() const isSsrServer = compileId === 'webpack-ssr-server' const { autoImport } = cacheProxy.getModule('quasarMeta') const useFastHash = ctx.dev || [ 'electron', 'cordova', 'capacitor', 'bex' ].includes(ctx.modeName) const fileHash = useFastHash === true ? '' : '.[contenthash:8]' const assetHash = useFastHash === true ? '.[hash:8]' : '.[contenthash:8]' const resolveModules = [ 'node_modules', appPaths.resolve.app('node_modules'), appPaths.resolve.cli('node_modules') ] chain.entry('app').add(appPaths.resolve.entry('client-entry.js')) chain.mode(ctx.dev ? 'development' : 'production') chain.devtool(quasarConf.build.sourcemap ? quasarConf.build.webpackDevtool : false) if (ctx.prod || ctx.mode.ssr) { chain.output .path(quasarConf.build.distDir) .publicPath(quasarConf.build.publicPath) .filename(`js/[name]${ fileHash }.js`) .chunkFilename(`js/[name]${ useFastHash === true ? '' : '.[chunkhash:8]' }.js`) } const hasTypescript = cacheProxy.getModule('hasTypescript') chain.resolve.extensions .merge( hasTypescript === true ? [ '.mjs', '.ts', '.js', '.cjs', '.vue', '.json', '.wasm' ] : [ '.mjs', '.js', '.cjs', '.vue', '.json', '.wasm' ] ) chain.resolve.modules .merge(resolveModules) chain.resolve.alias .merge(quasarConf.build.alias) const extrasPath = cacheProxy.getModule('extrasPath') if (extrasPath) { // required so quasar/icon-sets/* with imports to work correctly chain.resolve.alias.merge({ '@quasar/extras': extrasPath }) } const vueFile = isSsrServer ? (ctx.prod ? 'vue.cjs.prod.js' : 'vue.cjs.js') : ( quasarConf.build.vueCompiler ? 'vue.esm-bundler.js' : 'vue.runtime.esm-bundler.js' ) chain.resolve.alias.set('vue$', 'vue/dist/' + vueFile) const vueI18nFile = isSsrServer ? (ctx.prod ? 'vue-i18n.cjs.prod.js' : 'vue-i18n.cjs.js') : 'vue-i18n.esm-bundler.js' chain.resolve.alias.set('vue-i18n$', 'vue-i18n/dist/' + vueI18nFile) chain.resolveLoader.modules .merge(resolveModules) chain.module.noParse( /^(vue|vue-router|pinia|@quasar[\\/]extras|quasar[\\/]dist)$/ ) const vueRule = chain.module.rule('vue') .test(/\.vue$/) vueRule.use('vue-auto-import-quasar') .loader(join(__dirname, 'loaders/loader.vue.auto-import-quasar.js')) .options({ autoImportComponentCase: quasarConf.framework.autoImportComponentCase, isServerBuild: isSsrServer, ...autoImport }) vueRule.use('vue-loader') .loader('vue-loader') .options( merge( {}, quasarConf.build.vueLoaderOptions, { isServerBuild: isSsrServer } ) ) if (isSsrServer === false) { chain.module.rule('js-transform-quasar-imports') .test(/\.(t|j)sx?$/) .use('transform-quasar-imports') .loader(join(__dirname, 'loaders/loader.js.transform-quasar-imports.js')) .options(autoImport) } if (quasarConf.build.webpackTranspile === true) { const exceptionsRegex = getDependenciesRegex( [ /\.vue\.js$/, isSsrServer ? 'quasar/src' : 'quasar', '@babel/runtime' ] .concat(quasarConf.build.webpackTranspileDependencies) ) chain.module.rule('babel') .test(/\.js$/) .exclude .add(filepath => ( // Transpile the exceptions: exceptionsRegex.test(filepath) === false // Don't transpile anything else in node_modules: && nodeModulesRegex.test(filepath) )) .end() .use('babel-loader') .loader('babel-loader') .options({ compact: false, extends: appPaths.babelConfigFilename }) } if (hasTypescript === true) { chain.module .rule('typescript') .test(/\.ts$/) .use('ts-loader') .loader('ts-loader') .options({ appendTsSuffixTo: [ /\.vue$/ ], transpileOnly: true, // custom config is merged if present, but vue setup and type checking disable are always applied ...quasarConf.build.tsLoaderOptions }) } chain.module.rule('images') .test(/\.(png|jpe?g|gif|svg|webp|avif|ico)(\?.*)?$/) .type('javascript/auto') .use('url-loader') .loader('url-loader') .options({ esModule: false, limit: 10000, name: `img/[name]${ assetHash }.[ext]` }) chain.module.rule('fonts') .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/) .type('javascript/auto') .use('url-loader') .loader('url-loader') .options({ esModule: false, limit: 10000, name: `fonts/[name]${ assetHash }.[ext]` }) chain.module.rule('media') .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/) .type('javascript/auto') .use('url-loader') .loader('url-loader') .options({ esModule: false, limit: 10000, name: `media/[name]${ assetHash }.[ext]` }) await injectStyleRules(chain, { appPaths, cssVariables: cacheProxy.getModule('cssVariables'), isServerBuild: isSsrServer, rtl: quasarConf.build.rtl, sourceMap: quasarConf.build.sourcemap, extract: quasarConf.build.extractCSS, minify: quasarConf.build.minify, stylusLoaderOptions: quasarConf.build.stylusLoaderOptions, sassLoaderOptions: quasarConf.build.sassLoaderOptions, scssLoaderOptions: quasarConf.build.scssLoaderOptions, lessLoaderOptions: quasarConf.build.lessLoaderOptions }) chain.module // fixes https://github.com/graphql/graphql-js/issues/1272 .rule('mjs') .test(/\.mjs$/) .type('javascript/auto') .include .add(nodeModulesRegex) chain.plugin('vue-loader') .use(VueLoaderPlugin, [ quasarConf.build.vueLoaderOptions ]) chain.plugin('define') .use(webpack.DefinePlugin, [ getBuildSystemDefine({ buildEnv: quasarConf.build.env, buildRawDefine: getRawDefine(quasarConf.build.rawDefine, compileId), fileEnv: quasarConf.metaConf.fileEnv }) ]) chain.optimization .nodeEnv(false) if (ctx.dev && isSsrServer === false && ctx.mode.pwa) { // need to place it here before the status plugin const { WorkboxWarningPlugin } = require('./modes/pwa/plugin.webpack.workbox-warning.js') chain.plugin('workbox-warning') .use(WorkboxWarningPlugin) } chain.plugin('progress') .use(WebpackProgressPlugin, [ { name: threadName, quasarConf } ]) chain.plugin('boot-default-export') .use(BootDefaultExportPlugin) chain.performance .hints(false) .maxAssetSize(500000) if (isSsrServer === false && quasarConf.vendor.disable !== true) { const { add, remove } = quasarConf.vendor chain.optimization.splitChunks({ cacheGroups: { defaultVendors: { 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 (remove !== void 0 && remove.test(module.resource)) { return false } if (add !== void 0 && add.test(module.resource)) { return true } } return nodeModulesRegex.test(module.resource) } : nodeModulesRegex }, common: { name: 'chunk-common', minChunks: 2, priority: -20, chunks: 'all', reuseExistingChunk: true } } }) } // extract css into its own file if (isSsrServer === false && quasarConf.build.extractCSS) { const MiniCssExtractPlugin = require('mini-css-extract-plugin') chain.plugin('mini-css-extract') .use(MiniCssExtractPlugin, [ { filename: `css/[name]${ fileHash }.css` } ]) } if ( (ctx.prod || ctx.mode.bex) && isSsrServer === false && quasarConf.build.ignorePublicFolder !== true ) { // copy /public to dist folder const CopyWebpackPlugin = require('copy-webpack-plugin') const ignore = [ '**/.DS_Store', '**/.Thumbs.db', '**/*.sublime*', '**/.idea', '**/.editorconfig', '**/.vscode' ] // avoid useless files to be copied if ([ 'electron', 'cordova', 'capacitor' ].includes(ctx.modeName)) { ignore.push( '**/public/icons', '**/public/favicon.ico' ) } const patterns = [ { from: appPaths.resolve.app('public'), noErrorOnMissing: true, globOptions: { ignore } } ] chain.plugin('copy-webpack') .use(CopyWebpackPlugin, [ { patterns } ]) } if (ctx.dev) { chain.optimization .emitOnErrors(false) chain.infrastructureLogging({ colors: true, level: 'warn' }) } // ctx.prod else { chain.optimization .concatenateModules(ctx.mode.ssr !== true) if (quasarConf.metaConf.debugging === true) { // reset default webpack 4 minimizer chain.optimization.minimizers.delete('js') // also: chain.optimization.minimize(false) } else if (quasarConf.build.minify) { const TerserPlugin = require('terser-webpack-plugin') chain.optimization .minimizer('js') .use(TerserPlugin, [ { terserOptions: quasarConf.build.uglifyOptions, extractComments: false, parallel: true } ]) } if (isSsrServer === false) { // dedupe & minify CSS (only if extracted) if (quasarConf.build.extractCSS && quasarConf.build.minify) { const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') // We are using this plugin so that possible // duplicated CSS = require(different components) can be deduped. chain.optimization .minimizer('css') .use(CssMinimizerPlugin, [ { parallel: true } ]) } // also produce a gzipped version if (quasarConf.build.gzip) { const CompressionWebpackPlugin = require('compression-webpack-plugin') chain.plugin('compress-webpack') .use(CompressionWebpackPlugin, [ quasarConf.build.gzip ]) } if (quasarConf.build.analyze) { const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin chain.plugin('bundle-analyzer') .use(BundleAnalyzerPlugin, [ Object.assign({ analyzerMode: 'static' }, quasarConf.build.analyze) ]) } } } const { hasEslint, EslintWebpackPlugin } = cacheProxy.getModule('eslint') if (hasEslint === true && EslintWebpackPlugin !== void 0) { const { warnings, errors } = quasarConf.eslint if (warnings === true || errors === true) { const { injectESLintPlugin } = require('./utils/inject-eslint-plugin.js') injectESLintPlugin(chain, quasarConf, compileId) } } return chain } module.exports.extendWebpackChain = async function extendWebpackChain (webpackChain, quasarConf, invokeParams) { const opts = { isClient: false, isServer: false, ...invokeParams } const { appExt } = quasarConf.ctx if (typeof quasarConf.build.chainWebpack === 'function') { quasarConf.build.chainWebpack(webpackChain, opts) } await appExt.runAppExtensionHook('chainWebpack', async hook => { log(`Extension(${ hook.api.extId }): Chaining Webpack config`) await hook.fn(webpackChain, invokeParams, hook.api) }) const webpackConf = webpackChain.toConfig() if (typeof quasarConf.build.extendWebpack === 'function') { quasarConf.build.extendWebpack(webpackConf, opts) } const promise = appExt.runAppExtensionHook('extendWebpack', async hook => { log(`Extension(${ hook.api.extId }): Extending Webpack config`) await hook.fn(webpackConf, opts, hook.api) }) return promise.then(() => webpackConf) } module.exports.createNodeEsbuildConfig = async function createNodeEsbuildConfig (quasarConf, { compileId, format }) { const { ctx: { pkg: { appPkg }, cacheProxy } } = quasarConf const externalsList = cacheProxy.getRuntime('externalEsbuildParam', () => [ ...cliPkgDependencies, ...Object.keys(appPkg.dependencies || {}), ...Object.keys(appPkg.devDependencies || {}) ].filter( // the possible imports of '#q-app/wrappers' / '@quasar/app-webpack/wrappers' dep => dep !== cliPkg.name )) const esbuildConfig = { platform: 'node', target: quasarConf.build.esbuildTarget.node, format, bundle: true, sourcemap: quasarConf.metaConf.debugging === true ? 'inline' : false, minify: quasarConf.build.minify !== false, alias: { ...quasarConf.build.alias }, resolveExtensions: format === 'esm' ? [ '.mjs', '.js', '.cjs', '.ts', '.json' ] : [ '.cjs', '.js', '.mjs', '.ts', '.json' ], // we use a fresh list since this can be tampered with by the user: external: [ ...externalsList ], define: getBuildSystemDefine({ buildEnv: quasarConf.build.env, buildRawDefine: quasarConf.build.rawDefine, fileEnv: quasarConf.metaConf.fileEnv }), plugins: [] } const { hasEslint, ESLint } = cacheProxy.getModule('eslint') if (hasEslint === true && ESLint !== void 0) { const { warnings, errors } = quasarConf.eslint if (warnings === true || errors === true) { // import only if actually needed (as it imports app's eslint pkg) const { quasarEsbuildESLintPlugin } = require('./plugins/esbuild.eslint.js') esbuildConfig.plugins.push( await quasarEsbuildESLintPlugin(quasarConf, compileId) ) } } return esbuildConfig } module.exports.createBrowserEsbuildConfig = async function createBrowserEsbuildConfig (quasarConf, { compileId }) { const esbuildConfig = { platform: 'browser', target: quasarConf.build.esbuildTarget.browser, format: 'iife', bundle: true, sourcemap: quasarConf.metaConf.debugging === true ? 'inline' : false, minify: quasarConf.build.minify !== false, alias: { ...quasarConf.build.alias }, define: getBuildSystemDefine({ buildEnv: quasarConf.build.env, buildRawDefine: quasarConf.build.rawDefine, fileEnv: quasarConf.metaConf.fileEnv }), plugins: [] } const { hasEslint, ESLint } = await quasarConf.ctx.cacheProxy.getModule('eslint') if (hasEslint === true && ESLint !== void 0) { const { warnings, errors } = quasarConf.eslint if (warnings === true || errors === true) { // import only if actually needed (as it imports app's eslint pkg) const { quasarEsbuildESLintPlugin } = require('./plugins/esbuild.eslint.js') esbuildConfig.plugins.push( await quasarEsbuildESLintPlugin(quasarConf, compileId) ) } } return esbuildConfig } module.exports.extendEsbuildConfig = function extendEsbuildConfig (esbuildConf, quasarConfTarget, ctx, methodName) { // example: quasarConf.ssr.extendSSRWebserverConf if (typeof quasarConfTarget[ methodName ] === 'function') { quasarConfTarget[ methodName ](esbuildConf) } const promise = ctx.appExt.runAppExtensionHook(methodName, async hook => { log(`Extension(${ hook.api.extId }): Running "${ methodName }(esbuildConf)"`) await hook.fn(esbuildConf, hook.api) }) return promise.then(() => esbuildConf) }