UNPKG

quasar-cli

Version:
746 lines (649 loc) 20.4 kB
const path = require('path'), fs = require('fs'), merge = require('webpack-merge'), chokidar = require('chokidar'), debounce = require('lodash.debounce') const appPaths = require('./app-paths'), logger = require('./helpers/logger'), log = logger('app:quasar-conf'), warn = logger('app:quasar-conf', 'red'), legacyValidations = require('./legacy-validations') function getQuasarConfigCtx (opts) { const ctx = { dev: opts.dev || false, prod: opts.prod || false, theme: {}, themeName: opts.theme, mode: {}, modeName: opts.mode, target: {}, targetName: opts.target, emulator: opts.emulator, arch: {}, archName: opts.arch, bundler: {}, bundlerName: opts.bundler, debug: opts.debug } ctx.theme[opts.theme] = true ctx.mode[opts.mode] = true if (opts.target) { ctx.target[opts.target] = true } if (opts.arch) { ctx.arch[opts.arch] = true } if (opts.bundler) { ctx.bundler[opts.bundler] = true } return ctx } function encode (obj) { return JSON.stringify(obj, (key, value) => { return typeof value === 'function' ? `/fn(${value.toString()})` : value }) } function formatPublicPath (path) { if (!path) { return path } if (!path.startsWith('/')) { path = `/${path}` } if (!path.endsWith('/')) { path = `${path}/` } return path } function parseBuildEnv (env) { const obj = {} Object.keys(env).forEach(key => { try { obj[key] = JSON.parse(env[key]) } catch (e) { obj[key] = '' } }) return obj } /* * 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 (opts) { this.filename = appPaths.resolve.app('quasar.conf.js') this.pkg = require(appPaths.resolve.app('package.json')) this.opts = opts this.ctx = getQuasarConfigCtx(opts) 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 } this.compile() if (this.webpackConfigChanged) { opts.onBuildChange() } else { opts.onAppChange() } }, 1000)) if (this.ctx.mode.ssr) { this.ssrExtensionFile = appPaths.resolve.ssr('extension.js') chokidar .watch(this.ssrExtensionFile, { watchers: { chokidar: { ignoreInitial: true } } }) .on('change', debounce(async () => { console.log() log(`src-ssr/extension.js changed`) try { this.readSSRextension() } catch (e) { if (e.message !== 'NETWORK_ERROR') { console.log(e) warn(`src-ssr/extension.js has JS errors. Please fix them then save file again.`) warn() } return } opts.onBuildChange() }, 1000)) } } } // synchronous for build async prepare () { this.readConfig() if (this.watch && this.ctx.mode.ssr) { this.readSSRextension() } const cfg = merge({ ctx: this.ctx, css: false, plugins: false, animations: false, extras: false }, this.quasarConfigFunction(this.ctx)) if (cfg.framework === void 0 || cfg.framework === 'all') { cfg.framework = { all: true } } cfg.framework.config = cfg.framework.config || {} if (this.ctx.dev) { cfg.devServer = cfg.devServer || {} 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 = await this.opts.onAddress(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) } } readSSRextension () { log(`Reading src-ssr/extension.js`) if (fs.existsSync(this.ssrExtensionFile)) { delete require.cache[this.ssrExtensionFile] this.ssrExtension = require(this.ssrExtensionFile) } else { warn(`⚠️ [FAIL] Could not load src-ssr/extension.js file`) process.exit(1) } } compile () { let cfg = this.quasarConfig // 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.all, 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 it exists cfg.supportIE = this.ctx.mode.electron ? false : (cfg.supportIE || false) cfg.vendor = merge({ vendor: { add: false, remove: false } }, cfg.vendor || {}) if (cfg.vendor.add) { cfg.vendor.add = cfg.vendor.add.filter(v => v).join('|') if (cfg.vendor.add) { cfg.vendor.add = new RegExp(cfg.vendor.add) } } if (cfg.vendor.remove) { cfg.vendor.remove = cfg.vendor.remove.filter(v => v).join('|') if (cfg.vendor.remove) { cfg.vendor.remove = new RegExp(cfg.vendor.remove) } } if (cfg.css) { cfg.css = cfg.css.filter(_ => _).map( asset => asset[0] === '~' ? asset.substring(1) : `src/css/${asset}` ) if (cfg.css.length === 0) { cfg.css = false } } if (cfg.plugins) { cfg.plugins = cfg.plugins.filter(_ => _).map(asset => { return typeof asset === 'string' ? { path: asset } : asset }).filter(asset => asset.path) if (cfg.plugins.length === 0) { cfg.plugins = false } } cfg.build = merge({ 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}-${this.ctx.themeName}`), htmlFilename: 'index.html', webpackManifest: this.ctx.prod, vueRouterMode: 'hash', preloadChunks: true, transpileDependencies: [], 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, THEME: `"${this.ctx.themeName}"`, 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: { /* Support non-standard Safari 10/11. By default `uglify-es` will not work around Safari 10/11 bugs. */ safari10: true } } }, cfg.build || {}) cfg.build.transpileDependencies.push(/[\\/]node_modules[\\/]quasar-framework[\\/]/) cfg.__loadingBar = cfg.framework.all || (cfg.framework.plugins && cfg.framework.plugins.includes('LoadingBar')) cfg.__meta = cfg.framework.all || (cfg.framework.plugins && cfg.framework.plugins.includes('Meta')) if (this.ctx.dev || this.ctx.debug) { Object.assign(cfg.build, { minify: false, extractCSS: false, gzip: false }) } if (this.ctx.debug) { cfg.build.sourceMap = true } if (this.ctx.mode.ssr) { Object.assign(cfg.build, { extractCSS: false, vueRouterMode: 'history', publicPath: '/', gzip: false }) } else if (this.ctx.mode.cordova || this.ctx.mode.electron) { Object.assign(cfg.build, { extractCSS: false, htmlFilename: 'index.html', vueRouterMode: 'hash', gzip: false, webpackManifest: false }) } if (this.ctx.mode.cordova) { cfg.build.distDir = appPaths.resolve.app(path.join('src-cordova', 'www')) } else if (!path.isAbsolute(cfg.build.distDir)) { cfg.build.distDir = appPaths.resolve.app(cfg.build.distDir) } if (this.ctx.mode.electron) { cfg.build.packagedElectronDist = cfg.build.distDir cfg.build.distDir = path.join(cfg.build.distDir, 'UnPackaged') } cfg.build.publicPath = this.ctx.prod && cfg.build.publicPath && ['spa', 'pwa'].includes(this.ctx.modeName) ? formatPublicPath(cfg.build.publicPath) : (cfg.build.vueRouterMode !== 'hash' ? '/' : '') cfg.build.appBase = cfg.build.vueRouterMode === 'history' ? cfg.build.publicPath : '' cfg.sourceFiles = merge({ rootComponent: 'src/App.vue', router: 'src/router/index.js', store: 'src/store/index.js', 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? cfg.store = fs.existsSync(appPaths.resolve.app(cfg.sourceFiles.store)) //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, componentCache: { max: 1000, maxAge: 1000 * 60 * 15 } }, cfg.ssr || {}) cfg.ssr.debug = this.ctx.debug cfg.ssr.__templateOpts = JSON.stringify(cfg.ssr, 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 this.watch && (cfg.__ssrExtension = this.ssrExtension) } if (this.ctx.dev) { const initialPort = cfg.devServer && cfg.devServer.port, initialHost = cfg.devServer && cfg.devServer.host 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: [ appPaths.srcDir ] }) if (this.ctx.mode.ssr) { cfg.devServer.contentBase = false } else if (this.ctx.mode.cordova || this.ctx.mode.electron) { cfg.devServer.open = false if (this.ctx.mode.electron) { cfg.devServer.https = false } } if (this.ctx.mode.cordova) { cfg.devServer.contentBase.push( appPaths.resolve.cordova(`platforms/${this.ctx.targetName}/platform_www`) ) } if (cfg.devServer.open) { const isMinimalTerminal = require('./helpers/is-minimal-terminal') if (isMinimalTerminal) { cfg.devServer.open = false } } } 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.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: '.' } }, cfg.pwa || {}) 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.htmlFilename : '') : ''}` cfg.build.APP_URL = `http${cfg.devServer.https ? 's' : ''}://${host}:${cfg.devServer.port}/${urlPath}` } else if (this.ctx.mode.cordova) { 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.publicPath}"`, 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 } if (this.ctx.mode.electron) { if (this.ctx.dev) { cfg.build.env.__statics = `"${appPaths.resolve.src('statics').replace(/\\/g, '\\\\')}"` } } else { cfg.build.env.__statics = `"${this.ctx.dev ? '/' : cfg.build.publicPath || '/'}statics"` } legacyValidations(cfg) if (this.ctx.mode.cordova && !cfg.cordova) { cfg.cordova = {} } if (this.ctx.mode.electron) { if (this.ctx.prod) { const bundler = require('./electron/bundler') cfg.electron = merge({ 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.packagedElectronDist }, builder: { directories: { app: cfg.build.distDir, output: path.join(cfg.build.packagedElectronDist, '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 = { platform: cfg.ctx.targetName, arch: cfg.ctx.archName, config: cfg.electron.builder } bundler.ensureBuilderCompatibility() } bundler.ensureInstall(cfg.electron.bundler) } } cfg.__html = { variables: Object.assign({ 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 } : undefined } this.webpackConfig = require('./webpack')(cfg) this.buildConfig = cfg } } module.exports = QuasarConfig