@quasar/app-webpack
Version:
Quasar Framework App CLI with Webpack
1,340 lines (1,122 loc) • 40.2 kB
JavaScript
const { join, isAbsolute, basename, dirname } = require('node:path')
const { pathToFileURL } = require('node:url')
const { existsSync, readFileSync } = require('node:fs')
const fse = require('fs-extra')
const { merge } = require('webpack-merge')
const debounce = require('lodash/debounce.js')
const { build: esBuild, context: esContextBuild } = require('esbuild')
const { green, dim } = require('kolorist')
const { log, warn, fatal, tip } = require('./utils/logger.js')
const { appFilesValidations } = require('./utils/app-files-validations.js')
const { getPackageMajorVersion } = require('./utils/get-package-major-version.js')
const { resolveExtension } = require('./utils/resolve-extension.js')
const { ensureElectronArgv } = require('./utils/ensure-argv.js')
const { quasarEsbuildInjectReplacementsDefine, quasarEsbuildInjectReplacementsPlugin } = require('./plugins/esbuild.inject-replacements.js')
const { quasarEsbuildVueShimPlugin } = require('./plugins/esbuild.vue-shim.js')
const urlRegex = /^http(s)?:\/\//i
const { findClosestOpenPort, localHostList } = require('./utils/net.js')
const { isMinimalTerminal } = require('./utils/is-minimal-terminal.js')
const { readFileEnv } = require('./utils/env.js')
const defaultPortMapping = {
spa: 9000,
ssr: 9100, // 9150 for SSR + PWA
pwa: 9200,
electron: 9300,
cordova: 9400,
capacitor: 9500,
bex: 9600
}
const quasarComponentRE = /^(Q[A-Z]|q-)/
const quasarConfigBanner = `/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
`
function clone (obj) {
return JSON.parse(JSON.stringify(obj))
}
function escapeHTMLTagContent (str) {
return str ? str.replace(/[<>]/g, '') : ''
}
function escapeHTMLAttribute (str) {
return str ? str.replace(/\"/g, '') : ''
}
function formatPublicPath (publicPath) {
if (!publicPath) {
return '/'
}
if (!publicPath.endsWith('/')) {
publicPath = `${ publicPath }/`
}
if (urlRegex.test(publicPath) === true) {
return publicPath
}
if (!publicPath.startsWith('/')) {
publicPath = `/${ publicPath }`
}
return publicPath
}
function formatRouterBase (publicPath) {
if (!publicPath || !publicPath.startsWith('http')) {
return publicPath
}
const match = publicPath.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/)
return formatPublicPath(match[ 5 ] || '')
}
function parseAssetProperty (prefix) {
return asset => {
if (typeof asset === 'string') {
return {
path: asset[ 0 ] === '~' ? asset.substring(1) : prefix + `/${ asset }`
}
}
return {
...asset,
path: typeof asset.path === 'string'
? (asset.path[ 0 ] === '~' ? asset.path.substring(1) : prefix + `/${ asset.path }`)
: asset.path
}
}
}
function getUniqueArray (original) {
return Array.from(new Set(original))
}
function uniquePathFilter (value, index, self) {
return self.map(obj => obj.path).indexOf(value.path) === index
}
function uniqueRegexFilter (value, index, self) {
return self.map(regex => regex.toString()).indexOf(value.toString()) === index
}
const extRE = /\.[m|c]?[j|t]s$/
function formatQuasarAssetPath (asset, type) {
return asset.indexOf('/') !== -1
? (
extRE.test(asset) === true
? asset
: `${ asset }.js`
)
: `quasar/${ type }/${ asset }.js`
}
let cachedExternalHost, addressRunning = false
async function onAddress ({ host, port }, mode) {
if (
[ 'cordova', 'capacitor' ].includes(mode)
&& (!host || localHostList.includes(host.toLowerCase()))
) {
if (cachedExternalHost) {
host = cachedExternalHost
}
else {
const { getExternalIP } = require('./utils/get-external-ip.js')
host = await getExternalIP()
cachedExternalHost = host
}
}
try {
const openPort = await findClosestOpenPort(port, host)
if (port !== openPort) {
warn()
warn(`️️Setting port to closest one available: ${ openPort }`)
warn()
port = openPort
}
}
catch (e) {
warn()
if (e.message === 'ERROR_NETWORK_PORT_NOT_AVAIL') {
warn('Could not find an open port. Please configure a lower one to start searching with.')
}
else if (e.message === 'ERROR_NETWORK_ADDRESS_NOT_AVAIL') {
warn('Invalid host specified. No network address matches. Please specify another one.')
}
else {
warn('Unknown network error occurred')
console.error(e)
}
warn()
if (addressRunning === false) {
process.exit(1)
}
return null
}
addressRunning = true
return { host, port }
}
module.exports.QuasarConfigFile = class QuasarConfigFile {
#ctx
#opts
#versions = {}
#address
#isWatching = false
#tempFile
#cssVariables
#storeProvider
#transformAssetUrls
#vueDevtools
#electronInspectPort
constructor ({ ctx, host, port, verifyAddress, watch }) {
this.#ctx = ctx
this.#opts = { host, port, verifyAddress }
if (watch !== void 0) {
this.#opts.watch = debounce(watch, 550)
}
const { appPaths } = ctx
const quasarConfigFileExtension = appPaths.quasarConfigOutputFormat === 'esm' ? 'mjs' : appPaths.quasarConfigOutputFormat
// if filename syntax gets changed, then also update the "clean" cmd
this.#tempFile = `${ appPaths.quasarConfigFilename }.temporary.compiled.${ Date.now() }.${ quasarConfigFileExtension }`
log(`Using ${ basename(appPaths.quasarConfigFilename) } in "${ appPaths.quasarConfigInputFormat }" format`)
}
async init () {
const { appPaths, cacheProxy, appExt } = this.#ctx
this.#cssVariables = cacheProxy.getModule('cssVariables')
this.#storeProvider = cacheProxy.getModule('storeProvider')
const { transformAssetUrls } = cacheProxy.getModule('quasarMeta')
this.#transformAssetUrls = transformAssetUrls
await appExt.registerAppExtensions()
if (this.#ctx.mode.pwa) {
// Enable this when workbox bumps version (as of writing these lines, we're handling v6 & v7)
// this.#versions.workbox = getPackageMajorVersion('workbox-webpack-plugin', appPaths.appDir)
}
else if (this.#ctx.mode.capacitor) {
const { capVersion } = cacheProxy.getModule('capCli')
const getCapPluginVersion = capVersion <= 2
? () => true
: name => {
const version = getPackageMajorVersion(name, appPaths.capacitorDir)
return version === void 0
? false
: version || true
}
Object.assign(this.#versions, {
capacitor: capVersion,
capacitorPluginApp: getCapPluginVersion('@capacitor/app'),
capacitorPluginSplashscreen: getCapPluginVersion('@capacitor/splash-screen')
})
}
}
read () {
const esbuildConfig = this.#createEsbuildConfig()
return this.#opts.watch !== void 0
? this.#buildAndWatch(esbuildConfig)
: this.#build(esbuildConfig)
}
// start watching for changes
watch () {
this.#isWatching = true
}
#createEsbuildConfig () {
const { appPaths } = this.#ctx
return {
platform: 'node',
format: appPaths.quasarConfigOutputFormat,
bundle: true,
packages: 'external',
banner: {
js: quasarConfigBanner
},
define: quasarEsbuildInjectReplacementsDefine,
// Define the aliases which have to be usable in the quasar.config file
alias: {
'#q-app': '@quasar/app-webpack'
},
resolveExtensions: [ appPaths.quasarConfigOutputFormat === 'esm' ? '.mjs' : '.cjs', '.js', '.mts', '.ts', '.json' ],
entryPoints: [ appPaths.quasarConfigFilename ],
outfile: this.#tempFile,
plugins: [
quasarEsbuildInjectReplacementsPlugin,
quasarEsbuildVueShimPlugin
],
logOverride: {
// .quasar/tsconfig.json won't be available for the first time executing dev/build/prepare.
// So, esbuild will show a warning saying it can't find the `extends` file.
// We need to suppress the warning. Otherwise, it will be noisy and cause a temp file to be created.
// tsconfig is not really important for the config file itself anyway.
'tsconfig.json': 'silent'
}
}
}
async #build (esbuildConfig) {
try {
await esBuild(esbuildConfig)
}
catch (e) {
fse.removeSync(this.#tempFile)
console.log()
console.error(e)
fatal('Could not compile the quasar.config file because it has errors.', 'FAIL')
}
let quasarConfigFn
try {
const fnResult = await import(
pathToFileURL(this.#tempFile)
)
quasarConfigFn = fnResult.default || fnResult
}
catch (e) {
console.log()
console.error(e)
fatal(
'The quasar.config file has runtime errors. Please check the Node.js stack above against the'
+ ` temporarily created ${ basename(this.#tempFile) } file, fix the original file`
+ ' then DELETE the temporary one ("quasar clean --qconf" can be used).',
'FAIL'
)
}
return this.#computeConfig(quasarConfigFn, true)
}
async #buildAndWatch (esbuildConfig) {
let firstBuildIsDone
const { appPaths } = this.#ctx
const tempFile = this.#tempFile
esbuildConfig.plugins.push({
name: 'quasar:watcher',
setup: build => {
let isFirst = true
build.onStart(() => {
if (isFirst === false) {
log()
log('The quasar.config file (or its dependencies) changed. Reading it again...')
}
})
build.onEnd(async result => {
if (
// not ready yet; watch() has not been issued yet
isFirst === false
&& this.#isWatching === false
) return
if (result.errors.length !== 0) {
fse.removeSync(tempFile)
const msg = 'Could not compile the quasar.config file because it has errors.'
if (isFirst === true) {
fatal(msg, 'FAIL')
}
warn(msg + ' Please fix them.\n')
return
}
let quasarConfigFn
// ensure we grab the latest version
if (appPaths.quasarConfigOutputFormat === 'cjs') {
delete require.cache[ tempFile ]
}
try {
const result = appPaths.quasarConfigOutputFormat === 'esm'
? await import(pathToFileURL(tempFile) + '?t=' + Date.now()) // we also need to cache bust it, hence the ?t= param
: require(tempFile)
quasarConfigFn = result.default || result
}
catch (e) {
// free up memory immediately
if (appPaths.quasarConfigOutputFormat === 'cjs') {
delete require.cache[ tempFile ]
}
console.log()
console.error(e)
const msg = 'Importing quasar.config file results in error. Please check the'
+ ` Node.js stack above against the temporarily created ${ basename(tempFile) } file`
+ ' and fix the original file then DELETE the temporary one ("quasar clean --qconf" can be used).'
if (isFirst === true) {
fatal(msg, 'FAIL')
}
warn(msg + '\n')
return
}
// free up memory immediately
if (appPaths.quasarConfigOutputFormat === 'cjs') {
delete require.cache[ tempFile ]
}
const quasarConf = await this.#computeConfig(quasarConfigFn, isFirst)
if (quasarConf === void 0) return
if (isFirst === true) {
isFirst = false
firstBuildIsDone(quasarConf)
return
}
log('Scheduled to apply quasar.config changes in 550ms')
this.#opts.watch(quasarConf)
})
}
})
const esbuildCtx = await esContextBuild(esbuildConfig)
await esbuildCtx.watch()
return new Promise(res => { // eslint-disable-line promise/param-names
firstBuildIsDone = res
})
}
// return void 0 if it encounters errors
// and quasarConf otherwise
async #computeConfig (quasarConfigFn, failOnError) {
if (typeof quasarConfigFn !== 'function') {
fse.removeSync(this.#tempFile)
const msg = 'The default export value of the quasar.config file is not a function.'
if (failOnError === true) {
fatal(msg, 'FAIL')
}
warn(msg + ' Please fix it.\n')
return
}
let userCfg
try {
userCfg = await quasarConfigFn(this.#ctx)
}
catch (e) {
console.log()
console.error(e)
const msg = 'The quasar.config file has runtime errors.'
+ ' Please check the Node.js stack above against the'
+ ` temporarily created ${ basename(this.#tempFile) } file`
+ ' then DELETE it ("quasar clean --qconf" can be used).'
if (failOnError === true) {
fatal(msg, 'FAIL')
}
warn(msg + ' Please fix the errors in the original file.\n')
return
}
if (Object(userCfg) !== userCfg) {
fse.removeSync(this.#tempFile)
const msg = 'The quasar.config file does not default exports an Object.'
if (failOnError === true) {
fatal(msg, 'FAIL')
}
warn(msg + ' Please fix it.\n')
return
}
fse.removeSync(this.#tempFile)
const { appPaths } = this.#ctx
const rawQuasarConf = merge({
ctx: this.#ctx,
boot: [],
css: [],
extras: [],
animations: [],
framework: {
components: [],
directives: [],
plugins: [],
config: {}
},
vendor: {
add: [],
remove: []
},
eslint: {
include: [],
exclude: [],
rawWebpackEslintPluginOptions: {},
rawEsbuildEslintOptions: {}
},
sourceFiles: {},
bin: {},
htmlVariables: {},
devServer: {
server: {}
},
build: {
esbuildTarget: {},
vueLoaderOptions: {
transformAssetUrls: {}
},
sassLoaderOptions: {},
scssLoaderOptions: {},
stylusLoaderOptions: {},
lessLoaderOptions: {},
tsLoaderOptions: {},
env: {},
rawDefine: {},
envFiles: [],
webpackTranspileDependencies: [],
uglifyOptions: {
compress: {}
},
htmlMinifyOptions: {}
},
ssr: {
middlewares: []
},
pwa: {},
electron: {
preloadScripts: [],
unPackagedInstallParams: [],
packager: {},
builder: {}
},
cordova: {},
capacitor: {
capacitorCliPreparationParams: []
},
bex: {
extraScripts: []
}
}, userCfg)
const metaConf = {
debugging: this.#ctx.dev === true || this.#ctx.debug === true,
needsAppMountHook: false,
vueDevtools: false,
versions: { ...this.#versions }, // used by entry templates
css: { ...this.#cssVariables }
}
if (rawQuasarConf.animations === 'all') {
rawQuasarConf.animations = this.#ctx.cacheProxy.getModule('animations')
}
try {
await this.#ctx.appExt.runAppExtensionHook('extendQuasarConf', async hook => {
log(`Extension(${ hook.api.extId }): Extending quasar.config file configuration...`)
await hook.fn(rawQuasarConf, hook.api)
})
}
catch (e) {
console.log()
console.error(e)
if (failOnError === true) {
fatal('One of your installed App Extensions failed to run', 'FAIL')
}
warn('One of your installed App Extensions failed to run.\n')
return
}
const cfg = {
...rawQuasarConf,
metaConf
}
// we need to know if using SSR + PWA immediately
if (this.#ctx.mode.ssr) {
cfg.ssr = merge({
pwa: false,
pwaOfflineHtmlFilename: 'offline.html',
manualStoreHydration: false,
manualPostHydrationTrigger: false,
prodPort: 3000 // gets superseded in production by an eventual process.env.PORT
}, cfg.ssr)
}
if (this.#ctx.dev) {
if (this.#opts.host) {
cfg.devServer.host = this.#opts.host
}
else if (!cfg.devServer.host) {
cfg.devServer.host = '0.0.0.0'
}
if (this.#opts.port) {
cfg.devServer.port = this.#opts.port
tip('You are using the --port parameter. It is recommended to use a different devServer port for each Quasar mode to avoid browser cache issues')
}
else if (!cfg.devServer.port) {
cfg.devServer.port = defaultPortMapping[ this.#ctx.modeName ]
+ (this.#ctx.mode.ssr === true && cfg.ssr.pwa === true ? 50 : 0)
}
else {
tip(
'You (or an AE) specified an explicit quasar.config file > devServer > port. It is recommended to use'
+ ' a different devServer > port for each Quasar mode to avoid browser cache issues.'
+ ' Example: ctx.mode.ssr ? 9100 : ...'
)
}
if (
this.#address
&& this.#address.from.host === cfg.devServer.host
&& this.#address.from.port === cfg.devServer.port
) {
cfg.devServer.host = this.#address.to.host
cfg.devServer.port = this.#address.to.port
}
else {
const addr = {
host: cfg.devServer.host,
port: cfg.devServer.port
}
const to = this.#opts.verifyAddress === true
? await onAddress(addr, this.#ctx.modeName)
: addr
// if network error while running
if (to === null) {
const msg = 'Network error encountered while following the quasar.config file host/port config.'
if (failOnError === true) {
fatal(msg, 'FAIL')
}
warn(msg + ' Reconfigure and save the file again.\n')
return
}
cfg.devServer = merge({ open: true }, cfg.devServer, to)
this.#address = {
from: addr,
to: {
host: cfg.devServer.host,
port: cfg.devServer.port
}
}
}
}
if (cfg.css.length > 0) {
cfg.css = cfg.css.filter(_ => _)
.map(parseAssetProperty('src/css'))
.filter(asset => asset.path)
.filter(uniquePathFilter)
}
if (cfg.boot.length > 0) {
cfg.boot = cfg.boot.filter(_ => _)
.map(parseAssetProperty('boot'))
.filter(asset => asset.path)
.filter(uniquePathFilter)
}
if (cfg.extras.length > 0) {
cfg.extras = getUniqueArray(cfg.extras)
}
if (cfg.animations.length > 0) {
cfg.animations = getUniqueArray(cfg.animations)
}
if (![ 'kebab', 'pascal', 'combined' ].includes(cfg.framework.autoImportComponentCase)) {
cfg.framework.autoImportComponentCase = 'kebab'
}
// special case where a component can be designated for a framework > config prop
const { config } = cfg.framework
if (config.loading) {
const { spinner } = config.loading
if (quasarComponentRE.test(spinner)) {
cfg.framework.components.push(spinner)
}
}
if (config.notify) {
const { spinner } = config.notify
if (quasarComponentRE.test(spinner)) {
cfg.framework.components.push(spinner)
}
}
cfg.framework.components = getUniqueArray(cfg.framework.components)
cfg.framework.directives = getUniqueArray(cfg.framework.directives)
cfg.framework.plugins = getUniqueArray(cfg.framework.plugins)
const { lang, iconSet } = cfg.framework
if (lang !== void 0) {
cfg.framework.lang = formatQuasarAssetPath(lang, 'lang')
}
if (iconSet !== void 0) {
cfg.framework.iconSet = formatQuasarAssetPath(iconSet, 'icon-set')
}
Object.assign(cfg.metaConf, {
hasLoadingBarPlugin: cfg.framework.plugins.includes('LoadingBar'),
hasMetaPlugin: cfg.framework.plugins.includes('Meta')
})
cfg.eslint = merge({
warnings: false,
errors: false,
fix: false,
formatter: 'stylish',
cache: true,
include: [],
exclude: [],
rawWebpackEslintPluginOptions: {},
rawEsbuildEslintOptions: {}
}, cfg.eslint)
cfg.build = merge({
vueLoaderOptions: {
transformAssetUrls: clone(this.#transformAssetUrls)
},
vueOptionsAPI: true,
vueRouterMode: 'hash',
minify: cfg.metaConf.debugging !== true
&& (this.#ctx.mode.bex !== true || cfg.bex.minify === true),
sourcemap: cfg.metaConf.debugging === true,
// need to force extraction for SSR due to
// missing functionality in vue-loader
extractCSS: this.#ctx.prod || this.#ctx.mode.ssr,
distDir: join('dist', this.#ctx.modeName),
webpackTranspile: true,
htmlFilename: 'index.html',
webpackShowProgress: true,
webpackDevtool: this.#ctx.dev
// eval does not suit CSP of browser extensions
? (this.#ctx.mode.bex ? 'cheap-source-map' : 'eval-cheap-module-source-map')
: 'source-map',
uglifyOptions: {
compress: {
// turn off flags with small gains to speed up minification
arrows: false,
collapse_vars: false, // 0.3kb
comparisons: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
// a few flags with noticeable gains/speed ratio
// numbers based on out of the box vendor bundle
booleans: true, // 0.7kb
if_return: true, // 0.4kb
sequences: true, // 0.7kb
unused: true, // 2.3kb
// required features to drop conditional branches
conditionals: true,
dead_code: true,
evaluate: true
}
},
htmlMinifyOptions: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
collapseBooleanAttributes: true,
removeScriptTypeAttributes: true
// more options:
// https://github.com/terser/html-minifier-terser?tab=readme-ov-file#options-quick-reference
},
rawDefine: {
// quasar
__QUASAR_VERSION__: JSON.stringify(this.#ctx.pkg.quasarPkg.version),
__QUASAR_SSR__: this.#ctx.mode.ssr === true,
__QUASAR_SSR_SERVER__: false,
__QUASAR_SSR_CLIENT__: false,
__QUASAR_SSR_PWA__: false,
// vue
__VUE_OPTIONS_API__: cfg.build.vueOptionsAPI !== false,
__VUE_PROD_DEVTOOLS__: cfg.metaConf.debugging,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: cfg.metaConf.debugging, // Vue 3.4+
// vue-i18n
__VUE_I18N_FULL_INSTALL__: true,
__VUE_I18N_LEGACY_API__: true,
__VUE_I18N_PROD_DEVTOOLS__: cfg.metaConf.debugging,
__INTLIFY_PROD_DEVTOOLS__: cfg.metaConf.debugging
},
alias: {
'#q-app': '@quasar/app-webpack',
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'),
stores: appPaths.resolve.src('stores')
},
typescript: {
strict: false,
vueShim: false
}
}, cfg.build)
if (cfg.vendor.disable !== true) {
cfg.vendor.add = cfg.vendor.add.length > 0
? new RegExp(cfg.vendor.add.filter(v => v).join('|'))
: void 0
cfg.vendor.remove = cfg.vendor.remove.length > 0
? new RegExp(cfg.vendor.remove.filter(v => v).join('|'))
: void 0
}
if (cfg.build.webpackTranspile === true) {
cfg.build.webpackTranspileDependencies = cfg.build.webpackTranspileDependencies.filter(uniqueRegexFilter)
cfg.metaConf.webpackTranspileBanner = green('yes (Babel)')
}
else {
cfg.metaConf.webpackTranspileBanner = dim('no')
}
if (!cfg.build.esbuildTarget.browser) {
cfg.build.esbuildTarget.browser = [ 'es2022', 'firefox115', 'chrome115', 'safari14' ]
}
if (!cfg.build.esbuildTarget.node) {
cfg.build.esbuildTarget.node = 'node20'
}
if (this.#ctx.mode.ssr) {
cfg.build.vueRouterMode = 'history'
}
else if (this.#ctx.mode.cordova || this.#ctx.mode.capacitor || this.#ctx.mode.electron || this.#ctx.mode.bex) {
Object.assign(cfg.build, {
htmlFilename: 'index.html',
vueRouterMode: 'hash',
gzip: false
})
}
if (this.#ctx.mode.bex) {
// we want to differentiate the folder
// otherwise we can't run dev and build simultaneously;
// it's better regardless because it's easier to select the dev folder
// when loading the browser extension
const name = basename(cfg.build.distDir)
cfg.build.distDir = join(
dirname(cfg.build.distDir),
`bex-${ this.#ctx.targetName }${ name !== 'bex' ? `-${ name }` : '' }${ this.#ctx.dev ? '--dev' : '' }`
)
}
if (!isAbsolute(cfg.build.distDir)) {
cfg.build.distDir = appPaths.resolve.app(cfg.build.distDir)
}
cfg.build.publicPath
= this.#ctx.mode.bex
? '/www'
: (
cfg.build.publicPath && [ 'spa', 'pwa', 'ssr' ].includes(this.#ctx.modeName)
? formatPublicPath(cfg.build.publicPath)
: ([ 'capacitor', 'cordova', 'electron', 'bex' ].includes(this.#ctx.modeName) ? '' : '/')
)
/* careful if you configure the following; make sure that you really know what you are doing */
cfg.build.vueRouterBase = cfg.build.vueRouterBase !== void 0
? cfg.build.vueRouterBase
: formatRouterBase(cfg.build.publicPath)
// When adding new props here be sure to update
// all impacted devserver diffs (look for this.registerDiff() calls)
cfg.sourceFiles = merge({
// For esbuild JS/TS entry points, make sure this
// gets appPaths.resolve.app() applied to it further down the line
rootComponent: 'src/App.vue',
router: 'src/router/index',
store: `src/${ this.#storeProvider.pathKey }/index`,
indexHtmlTemplate: 'index.html',
pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
pwaServiceWorker: 'src-pwa/custom-service-worker',
pwaManifestFile: 'src-pwa/manifest.json',
electronMain: 'src-electron/electron-main',
bexManifestFile: 'src-bex/manifest.json'
}, cfg.sourceFiles)
if (appFilesValidations(appPaths, cfg.sourceFiles) === false) {
if (failOnError === true) {
fatal('Files validation not passed successfully', 'FAIL')
}
warn('Files validation not passed successfully. Please fix the issues.\n')
return
}
// do we have a store?
const storePath = appPaths.resolve.app(cfg.sourceFiles.store)
Object.assign(cfg.metaConf, {
hasStore: resolveExtension(storePath) !== void 0,
storePackage: this.#storeProvider.name
})
// make sure we have preFetch in config
cfg.preFetch = cfg.preFetch || false
if (this.#ctx.mode.capacitor & cfg.capacitor.capacitorCliPreparationParams.length === 0) {
cfg.capacitor.capacitorCliPreparationParams = [ 'sync', this.#ctx.targetName ]
}
if (this.#ctx.mode.ssr) {
if (cfg.ssr.manualPostHydrationTrigger !== true) {
cfg.metaConf.needsAppMountHook = true
}
if (cfg.ssr.middlewares.length > 0) {
cfg.ssr.middlewares = cfg.ssr.middlewares.filter(_ => _)
.map(parseAssetProperty('app/src-ssr/middlewares'))
.filter(asset => asset.path)
.filter(uniquePathFilter)
}
if (cfg.ssr.pwa === true) {
// install pwa mode if it's missing
const { addMode } = require('../lib/modes/pwa/pwa-installation.js')
await addMode({ ctx: this.#ctx, silent: true })
cfg.build.rawDefine.__QUASAR_SSR_PWA__ = true
}
this.#ctx.mode.pwa = cfg.ctx.mode.pwa = cfg.ssr.pwa === true
}
// (backward compatibility for upstream)
// webpack-dev-server 4.5.0 / 5.0.0 introduced a change in behavior
// along with deprecation notices; so we transform it automatically
// for a better experience for our developers
if (typeof cfg.devServer.server === 'string') {
cfg.devServer.server = {
type: cfg.devServer.server
}
}
else if (cfg.devServer.https !== void 0) {
const { https } = cfg.devServer
delete cfg.devServer.https
if (https !== false) {
cfg.devServer.server = {
type: 'https'
}
if (Object(https) === https) {
cfg.devServer.server.options = https
}
}
}
if (this.#ctx.dev) {
const originalSetup = cfg.devServer.setupMiddlewares
const openInEditor = require('launch-editor-middleware')
cfg.devServer = merge({
hot: true,
allowedHosts: 'all',
compress: true,
open: true,
client: {
overlay: {
warnings: false
}
},
server: {
type: 'http'
},
devMiddleware: {
publicPath: cfg.build.publicPath,
stats: false
}
},
this.#ctx.mode.ssr === true
? {
devMiddleware: {
index: false
},
static: {
serveIndex: false
}
}
: {
historyApiFallback: cfg.build.vueRouterMode === 'history'
? { index: `${ cfg.build.publicPath || '/' }${ cfg.build.htmlFilename }` }
: false,
devMiddleware: {
index: cfg.build.htmlFilename
}
},
cfg.devServer,
{
setupMiddlewares: (middlewares, opts) => {
const { app } = opts
if (!this.#ctx.mode.ssr) {
const express = require('express')
if (cfg.build.ignorePublicFolder !== true) {
app.use((cfg.build.publicPath || '/'), express.static(appPaths.resolve.app('public'), {
maxAge: 0
}))
}
if (this.#ctx.mode.cordova) {
const folder = appPaths.resolve.cordova(`platforms/${ this.#ctx.targetName }/platform_www`)
app.use('/', express.static(folder, { maxAge: 0 }))
}
}
app.use('/__open-in-editor', openInEditor(void 0, appPaths.appDir))
return originalSetup
? originalSetup(middlewares, opts)
: middlewares
}
})
if (this.#ctx.vueDevtools === true || cfg.devServer.vueDevtools === true) {
if (this.#vueDevtools === void 0) {
const host = localHostList.includes(cfg.devServer.host.toLowerCase())
? 'localhost'
: cfg.devServer.host
this.#vueDevtools = {
host,
port: await findClosestOpenPort(11111, '0.0.0.0')
}
}
cfg.metaConf.vueDevtools = { ...this.#vueDevtools }
}
if (this.#ctx.mode.electron || this.#ctx.mode.bex) {
cfg.devServer.server.type = 'http'
}
else if (cfg.devServer.open && !this.#ctx.mode.cordova && !this.#ctx.mode.capacitor) {
cfg.metaConf.openBrowser = !isMinimalTerminal
? cfg.devServer.open
: false
}
delete cfg.devServer.open
if (this.#ctx.mode.bex === true && this.#ctx.target.firefox === true) {
cfg.devServer.devMiddleware = cfg.devServer.devMiddleware || {}
cfg.devServer.devMiddleware.writeToDisk = true
}
if (cfg.devServer.server.type === 'https') {
const { options } = cfg.devServer.server
if (options === void 0) {
const { getCertificate } = await import('@quasar/ssl-certificate')
const sslCertificate = getCertificate({ log, fatal })
cfg.devServer.server.options = {
key: sslCertificate,
cert: sslCertificate
}
}
else {
// we now check if config is specifying a file path
// and we actually read the contents so we can later supply correct
// params to the node HTTPS server
[ 'ca', 'pfx', 'key', 'cert' ].forEach(prop => {
if (typeof options[ prop ] === 'string') {
try {
options[ prop ] = readFileSync(options[ prop ])
}
catch (e) {
console.error(e)
console.log()
delete options[ prop ]
warn(`The devServer.server.options.${ prop } file could not be read. Removed the config.`)
}
}
})
}
}
}
if (cfg.build.gzip) {
const gzip = cfg.build.gzip === true
? {}
: cfg.build.gzip
let ext = [ 'js', 'css' ]
if (gzip.extensions) {
ext = gzip.extensions
delete gzip.extensions
}
cfg.build.gzip = merge({
algorithm: 'gzip',
test: new RegExp('\\.(' + ext.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8
}, gzip)
}
if (this.#ctx.mode.pwa) {
cfg.pwa = merge({
workboxMode: 'GenerateSW',
injectPwaMetaTags: true,
swFilename: 'sw.js', // should be .js (as it's the distribution file, not the input file)
manifestFilename: 'manifest.json',
useCredentialsForManifestTag: false
}, cfg.pwa)
if (![ 'GenerateSW', 'InjectManifest' ].includes(cfg.pwa.workboxMode)) {
const msg = `Workbox strategy "${ cfg.pwa.workboxMode }" is invalid. `
+ 'Valid quasar.config file > pwa > workboxMode options are: GenerateSW or InjectManifest.'
if (failOnError === true) {
fatal(msg, 'FAIL')
}
warn(msg + ' Please fix it.\n')
return
}
cfg.build.env.SERVICE_WORKER_FILE = `${ cfg.build.publicPath }${ cfg.pwa.swFilename }`
cfg.metaConf.pwaManifestFile = appPaths.resolve.app(cfg.sourceFiles.pwaManifestFile)
cfg.sourceFiles.pwaServiceWorker = appPaths.resolve.app(cfg.sourceFiles.pwaServiceWorker)
}
else if (this.#ctx.mode.bex) {
cfg.metaConf.bexManifestFile = appPaths.resolve.app(cfg.sourceFiles.bexManifestFile)
}
if (this.#ctx.dev) {
const getUrl = hostname => `http${ cfg.devServer.server.type === 'https' ? 's' : '' }://${ hostname }:${ cfg.devServer.port }${ cfg.build.publicPath }`
const hostname = cfg.devServer.host === '0.0.0.0'
? 'localhost'
: cfg.devServer.host
cfg.metaConf.APP_URL = this.#ctx.mode.bex ? 'index.html' : getUrl(hostname)
cfg.metaConf.getUrl = getUrl
}
else if (this.#ctx.mode.cordova || this.#ctx.mode.capacitor || this.#ctx.mode.bex) {
cfg.metaConf.APP_URL = 'index.html'
}
Object.assign(cfg.build.env, {
NODE_ENV: this.#ctx.prod ? 'production' : 'development',
CLIENT: true,
SERVER: false,
DEV: this.#ctx.dev === true,
PROD: this.#ctx.prod === true,
DEBUGGING: cfg.metaConf.debugging === true,
MODE: this.#ctx.modeName,
VUE_ROUTER_MODE: cfg.build.vueRouterMode,
VUE_ROUTER_BASE: cfg.build.vueRouterBase
})
if (this.#ctx.mode.bex || this.#ctx.mode.capacitor || this.#ctx.mode.cordova) {
cfg.build.env.TARGET = this.#ctx.targetName
}
if (cfg.metaConf.APP_URL) {
cfg.build.env.APP_URL = cfg.metaConf.APP_URL
}
// get the env variables from host project env files
const { fileEnv, usedEnvFiles, envFromCache } = readFileEnv({
ctx: this.#ctx,
quasarConf: cfg
})
cfg.metaConf.fileEnv = fileEnv
if (envFromCache === false && usedEnvFiles.length !== 0) {
log(`Using .env files: ${ usedEnvFiles.join(', ') }`)
}
if (this.#ctx.mode.electron) {
cfg.sourceFiles.electronMain = appPaths.resolve.app(cfg.sourceFiles.electronMain)
if (!userCfg.electron?.preloadScripts) {
cfg.electron.preloadScripts = [ 'electron-preload' ]
}
if (this.#ctx.dev) {
if (this.#electronInspectPort === void 0) {
this.#electronInspectPort = await findClosestOpenPort(userCfg.electron?.inspectPort || 5858, '127.0.0.1')
}
cfg.electron.inspectPort = this.#electronInspectPort
}
else {
const { ensureInstall, getDefaultName } = await this.#ctx.cacheProxy.getModule('electron')
const icon = appPaths.resolve.electron('icons/icon.png')
const builderIcon = process.platform === 'linux'
// backward compatible (linux-512x512.png)
? (existsSync(icon) === true ? icon : appPaths.resolve.electron('icons/linux-512x512.png'))
: appPaths.resolve.electron('icons/icon')
cfg.electron = merge({
packager: {
asar: true,
icon: appPaths.resolve.electron('icons/icon'),
overwrite: true
},
builder: {
appId: 'quasar-app',
icon: builderIcon,
productName: this.#ctx.pkg.appPkg.productName || this.#ctx.pkg.appPkg.name || 'Quasar App',
directories: {
buildResources: appPaths.resolve.electron('')
}
}
}, cfg.electron, {
packager: {
dir: join(cfg.build.distDir, 'UnPackaged'),
out: join(cfg.build.distDir, 'Packaged')
},
builder: {
directories: {
app: join(cfg.build.distDir, 'UnPackaged'),
output: join(cfg.build.distDir, 'Packaged')
}
}
})
if (cfg.ctx.bundlerName) {
cfg.electron.bundler = cfg.ctx.bundlerName
}
else if (!cfg.electron.bundler) {
cfg.electron.bundler = getDefaultName()
}
ensureElectronArgv(cfg.electron.bundler, this.#ctx)
if (cfg.electron.bundler === 'packager') {
if (cfg.ctx.targetName) {
cfg.electron.packager.platform = cfg.ctx.targetName
}
if (cfg.ctx.archName) {
cfg.electron.packager.arch = cfg.ctx.archName
}
}
else {
cfg.electron.builder = {
config: cfg.electron.builder
}
if (cfg.ctx.targetName === 'mac' || cfg.ctx.targetName === 'darwin' || cfg.ctx.targetName === 'all') {
cfg.electron.builder.mac = []
}
if (cfg.ctx.targetName === 'linux' || cfg.ctx.targetName === 'all') {
cfg.electron.builder.linux = []
}
if (cfg.ctx.targetName === 'win' || cfg.ctx.targetName === 'win32' || cfg.ctx.targetName === 'all') {
cfg.electron.builder.win = []
}
if (cfg.ctx.archName) {
cfg.electron.builder[ cfg.ctx.archName ] = true
}
if (cfg.ctx.publish) {
cfg.electron.builder.publish = cfg.ctx.publish
}
}
ensureInstall(cfg.electron.bundler)
}
}
cfg.htmlVariables = merge({
ctx: cfg.ctx,
process: { env: cfg.build.env },
productName: escapeHTMLTagContent(this.#ctx.pkg.appPkg.productName),
productDescription: escapeHTMLAttribute(this.#ctx.pkg.appPkg.description)
}, cfg.htmlVariables)
if (this.#ctx.mode.capacitor && cfg.metaConf.versions.capacitorPluginSplashscreen && cfg.capacitor.hideSplashscreen !== false) {
cfg.metaConf.needsAppMountHook = true
}
return cfg
}
}