@quasar/app-vite
Version:
Quasar Framework App CLI with Vite
1,278 lines (1,083 loc) • 36.3 kB
JavaScript
import { join, isAbsolute, basename, dirname, relative } from 'node:path'
import { pathToFileURL } from 'node:url'
import { existsSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import fse from 'fs-extra'
import { merge } from 'webpack-merge'
import debounce from 'lodash/debounce.js'
import { build as esBuild, context as esContextBuild } from 'esbuild'
import { transformAssetUrls } from '@quasar/vite-plugin'
import { log, warn, fatal, tip } from './utils/logger.js'
import { appFilesValidations } from './utils/app-files-validations.js'
import { getPackageMajorVersion } from './utils/get-package-major-version.js'
import { resolveExtension } from './utils/resolve-extension.js'
import { ensureElectronArgv } from './utils/ensure-argv.js'
import {
quasarEsbuildInjectReplacementsDefine,
quasarEsbuildInjectReplacementsPlugin
} from './plugins/esbuild.inject-replacements.js'
import { quasarEsbuildVueShimPlugin } from './plugins/esbuild.vue-shim.js'
const urlRegex = /^http(s)?:\/\//i
import { findClosestOpenPort, localHostList } from './utils/net.js'
import { isMinimalTerminal } from './utils/is-minimal-terminal.js'
import { readFileEnv } from './utils/env.js'
import { BASELINE_WIDELY_AVAILABLE_TARGET_STRING } from './config-tools.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 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
}
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 } = await import('./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 }
}
let wsToken = null
function createWsToken() {
if (wsToken === null) {
const list =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const listLen = list.length
wsToken = ''
for (let i = 0; i < 12; i++) {
wsToken += list.charAt(Math.floor(Math.random() * listLen))
}
}
return wsToken
}
export class QuasarConfigFile {
/** @type {import('../types/configuration/context').QuasarContext} */
#ctx
#opts
#versions = {}
#address
#isWatching = false
#require
#tempFile
#cssVariables
#storeProvider
#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
this.#require =
appPaths.quasarConfigOutputFormat === 'cjs'
? createRequire(import.meta.url)
: () => {}
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
await Promise.all([
cacheProxy.getModule('cssVariables').then(cssVariables => {
this.#cssVariables = cssVariables
}),
cacheProxy.getModule('storeProvider').then(storeProvider => {
this.#storeProvider = storeProvider
}),
cacheProxy.getModule('vueRouterVersion').then(vueRouterVersion => {
this.#versions.vueRouter = vueRouterVersion
})
])
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-build', appPaths.appDir)
} else if (this.#ctx.mode.capacitor) {
const { capVersion } = await 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
}
/**
* @returns {import('esbuild').BuildOptions}
*/
#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-vite'
},
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 => {
// not ready yet; watch() has not been issued yet
if (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 this.#require.cache[tempFile]
}
try {
const res =
appPaths.quasarConfigOutputFormat === 'esm'
? await import(pathToFileURL(tempFile) + '?t=' + Date.now()) // we also need to cache bust it, hence the ?t= param
: this.#require(tempFile)
quasarConfigFn = res.default || res
} catch (e) {
// free up memory immediately
if (appPaths.quasarConfigOutputFormat === 'cjs') {
delete this.#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 this.#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 => {
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: {}
},
sourceFiles: {},
bin: {},
htmlVariables: {},
devServer: {
fs: {}
},
build: {
target: {},
viteVuePluginOptions: {},
vitePlugins: [],
env: {},
rawDefine: {},
envFiles: [],
resolve: {},
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 =
await 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.build = merge(
{
viteVuePluginOptions: {
isProduction: this.#ctx.prod === true,
template: {
isProd: this.#ctx.prod === true,
transformAssetUrls
}
},
vueOptionsAPI: true,
vueRouterMode: 'hash',
minify:
cfg.metaConf.debugging !== true &&
(this.#ctx.mode.bex !== true || cfg.bex.minify === true),
sourcemap: cfg.metaConf.debugging === true,
useFilenameHashes: true,
polyfillModulePreload: false,
distDir: join('dist', this.#ctx.modeName),
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: {
// 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-vite',
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.build.target.browser) {
cfg.build.target.browser = BASELINE_WIDELY_AVAILABLE_TARGET_STRING
}
if (!cfg.build.target.node) {
cfg.build.target.node = 'node22'
}
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
) {
cfg.build.vueRouterMode = 'hash'
}
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 (this.#ctx.dev) {
// we need this token to remain the same across quasar.config changes + server restarts
cfg.metaConf.bexWsToken = createWsToken()
}
}
if (!isAbsolute(cfg.build.distDir)) {
cfg.build.distDir = appPaths.resolve.app(cfg.build.distDir)
}
cfg.build.publicPath =
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`,
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) === 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 } = await import('../lib/modes/pwa/pwa-installation.js')
await addMode({ ctx: this.#ctx, silent: true })
}
this.#ctx.mode.pwa = cfg.ctx.mode.pwa = cfg.ssr.pwa === true
}
if (this.#ctx.dev) {
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.https = false
} 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.dev) {
if (cfg.devServer.https === true) {
const { getCertificate } = await import('@quasar/ssl-certificate')
const sslCertificate = await getCertificate({ log, fatal })
cfg.devServer.https = {
key: sslCertificate,
cert: sslCertificate
}
} else if (Object(cfg.devServer.https) === cfg.devServer.https) {
const { https } = cfg.devServer
// 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 https[prop] === 'string') {
try {
https[prop] = readFileSync(https[prop])
} catch (e) {
console.error(e)
console.log()
delete https[prop]
warn(
`The devServer.https.${prop} file could not be read. Removed the config.`
)
}
}
})
}
}
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.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)
}
}
const entryScriptAbsolutePath = appPaths.resolve.entry('client-entry.js')
const entryScriptWebPath = relative(
appPaths.appDir,
entryScriptAbsolutePath
)
cfg.metaConf.entryScript = {
absolutePath: entryScriptAbsolutePath,
webPath: cfg.build.publicPath + entryScriptWebPath,
// publicPath will be handled by Vite middleware:
tag: `<script type="module" src="/${entryScriptWebPath}"></script>`
}
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
}
}