UNPKG

@quasar/app

Version:

Quasar Framework local CLI

905 lines (781 loc) 26.1 kB
const path = require('path') const fs = require('fs') const merge = require('webpack-merge') const chokidar = require('chokidar') const debounce = require('lodash.debounce') const appPaths = require('./app-paths') const logger = require('./helpers/logger') const log = logger('app:quasar-conf') const warn = logger('app:quasar-conf', 'red') const appFilesValidations = require('./app-files-validations') const extensionRunner = require('./app-extension/extensions-runner') const supportIE = require('./helpers/support-ie') const cssVariables = require('./helpers/css-variables') const getDevlandFile = require('./helpers/get-devland-file') const transformAssetUrls = getDevlandFile('quasar/dist/transform-asset-urls.json') function encode (obj) { return JSON.stringify(obj, (_, value) => { return typeof value === 'function' ? `/fn(${value.toString()})` : value }) } function formatPublicPath (path) { if (!path) { return '' } if (!path.endsWith('/')) { path = `${path}/` } if (path.startsWith('http://') || path.startsWith('https://')) { return path } if (!path.startsWith('/')) { path = `/${path}` } return path } function formatRouterBase (publicPath) { if (!publicPath || !publicPath.startsWith('http')) { return publicPath } const match = publicPath.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/) return formatPublicPath(match[5] || '') } function parseBuildEnv (env) { const obj = {} Object.keys(env).forEach(key => { try { obj[key] = JSON.parse(env[key]) } catch (e) { obj[key] = '' } }) return obj } 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 uniqueFilter (value, index, self) { return self.indexOf(value) === index } 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 } /* * this.buildConfig - Compiled Object from quasar.conf.js * this.webpackConfig - Webpack config object for main thread * this.electronWebpackConfig - Webpack config object for electron main thread */ class QuasarConfig { constructor (ctx, opts = {}) { this.ctx = ctx this.opts = opts this.filename = appPaths.resolve.app('quasar.conf.js') this.pkg = require(appPaths.resolve.app('package.json')) this.watch = opts.onBuildChange || opts.onAppChange if (this.watch) { // Start watching for quasar.config.js changes chokidar .watch(this.filename, { watchers: { chokidar: { ignoreInitial: true } } }) .on('change', debounce(async () => { console.log() log(`quasar.conf.js changed`) try { await this.prepare() } catch (e) { if (e.message !== 'NETWORK_ERROR') { console.log(e) warn(`⚠️ quasar.conf.js has JS errors. Please fix them then save file again.`) warn() } return } await this.compile() if (this.webpackConfigChanged) { opts.onBuildChange() } else { opts.onAppChange() } }, 1000)) if (this.ctx.mode.ssr) { const SsrExtension = require('./ssr/ssr-extension') if (!SsrExtension.isValid()) { process.exit(1) } chokidar .watch(appPaths.ssrDir, { watchers: { chokidar: { ignoreInitial: true } } }) .on('change', debounce(async () => { console.log() log(`src-ssr/* changed`) SsrExtension.deleteCache() if (SsrExtension.isValid()) { // trigger build update opts.onBuildChange() } else { warn(`⚠️ [FAIL] Please fix the error then save the file so we can continue.`) } }, 1000)) } } } // synchronous for build async prepare () { this.readConfig() const cfg = merge({ ctx: this.ctx, css: [], boot: [], vendor: { add: [], remove: [] }, build: { transpileDependencies: [], transformAssetUrls: {}, stylusLoaderOptions: {}, sassLoaderOptions: {}, scssLoaderOptions: {}, lessLoaderOptions: {}, env: {}, uglifyOptions: { compress: {}, mangle: {} } }, devServer: {}, animations: [], extras: [], sourceFiles: {}, ssr: { componentCache: {} }, pwa: { workboxOptions: {}, manifest: { icons: [] }, metaVariables: {} }, electron: { unPackagedInstallParams: [], packager: {}, builder: {} }, cordova: {}, capacitor: {}, bin: {}, htmlVariables: {} }, this.quasarConfigFunction(this.ctx)) if (cfg.framework === void 0 || cfg.framework === 'all') { cfg.framework = { all: true } } if (!cfg.framework.components) { cfg.framework.components = [] } if (!cfg.framework.directives) { cfg.framework.directives = [] } if (!cfg.framework.plugins) { cfg.framework.plugins = [] } if (!cfg.framework.config) { cfg.framework.config = {} } 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 } else if (!cfg.devServer.port) { cfg.devServer.port = 8080 } 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.onAddress !== void 0 ? await this.opts.onAddress(addr) : addr // if network error while running if (to === null) { throw new Error('NETWORK_ERROR') } cfg.devServer = merge(cfg.devServer, to) this.address = { from: addr, to: { host: cfg.devServer.host, port: cfg.devServer.port } } } } this.quasarConfig = cfg } getBuildConfig () { return this.buildConfig } getWebpackConfig () { return this.webpackConfig } readConfig () { log(`Reading quasar.conf.js`) if (fs.existsSync(this.filename)) { delete require.cache[this.filename] this.quasarConfigFunction = require(this.filename) } else { warn(`⚠️ [FAIL] Could not load quasar.conf.js config file`) process.exit(1) } } async compile () { let cfg = this.quasarConfig await extensionRunner.runHook('extendQuasarConf', async hook => { log(`Extension(${hook.api.extId}): Extending quasar.conf...`) await hook.fn(cfg, hook.api) }) // if watching for changes, // then determine the type (webpack related or not) if (this.watch) { const newConfigSnapshot = [ cfg.build ? encode(cfg.build) : '', cfg.ssr ? cfg.ssr.pwa : '', cfg.framework ? cfg.framework.all + cfg.framework.autoImportComponentCase : '', cfg.devServer ? encode(cfg.devServer) : '', cfg.pwa ? encode(cfg.pwa) : '', cfg.electron ? encode(cfg.electron) : '' ].join('') if (this.oldConfigSnapshot) { this.webpackConfigChanged = newConfigSnapshot !== this.oldConfigSnapshot } this.oldConfigSnapshot = newConfigSnapshot } // make sure these exist cfg.__needsAppMountHook = false cfg.__vueDevtools = false // make sure it exists cfg.supportIE = supportIE(cfg.supportIE, this.ctx) 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.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 = cfg.extras.filter(uniqueFilter) } if (cfg.framework.all !== true && cfg.framework.all !== 'auto') { cfg.framework.all = false } else if (cfg.framework.all === 'auto') { if (!['kebab', 'pascal', 'combined'].includes(cfg.framework.autoImportComponentCase)) { cfg.framework.autoImportComponentCase = 'kebab' } } cfg.framework.components = cfg.framework.components.filter(uniqueFilter) cfg.framework.directives = cfg.framework.directives.filter(uniqueFilter) cfg.framework.plugins = cfg.framework.plugins.filter(uniqueFilter) cfg.build = merge({ transformAssetUrls: Object.assign({ video: ['src', 'poster'], source: 'src', img: 'src', image: ['xlink:href', 'href'], use: ['xlink:href', 'href'] }, transformAssetUrls), showProgress: true, scopeHoisting: true, productName: this.pkg.productName, productDescription: this.pkg.description, extractCSS: this.ctx.prod, sourceMap: this.ctx.dev, minify: this.ctx.prod, distDir: path.join('dist', this.ctx.modeName), htmlFilename: 'index.html', webpackManifest: this.ctx.prod, vueRouterMode: 'hash', preloadChunks: this.ctx.prod, forceDevPublicPath: false, // transpileDependencies: [], // leaving here for completeness devtool: this.ctx.dev ? '#cheap-module-eval-source-map' : '#source-map', env: { NODE_ENV: `"${this.ctx.prod ? 'production' : 'development'}"`, CLIENT: true, SERVER: false, DEV: this.ctx.dev, PROD: this.ctx.prod, MODE: `"${this.ctx.modeName}"` }, uglifyOptions: { compress: { // turn off flags with small gains to speed up minification arrows: false, collapse_vars: false, // 0.3kb comparisons: false, computed_props: 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 noticable 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 }, mangle: { safari10: true } } }, cfg.build) cfg.build.transpileDependencies = cfg.build.transpileDependencies.filter(uniqueRegexFilter) cfg.__loadingBar = cfg.framework.all === true || cfg.framework.plugins.includes('LoadingBar') cfg.__meta = cfg.framework.all === true || cfg.framework.plugins.includes('Meta') if (this.ctx.dev || this.ctx.debug) { Object.assign(cfg.build, { minify: false, gzip: false }) } if (this.ctx.dev) { cfg.build.extractCSS = false cfg.build.preloadChunks = false } if (this.ctx.debug) { cfg.build.sourceMap = true } if (this.ctx.mode.ssr) { Object.assign(cfg.build, { vueRouterMode: 'history', publicPath: '/', gzip: false }) } else if (this.ctx.mode.cordova || this.ctx.mode.capacitor || this.ctx.mode.electron) { Object.assign(cfg.build, { htmlFilename: 'index.html', vueRouterMode: 'hash', gzip: false, webpackManifest: false, preloadChunks: false }) } if (!path.isAbsolute(cfg.build.distDir)) { cfg.build.distDir = appPaths.resolve.app(cfg.build.distDir) } if (this.ctx.mode.cordova || this.ctx.mode.capacitor) { cfg.build.packagedDistDir = path.join(cfg.build.distDir, this.ctx.targetName) } if (this.ctx.mode.cordova || this.ctx.mode.capacitor) { cfg.build.distDir = appPaths.resolve[this.ctx.modeName]('www') } else if (this.ctx.mode.electron) { cfg.build.packagedDistDir = cfg.build.distDir cfg.build.distDir = path.join(cfg.build.distDir, 'UnPackaged') } cfg.build.publicPath = (this.ctx.prod || cfg.build.forceDevPublicPath) && cfg.build.publicPath && ['spa', 'pwa'].includes(this.ctx.modeName) ? formatPublicPath(cfg.build.publicPath) : (cfg.build.vueRouterMode === 'hash' ? '' : '/') /* 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) /* careful if you configure the following; make sure that you really know what you are doing */ cfg.build.appBase = cfg.build.appBase !== void 0 ? cfg.build.appBase : cfg.build.publicPath cfg.sourceFiles = merge({ rootComponent: 'src/App.vue', router: 'src/router/index', store: 'src/store/index', indexHtmlTemplate: 'src/index.template.html', registerServiceWorker: 'src-pwa/register-service-worker.js', serviceWorker: 'src-pwa/custom-service-worker.js', electronMainDev: 'src-electron/main-process/electron-main.dev.js', electronMainProd: 'src-electron/main-process/electron-main.js', ssrServerIndex: 'src-ssr/index.js' }, cfg.sourceFiles) // do we got vuex? const storePath = appPaths.resolve.app(cfg.sourceFiles.store) cfg.store = ( fs.existsSync(storePath) || fs.existsSync(storePath + '.js') || fs.existsSync(storePath + '.ts') ) //make sure we have preFetch in config cfg.preFetch = cfg.preFetch || false if (cfg.animations === 'all') { cfg.animations = require('./helpers/animations') } if (this.ctx.mode.ssr) { cfg.ssr = merge({ pwa: false, manualHydration: false, componentCache: { max: 1000, maxAge: 1000 * 60 * 15 } }, cfg.ssr) cfg.ssr.debug = this.ctx.debug cfg.ssr.__templateOpts = JSON.stringify( Object.assign({}, cfg.ssr, { preloadChunks: cfg.build.preloadChunks === true }), null, 2 ) cfg.ssr.__templateFlags = { meta: cfg.__meta } const file = appPaths.resolve.app(cfg.sourceFiles.ssrServerIndex) cfg.ssr.__dir = path.dirname(file) cfg.ssr.__index = path.basename(file) if (cfg.ssr.pwa) { require('./mode/install-missing')('pwa') } this.ctx.mode.pwa = cfg.ctx.mode.pwa = cfg.ssr.pwa !== false } if (this.ctx.dev) { const originalBefore = cfg.devServer.before const openInEditor = require('launch-editor-middleware') cfg.devServer = merge({ publicPath: cfg.build.publicPath, hot: true, inline: true, overlay: true, quiet: true, historyApiFallback: !this.ctx.mode.ssr, noInfo: true, disableHostCheck: true, compress: true, open: true }, cfg.devServer, { contentBase: false, watchContentBase: false, before: app => { if (!this.ctx.mode.ssr) { const express = require('express') app.use((cfg.build.publicPath || '/') + 'statics', express.static(appPaths.resolve.src('statics'), { 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)) originalBefore && originalBefore(app) } }) if (this.ctx.vueDevtools === true || cfg.devServer.vueDevtools === true) { cfg.__needsAppMountHook = true cfg.__vueDevtools = { host: cfg.devServer.host === '0.0.0.0' ? 'localhost' : cfg.devServer.host, port: 8098 } } // make sure the prop is not supplied to webpack dev server if (cfg.devServer.hasOwnProperty('vueDevtools')) { delete cfg.devServer.vueDevtools } if (this.ctx.mode.cordova || this.ctx.mode.capacitor || this.ctx.mode.electron) { cfg.devServer.open = false if (this.ctx.mode.electron) { cfg.devServer.https = false } } if (cfg.devServer.open) { const isMinimalTerminal = require('./helpers/is-minimal-terminal') if (isMinimalTerminal) { cfg.devServer.open = false } } if (cfg.devServer.open) { cfg.__devServer = { open: !!cfg.devServer.open, openOptions: cfg.devServer.open !== true ? cfg.devServer.open : false } cfg.devServer.open = false } else { cfg.__devServer = {} } } if (cfg.build.gzip) { let 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({ filename: '[path].gz[query]', algorithm: 'gzip', test: new RegExp('\\.(' + ext.join('|') + ')$'), threshold: 10240, minRatio: 0.8 }, gzip) } if (this.ctx.mode.pwa) { cfg.build.webpackManifest = false cfg.build.preloadChunks = true cfg.pwa = merge({ workboxPluginMode: 'GenerateSW', workboxOptions: {}, manifest: { name: this.pkg.productName || this.pkg.name || 'Quasar App', short_name: this.pkg.name || 'quasar-pwa', description: this.pkg.description, display: 'standalone', start_url: '.' }, metaVariables: { appleMobileWebAppCapable: 'yes', appleMobileWebAppStatusBarStyle: 'default', appleTouchIcon120: 'statics/icons/apple-icon-120x120.png', appleTouchIcon180: 'statics/icons/apple-icon-180x180.png', appleTouchIcon152: 'statics/icons/apple-icon-152x152.png', appleTouchIcon167: 'statics/icons/apple-icon-167x167.png', appleSafariPinnedTab: 'statics/icons/safari-pinned-tab.svg', msapplicationTileImage: 'statics/icons/ms-icon-144x144.png', msapplicationTileColor: '#000000' } }, cfg.pwa) if (cfg.pwa.manifest.icons.length === 0) { console.log() console.log(`⚠️ PWA manifest in quasar.conf.js > pwa > manifest is missing "icons" prop.`) console.log() process.exit(1) } if (!['GenerateSW', 'InjectManifest'].includes(cfg.pwa.workboxPluginMode)) { console.log() console.log(`⚠️ Workbox webpack plugin mode "${cfg.pwa.workboxPluginMode}" is invalid.`) console.log(` Valid Workbox modes are: GenerateSW, InjectManifest`) console.log() process.exit(1) } if (cfg.pwa.cacheExt) { console.log() console.log(`⚠️ Quasar CLI now uses Workbox, so quasar.conf.js > pwa > cacheExt is no longer relevant.`) console.log(` Please remove this property and try again.`) console.log() process.exit(1) } if ( fs.existsSync(appPaths.resolve.pwa('service-worker-dev.js')) || fs.existsSync(appPaths.resolve.pwa('service-worker-prod.js')) ) { console.log() console.log(`⚠️ Quasar CLI now uses Workbox, so src-pwa/service-worker-dev.js and src-pwa/service-worker-prod.js are obsolete.`) console.log(` Please remove and add PWA mode again:`) console.log(` $ quasar mode -r pwa # Warning: this will delete /src-pwa !`) console.log(` $ quasar mode -a pwa`) console.log() process.exit(1) } cfg.pwa.manifest.icons = cfg.pwa.manifest.icons.map(icon => { icon.src = `${cfg.build.publicPath}${icon.src}` return icon }) } if (this.ctx.dev) { const host = cfg.devServer.host === '0.0.0.0' ? 'localhost' : cfg.devServer.host const urlPath = cfg.build.vueRouterMode === 'hash' ? (cfg.build.htmlFilename !== 'index.html' ? (cfg.build.publicPath ? '' : '/') + cfg.build.htmlFilename : '') : '' cfg.build.APP_URL = `http${cfg.devServer.https ? 's' : ''}://${host}:${cfg.devServer.port}${cfg.build.publicPath}${urlPath}` } else if (this.ctx.mode.cordova || this.ctx.mode.capacitor) { cfg.build.APP_URL = 'index.html' } else if (this.ctx.mode.electron) { cfg.build.APP_URL = `file://" + __dirname + "/index.html` } cfg.build.env = merge(cfg.build.env, { VUE_ROUTER_MODE: `"${cfg.build.vueRouterMode}"`, VUE_ROUTER_BASE: `"${cfg.build.vueRouterBase}"`, APP_URL: `"${cfg.build.APP_URL}"` }) if (this.ctx.mode.pwa) { cfg.build.env.SERVICE_WORKER_FILE = `"${cfg.build.publicPath}service-worker.js"` } cfg.build.env = { 'process.env': cfg.build.env } appFilesValidations(cfg) if (this.ctx.mode.electron) { if (this.ctx.dev) { cfg.electron = merge({ nodeIntegration: true }, cfg.electron) if (cfg.electron.nodeIntegration) { cfg.build.env.__statics = `"${appPaths.resolve.src('statics').replace(/\\/g, '\\\\')}"` } } else { const bundler = require('./electron/bundler') cfg.electron = merge({ nodeIntegration: true, packager: { asar: true, icon: appPaths.resolve.electron('icons/icon'), overwrite: true }, builder: { appId: 'quasar-app', productName: this.pkg.productName || this.pkg.name || 'Quasar App', directories: { buildResources: appPaths.resolve.electron('') } } }, cfg.electron, { packager: { dir: cfg.build.distDir, out: cfg.build.packagedDistDir }, builder: { directories: { app: cfg.build.distDir, output: path.join(cfg.build.packagedDistDir, 'Packaged') } } }) if (cfg.ctx.bundlerName) { cfg.electron.bundler = cfg.ctx.bundlerName } else if (!cfg.electron.bundler) { cfg.electron.bundler = bundler.getDefaultName() } 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 } bundler.ensureBuilderCompatibility() } bundler.ensureInstall(cfg.electron.bundler) } } if (this.ctx.mode.capacitor && cfg.capacitor.hideSplashscreen !== false) { cfg.__needsAppMountHook = true } cfg.__html = { variables: { ctx: cfg.ctx, process: { env: parseBuildEnv(cfg.build.env['process.env']) }, productName: cfg.build.productName, productDescription: cfg.build.productDescription, ...cfg.htmlVariables }, minifyOptions: cfg.build.minify ? { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true // more options: // https://github.com/kangax/html-minifier#options-quick-reference } : void 0 } // used by .quasar entry templates cfg.__css = { quasarSrcExt: cssVariables.quasarSrcExt } this.webpackConfig = await require('./webpack')(cfg) this.buildConfig = cfg } } module.exports = QuasarConfig