UNPKG

20.4 kBJavaScriptView Raw
1const
2 path = require('path'),
3 fs = require('fs'),
4 merge = require('webpack-merge'),
5 chokidar = require('chokidar'),
6 debounce = require('lodash.debounce')
7
8const
9 appPaths = require('./app-paths'),
10 logger = require('./helpers/logger'),
11 log = logger('app:quasar-conf'),
12 warn = logger('app:quasar-conf', 'red'),
13 legacyValidations = require('./legacy-validations')
14
15function getQuasarConfigCtx (opts) {
16 const ctx = {
17 dev: opts.dev || false,
18 prod: opts.prod || false,
19 theme: {},
20 themeName: opts.theme,
21 mode: {},
22 modeName: opts.mode,
23 target: {},
24 targetName: opts.target,
25 emulator: opts.emulator,
26 arch: {},
27 archName: opts.arch,
28 bundler: {},
29 bundlerName: opts.bundler,
30 debug: opts.debug
31 }
32 ctx.theme[opts.theme] = true
33 ctx.mode[opts.mode] = true
34
35 if (opts.target) {
36 ctx.target[opts.target] = true
37 }
38 if (opts.arch) {
39 ctx.arch[opts.arch] = true
40 }
41 if (opts.bundler) {
42 ctx.bundler[opts.bundler] = true
43 }
44
45 return ctx
46}
47
48function encode (obj) {
49 return JSON.stringify(obj, (key, value) => {
50 return typeof value === 'function'
51 ? `/fn(${value.toString()})`
52 : value
53 })
54}
55
56function formatPublicPath (path) {
57 if (!path) {
58 return path
59 }
60 if (!path.startsWith('/')) {
61 path = `/${path}`
62 }
63 if (!path.endsWith('/')) {
64 path = `${path}/`
65 }
66 return path
67}
68
69function parseBuildEnv (env) {
70 const obj = {}
71 Object.keys(env).forEach(key => {
72 try {
73 obj[key] = JSON.parse(env[key])
74 }
75 catch (e) {
76 obj[key] = ''
77 }
78 })
79 return obj
80}
81
82/*
83 * this.buildConfig - Compiled Object from quasar.conf.js
84 * this.webpackConfig - Webpack config object for main thread
85 * this.electronWebpackConfig - Webpack config object for electron main thread
86 */
87
88class QuasarConfig {
89 constructor (opts) {
90 this.filename = appPaths.resolve.app('quasar.conf.js')
91 this.pkg = require(appPaths.resolve.app('package.json'))
92 this.opts = opts
93 this.ctx = getQuasarConfigCtx(opts)
94 this.watch = opts.onBuildChange || opts.onAppChange
95
96 if (this.watch) {
97 // Start watching for quasar.config.js changes
98 chokidar
99 .watch(this.filename, { watchers: { chokidar: { ignoreInitial: true } } })
100 .on('change', debounce(async () => {
101 console.log()
102 log(`quasar.conf.js changed`)
103
104 try {
105 await this.prepare()
106 }
107 catch (e) {
108 if (e.message !== 'NETWORK_ERROR') {
109 console.log(e)
110 warn(`quasar.conf.js has JS errors. Please fix them then save file again.`)
111 warn()
112 }
113
114 return
115 }
116
117 this.compile()
118
119 if (this.webpackConfigChanged) {
120 opts.onBuildChange()
121 }
122 else {
123 opts.onAppChange()
124 }
125 }, 1000))
126
127 if (this.ctx.mode.ssr) {
128 this.ssrExtensionFile = appPaths.resolve.ssr('extension.js')
129
130 chokidar
131 .watch(this.ssrExtensionFile, { watchers: { chokidar: { ignoreInitial: true } } })
132 .on('change', debounce(async () => {
133 console.log()
134 log(`src-ssr/extension.js changed`)
135
136 try {
137 this.readSSRextension()
138 }
139 catch (e) {
140 if (e.message !== 'NETWORK_ERROR') {
141 console.log(e)
142 warn(`src-ssr/extension.js has JS errors. Please fix them then save file again.`)
143 warn()
144 }
145
146 return
147 }
148
149 opts.onBuildChange()
150 }, 1000))
151 }
152 }
153 }
154
155 // synchronous for build
156 async prepare () {
157 this.readConfig()
158
159 if (this.watch && this.ctx.mode.ssr) {
160 this.readSSRextension()
161 }
162
163 const cfg = merge({
164 ctx: this.ctx,
165 css: false,
166 plugins: false,
167 animations: false,
168 extras: false
169 }, this.quasarConfigFunction(this.ctx))
170
171 if (cfg.framework === void 0 || cfg.framework === 'all') {
172 cfg.framework = {
173 all: true
174 }
175 }
176 cfg.framework.config = cfg.framework.config || {}
177
178 if (this.ctx.dev) {
179 cfg.devServer = cfg.devServer || {}
180
181 if (this.opts.host) {
182 cfg.devServer.host = this.opts.host
183 }
184 else if (!cfg.devServer.host) {
185 cfg.devServer.host = '0.0.0.0'
186 }
187
188 if (this.opts.port) {
189 cfg.devServer.port = this.opts.port
190 }
191 else if (!cfg.devServer.port) {
192 cfg.devServer.port = 8080
193 }
194
195 if (
196 this.address &&
197 this.address.from.host === cfg.devServer.host &&
198 this.address.from.port === cfg.devServer.port
199 ) {
200 cfg.devServer.host = this.address.to.host
201 cfg.devServer.port = this.address.to.port
202 }
203 else {
204 const addr = {
205 host: cfg.devServer.host,
206 port: cfg.devServer.port
207 }
208 const to = await this.opts.onAddress(addr)
209
210 // if network error while running
211 if (to === null) {
212 throw new Error('NETWORK_ERROR')
213 }
214
215 cfg.devServer = merge(cfg.devServer, to)
216 this.address = {
217 from: addr,
218 to: {
219 host: cfg.devServer.host,
220 port: cfg.devServer.port
221 }
222 }
223 }
224 }
225
226 this.quasarConfig = cfg
227 }
228
229 getBuildConfig () {
230 return this.buildConfig
231 }
232
233 getWebpackConfig () {
234 return this.webpackConfig
235 }
236
237 readConfig () {
238 log(`Reading quasar.conf.js`)
239
240 if (fs.existsSync(this.filename)) {
241 delete require.cache[this.filename]
242 this.quasarConfigFunction = require(this.filename)
243 }
244 else {
245 warn(`⚠️ [FAIL] Could not load quasar.conf.js config file`)
246 process.exit(1)
247 }
248 }
249
250 readSSRextension () {
251 log(`Reading src-ssr/extension.js`)
252
253 if (fs.existsSync(this.ssrExtensionFile)) {
254 delete require.cache[this.ssrExtensionFile]
255 this.ssrExtension = require(this.ssrExtensionFile)
256 }
257 else {
258 warn(`⚠️ [FAIL] Could not load src-ssr/extension.js file`)
259 process.exit(1)
260 }
261 }
262
263 compile () {
264 let cfg = this.quasarConfig
265
266 // if watching for changes,
267 // then determine the type (webpack related or not)
268 if (this.watch) {
269 const newConfigSnapshot = [
270 cfg.build ? encode(cfg.build) : '',
271 cfg.ssr ? cfg.ssr.pwa : '',
272 cfg.framework.all,
273 cfg.devServer ? encode(cfg.devServer) : '',
274 cfg.pwa ? encode(cfg.pwa) : '',
275 cfg.electron ? encode(cfg.electron) : ''
276 ].join('')
277
278 if (this.oldConfigSnapshot) {
279 this.webpackConfigChanged = newConfigSnapshot !== this.oldConfigSnapshot
280 }
281
282 this.oldConfigSnapshot = newConfigSnapshot
283 }
284
285 // make sure it exists
286 cfg.supportIE = this.ctx.mode.electron
287 ? false
288 : (cfg.supportIE || false)
289
290 cfg.vendor = merge({
291 vendor: {
292 add: false,
293 remove: false
294 }
295 }, cfg.vendor || {})
296
297 if (cfg.vendor.add) {
298 cfg.vendor.add = cfg.vendor.add.filter(v => v).join('|')
299 if (cfg.vendor.add) {
300 cfg.vendor.add = new RegExp(cfg.vendor.add)
301 }
302 }
303 if (cfg.vendor.remove) {
304 cfg.vendor.remove = cfg.vendor.remove.filter(v => v).join('|')
305 if (cfg.vendor.remove) {
306 cfg.vendor.remove = new RegExp(cfg.vendor.remove)
307 }
308 }
309
310 if (cfg.css) {
311 cfg.css = cfg.css.filter(_ => _).map(
312 asset => asset[0] === '~' ? asset.substring(1) : `src/css/${asset}`
313 )
314
315 if (cfg.css.length === 0) {
316 cfg.css = false
317 }
318 }
319
320 if (cfg.plugins) {
321 cfg.plugins = cfg.plugins.filter(_ => _).map(asset => {
322 return typeof asset === 'string'
323 ? { path: asset }
324 : asset
325 }).filter(asset => asset.path)
326
327 if (cfg.plugins.length === 0) {
328 cfg.plugins = false
329 }
330 }
331
332 cfg.build = merge({
333 showProgress: true,
334 scopeHoisting: true,
335 productName: this.pkg.productName,
336 productDescription: this.pkg.description,
337 extractCSS: this.ctx.prod,
338 sourceMap: this.ctx.dev,
339 minify: this.ctx.prod,
340 distDir: path.join('dist', `${this.ctx.modeName}-${this.ctx.themeName}`),
341 htmlFilename: 'index.html',
342 webpackManifest: this.ctx.prod,
343 vueRouterMode: 'hash',
344 preloadChunks: true,
345 transpileDependencies: [],
346 devtool: this.ctx.dev
347 ? '#cheap-module-eval-source-map'
348 : '#source-map',
349 env: {
350 NODE_ENV: `"${this.ctx.prod ? 'production' : 'development'}"`,
351 CLIENT: true,
352 SERVER: false,
353 DEV: this.ctx.dev,
354 PROD: this.ctx.prod,
355 THEME: `"${this.ctx.themeName}"`,
356 MODE: `"${this.ctx.modeName}"`
357 },
358 uglifyOptions: {
359 compress: {
360 // turn off flags with small gains to speed up minification
361 arrows: false,
362 collapse_vars: false, // 0.3kb
363 comparisons: false,
364 computed_props: false,
365 hoist_funs: false,
366 hoist_props: false,
367 hoist_vars: false,
368 inline: false,
369 loops: false,
370 negate_iife: false,
371 properties: false,
372 reduce_funcs: false,
373 reduce_vars: false,
374 switches: false,
375 toplevel: false,
376 typeofs: false,
377
378 // a few flags with noticable gains/speed ratio
379 // numbers based on out of the box vendor bundle
380 booleans: true, // 0.7kb
381 if_return: true, // 0.4kb
382 sequences: true, // 0.7kb
383 unused: true, // 2.3kb
384
385 // required features to drop conditional branches
386 conditionals: true,
387 dead_code: true,
388 evaluate: true
389 },
390 mangle: {
391 /*
392 Support non-standard Safari 10/11.
393 By default `uglify-es` will not work around
394 Safari 10/11 bugs.
395 */
396 safari10: true
397 }
398 }
399 }, cfg.build || {})
400
401 cfg.build.transpileDependencies.push(/[\\/]node_modules[\\/]quasar-framework[\\/]/)
402
403 cfg.__loadingBar = cfg.framework.all || (cfg.framework.plugins && cfg.framework.plugins.includes('LoadingBar'))
404 cfg.__meta = cfg.framework.all || (cfg.framework.plugins && cfg.framework.plugins.includes('Meta'))
405
406 if (this.ctx.dev || this.ctx.debug) {
407 Object.assign(cfg.build, {
408 minify: false,
409 extractCSS: false,
410 gzip: false
411 })
412 }
413 if (this.ctx.debug) {
414 cfg.build.sourceMap = true
415 }
416
417 if (this.ctx.mode.ssr) {
418 Object.assign(cfg.build, {
419 extractCSS: false,
420 vueRouterMode: 'history',
421 publicPath: '/',
422 gzip: false
423 })
424 }
425 else if (this.ctx.mode.cordova || this.ctx.mode.electron) {
426 Object.assign(cfg.build, {
427 extractCSS: false,
428 htmlFilename: 'index.html',
429 vueRouterMode: 'hash',
430 gzip: false,
431 webpackManifest: false
432 })
433 }
434
435 if (this.ctx.mode.cordova) {
436 cfg.build.distDir = appPaths.resolve.app(path.join('src-cordova', 'www'))
437 }
438 else if (!path.isAbsolute(cfg.build.distDir)) {
439 cfg.build.distDir = appPaths.resolve.app(cfg.build.distDir)
440 }
441
442 if (this.ctx.mode.electron) {
443 cfg.build.packagedElectronDist = cfg.build.distDir
444 cfg.build.distDir = path.join(cfg.build.distDir, 'UnPackaged')
445 }
446
447 cfg.build.publicPath =
448 this.ctx.prod && cfg.build.publicPath && ['spa', 'pwa'].includes(this.ctx.modeName)
449 ? formatPublicPath(cfg.build.publicPath)
450 : (cfg.build.vueRouterMode !== 'hash' ? '/' : '')
451 cfg.build.appBase = cfg.build.vueRouterMode === 'history'
452 ? cfg.build.publicPath
453 : ''
454
455 cfg.sourceFiles = merge({
456 rootComponent: 'src/App.vue',
457 router: 'src/router/index.js',
458 store: 'src/store/index.js',
459 indexHtmlTemplate: 'src/index.template.html',
460 registerServiceWorker: 'src-pwa/register-service-worker.js',
461 serviceWorker: 'src-pwa/custom-service-worker.js',
462 electronMainDev: 'src-electron/main-process/electron-main.dev.js',
463 electronMainProd: 'src-electron/main-process/electron-main.js',
464 ssrServerIndex: 'src-ssr/index.js'
465 }, cfg.sourceFiles || {})
466
467 // do we got vuex?
468 cfg.store = fs.existsSync(appPaths.resolve.app(cfg.sourceFiles.store))
469
470 //make sure we have preFetch in config
471 cfg.preFetch = cfg.preFetch || false
472
473 if (cfg.animations === 'all') {
474 cfg.animations = require('./helpers/animations')
475 }
476
477 if (this.ctx.mode.ssr) {
478 cfg.ssr = merge({
479 pwa: false,
480 componentCache: {
481 max: 1000,
482 maxAge: 1000 * 60 * 15
483 }
484 }, cfg.ssr || {})
485
486 cfg.ssr.debug = this.ctx.debug
487
488 cfg.ssr.__templateOpts = JSON.stringify(cfg.ssr, null, 2)
489 cfg.ssr.__templateFlags = {
490 meta: cfg.__meta
491 }
492
493 const file = appPaths.resolve.app(cfg.sourceFiles.ssrServerIndex)
494 cfg.ssr.__dir = path.dirname(file)
495 cfg.ssr.__index = path.basename(file)
496
497 if (cfg.ssr.pwa) {
498 require('./mode/install-missing')('pwa')
499 }
500 this.ctx.mode.pwa = cfg.ctx.mode.pwa = cfg.ssr.pwa !== false
501
502 this.watch && (cfg.__ssrExtension = this.ssrExtension)
503 }
504
505 if (this.ctx.dev) {
506 const
507 initialPort = cfg.devServer && cfg.devServer.port,
508 initialHost = cfg.devServer && cfg.devServer.host
509
510 cfg.devServer = merge({
511 publicPath: cfg.build.publicPath,
512 hot: true,
513 inline: true,
514 overlay: true,
515 quiet: true,
516 historyApiFallback: !this.ctx.mode.ssr,
517 noInfo: true,
518 disableHostCheck: true,
519 compress: true,
520 open: true
521 }, cfg.devServer || {}, {
522 contentBase: [ appPaths.srcDir ]
523 })
524
525 if (this.ctx.mode.ssr) {
526 cfg.devServer.contentBase = false
527 }
528 else if (this.ctx.mode.cordova || this.ctx.mode.electron) {
529 cfg.devServer.open = false
530
531 if (this.ctx.mode.electron) {
532 cfg.devServer.https = false
533 }
534 }
535
536 if (this.ctx.mode.cordova) {
537 cfg.devServer.contentBase.push(
538 appPaths.resolve.cordova(`platforms/${this.ctx.targetName}/platform_www`)
539 )
540 }
541
542 if (cfg.devServer.open) {
543 const isMinimalTerminal = require('./helpers/is-minimal-terminal')
544 if (isMinimalTerminal) {
545 cfg.devServer.open = false
546 }
547 }
548 }
549
550 if (cfg.build.gzip) {
551 let gzip = cfg.build.gzip === true
552 ? {}
553 : cfg.build.gzip
554 let ext = ['js', 'css']
555
556 if (gzip.extensions) {
557 ext = gzip.extensions
558 delete gzip.extensions
559 }
560
561 cfg.build.gzip = merge({
562 filename: '[path].gz[query]',
563 algorithm: 'gzip',
564 test: new RegExp('\\.(' + ext.join('|') + ')$'),
565 threshold: 10240,
566 minRatio: 0.8
567 }, gzip)
568 }
569
570 if (this.ctx.mode.pwa) {
571 cfg.build.webpackManifest = false
572
573 cfg.pwa = merge({
574 workboxPluginMode: 'GenerateSW',
575 workboxOptions: {},
576 manifest: {
577 name: this.pkg.productName || this.pkg.name || 'Quasar App',
578 short_name: this.pkg.name || 'quasar-pwa',
579 description: this.pkg.description,
580 display: 'standalone',
581 start_url: '.'
582 }
583 }, cfg.pwa || {})
584
585 if (!['GenerateSW', 'InjectManifest'].includes(cfg.pwa.workboxPluginMode)) {
586 console.log()
587 console.log(`⚠️ Workbox webpack plugin mode "${cfg.pwa.workboxPluginMode}" is invalid.`)
588 console.log(` Valid Workbox modes are: GenerateSW, InjectManifest`)
589 console.log()
590 process.exit(1)
591 }
592 if (cfg.pwa.cacheExt) {
593 console.log()
594 console.log(`⚠️ Quasar CLI now uses Workbox, so quasar.conf.js > pwa > cacheExt is no longer relevant.`)
595 console.log(` Please remove this property and try again.`)
596 console.log()
597 process.exit(1)
598 }
599 if (
600 fs.existsSync(appPaths.resolve.pwa('service-worker-dev.js')) ||
601 fs.existsSync(appPaths.resolve.pwa('service-worker-prod.js'))
602 ) {
603 console.log()
604 console.log(`⚠️ Quasar CLI now uses Workbox, so src-pwa/service-worker-dev.js and src-pwa/service-worker-prod.js are obsolete.`)
605 console.log(` Please remove and add PWA mode again:`)
606 console.log(` $ quasar mode -r pwa # Warning: this will delete /src-pwa !`)
607 console.log(` $ quasar mode -a pwa`)
608 console.log()
609 process.exit(1)
610 }
611
612 cfg.pwa.manifest.icons = cfg.pwa.manifest.icons.map(icon => {
613 icon.src = `${cfg.build.publicPath}${icon.src}`
614 return icon
615 })
616 }
617
618 if (this.ctx.dev) {
619 const host = cfg.devServer.host === '0.0.0.0'
620 ? 'localhost'
621 : cfg.devServer.host
622 const urlPath = `${cfg.build.vueRouterMode === 'hash' ? (cfg.build.htmlFilename !== 'index.html' ? cfg.build.htmlFilename : '') : ''}`
623 cfg.build.APP_URL = `http${cfg.devServer.https ? 's' : ''}://${host}:${cfg.devServer.port}/${urlPath}`
624 }
625 else if (this.ctx.mode.cordova) {
626 cfg.build.APP_URL = 'index.html'
627 }
628 else if (this.ctx.mode.electron) {
629 cfg.build.APP_URL = `file://" + __dirname + "/index.html`
630 }
631
632 cfg.build.env = merge(cfg.build.env || {}, {
633 VUE_ROUTER_MODE: `"${cfg.build.vueRouterMode}"`,
634 VUE_ROUTER_BASE: `"${cfg.build.publicPath}"`,
635 APP_URL: `"${cfg.build.APP_URL}"`
636 })
637
638 if (this.ctx.mode.pwa) {
639 cfg.build.env.SERVICE_WORKER_FILE = `"${cfg.build.publicPath}service-worker.js"`
640 }
641
642 cfg.build.env = {
643 'process.env': cfg.build.env
644 }
645
646 if (this.ctx.mode.electron) {
647 if (this.ctx.dev) {
648 cfg.build.env.__statics = `"${appPaths.resolve.src('statics').replace(/\\/g, '\\\\')}"`
649 }
650 }
651 else {
652 cfg.build.env.__statics = `"${this.ctx.dev ? '/' : cfg.build.publicPath || '/'}statics"`
653 }
654
655 legacyValidations(cfg)
656
657 if (this.ctx.mode.cordova && !cfg.cordova) {
658 cfg.cordova = {}
659 }
660
661 if (this.ctx.mode.electron) {
662 if (this.ctx.prod) {
663 const bundler = require('./electron/bundler')
664
665 cfg.electron = merge({
666 packager: {
667 asar: true,
668 icon: appPaths.resolve.electron('icons/icon'),
669 overwrite: true
670 },
671 builder: {
672 appId: 'quasar-app',
673 productName: this.pkg.productName || this.pkg.name || 'Quasar App',
674 directories: {
675 buildResources: appPaths.resolve.electron('')
676 }
677 }
678 }, cfg.electron || {}, {
679 packager: {
680 dir: cfg.build.distDir,
681 out: cfg.build.packagedElectronDist
682 },
683 builder: {
684 directories: {
685 app: cfg.build.distDir,
686 output: path.join(cfg.build.packagedElectronDist, 'Packaged')
687 }
688 }
689 })
690
691 if (cfg.ctx.bundlerName) {
692 cfg.electron.bundler = cfg.ctx.bundlerName
693 }
694 else if (!cfg.electron.bundler) {
695 cfg.electron.bundler = bundler.getDefaultName()
696 }
697
698 if (cfg.electron.bundler === 'packager') {
699 if (cfg.ctx.targetName) {
700 cfg.electron.packager.platform = cfg.ctx.targetName
701 }
702 if (cfg.ctx.archName) {
703 cfg.electron.packager.arch = cfg.ctx.archName
704 }
705 }
706 else {
707 cfg.electron.builder = {
708 platform: cfg.ctx.targetName,
709 arch: cfg.ctx.archName,
710 config: cfg.electron.builder
711 }
712
713 bundler.ensureBuilderCompatibility()
714 }
715
716 bundler.ensureInstall(cfg.electron.bundler)
717 }
718 }
719
720 cfg.__html = {
721 variables: Object.assign({
722 ctx: cfg.ctx,
723 process: {
724 env: parseBuildEnv(cfg.build.env['process.env'])
725 },
726 productName: cfg.build.productName,
727 productDescription: cfg.build.productDescription
728 }, cfg.htmlVariables || {}),
729 minifyOptions: cfg.build.minify
730 ? {
731 removeComments: true,
732 collapseWhitespace: true,
733 removeAttributeQuotes: true
734 // more options:
735 // https://github.com/kangax/html-minifier#options-quick-reference
736 }
737 : undefined
738 }
739
740 this.webpackConfig = require('./webpack')(cfg)
741 this.buildConfig = cfg
742 }
743}
744
745module.exports = QuasarConfig