├── .npmignore ├── templates ├── app │ ├── store │ │ ├── state.js │ │ ├── actions.js │ │ ├── getters.js │ │ ├── mutations.js │ │ └── index.js │ ├── plugin.js │ ├── page.vue │ ├── component.vue │ ├── app.styl │ ├── babelrc │ ├── variables.styl │ └── layout.vue ├── electron │ ├── icons │ │ ├── icon.ico │ │ ├── icon.icns │ │ └── linux-512x512.png │ └── main-process │ │ ├── electron-main.dev.js │ │ └── electron-main.js ├── pwa │ ├── custom-service-worker.js │ └── register-service-worker.js ├── ssr │ ├── extension.js │ └── index.js └── entry │ ├── import-quasar.js │ ├── app.js │ ├── client-prefetch.js │ ├── server-entry.js │ └── client-entry.js ├── .gitignore ├── assets └── logo.art ├── lib ├── helpers │ ├── is-minimal-terminal.js │ ├── animations.js │ ├── on-shutdown.js │ ├── logger.js │ ├── cli-error-handling.js │ ├── get-external-ip.js │ ├── node-packager.js │ ├── ensure-deps.js │ ├── spawn.js │ ├── net.js │ ├── ensure-argv.js │ └── banner.js ├── webpack │ ├── cordova │ │ └── index.js │ ├── inject.hot-update.js │ ├── spa │ │ └── index.js │ ├── pwa │ │ ├── plugin.pwa-manifest.js │ │ ├── plugin.html-pwa.js │ │ └── index.js │ ├── ssr │ │ ├── client.js │ │ ├── server.js │ │ ├── template.ssr.js │ │ └── plugin.ssr-prod-artifacts.js │ ├── inject.preload.js │ ├── inject.client-specifics.js │ ├── electron │ │ ├── plugin.electron-package-json.js │ │ ├── renderer.js │ │ └── main.js │ ├── inject.html.js │ ├── plugin.html-addons.js │ ├── inject.style-rules.js │ ├── plugin.progress.js │ ├── index.js │ └── create-chain.js ├── mode │ ├── index.js │ ├── install-missing.js │ ├── mode-pwa.js │ ├── mode-ssr.js │ ├── mode-electron.js │ └── mode-cordova.js ├── node-version-check.js ├── app-paths.js ├── artifacts.js ├── cordova │ ├── index.js │ └── cordova-config.js ├── ssr │ └── html-template.js ├── generator.js ├── electron │ ├── bundler.js │ └── index.js ├── legacy-validations.js ├── dev-server.js └── quasar-config.js ├── .editorconfig ├── bin ├── quasar-clean ├── quasar-help ├── quasar ├── quasar-mode ├── quasar-init ├── quasar-info ├── quasar-new ├── quasar-build ├── quasar-dev └── quasar-serve ├── lists └── app-templates.json ├── LICENSE ├── .github ├── CODE_OF_CONDUCT.md ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .github 3 | -------------------------------------------------------------------------------- /templates/app/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /templates/app/store/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | export function someAction (context) { 3 | } 4 | */ 5 | -------------------------------------------------------------------------------- /templates/app/store/getters.js: -------------------------------------------------------------------------------- 1 | /* 2 | export function someGetter (state) { 3 | } 4 | */ 5 | -------------------------------------------------------------------------------- /templates/app/store/mutations.js: -------------------------------------------------------------------------------- 1 | /* 2 | export function someMutation (state) { 3 | } 4 | */ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Thumbs.db 3 | node_modules/ 4 | ssl-server.pem 5 | npm-debug.log 6 | *.sublime* 7 | -------------------------------------------------------------------------------- /templates/electron/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarframework/quasar-cli/HEAD/templates/electron/icons/icon.ico -------------------------------------------------------------------------------- /templates/electron/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarframework/quasar-cli/HEAD/templates/electron/icons/icon.icns -------------------------------------------------------------------------------- /templates/electron/icons/linux-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarframework/quasar-cli/HEAD/templates/electron/icons/linux-512x512.png -------------------------------------------------------------------------------- /templates/app/plugin.js: -------------------------------------------------------------------------------- 1 | // import something here 2 | 3 | // leave the export, even if you don't use it 4 | export default ({ app, router, Vue }) => { 5 | // something to do 6 | } 7 | -------------------------------------------------------------------------------- /assets/logo.art: -------------------------------------------------------------------------------- 1 | ___ 2 | / _ \ _ _ __ _ ___ __ _ _ __ 3 | | | | | | | |/ _` / __|/ _` | '__| 4 | | |_| | |_| | (_| \__ \ (_| | | 5 | \__\_\\__,_|\__,_|___/\__,_|_| 6 | 7 | -------------------------------------------------------------------------------- /templates/pwa/custom-service-worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file (which will be your service worker) 3 | * is picked up by the build system ONLY if 4 | * quasar.conf > pwa > workboxPluginMode is set to "InjectManifest" 5 | */ 6 | -------------------------------------------------------------------------------- /lib/helpers/is-minimal-terminal.js: -------------------------------------------------------------------------------- 1 | const ci = require('ci-info') 2 | 3 | const isMinimal = ( 4 | ci.isCI || 5 | process.env.NODE_ENV === 'test' || 6 | !process.stdout.isTTY 7 | ) 8 | 9 | module.exports = isMinimal 10 | -------------------------------------------------------------------------------- /templates/app/page.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /lib/helpers/animations.js: -------------------------------------------------------------------------------- 1 | const { 2 | generalAnimations, 3 | inAnimations, 4 | outAnimations 5 | } = require('quasar-extras/animate/animate-list.common') 6 | 7 | module.exports = generalAnimations.concat(inAnimations).concat(outAnimations) 8 | -------------------------------------------------------------------------------- /templates/app/component.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /templates/app/store/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | } 13 | -------------------------------------------------------------------------------- /lib/webpack/cordova/index.js: -------------------------------------------------------------------------------- 1 | const 2 | injectHtml = require('../inject.html'), 3 | injectClientSpecifics = require('../inject.client-specifics'), 4 | injectHotUpdate = require('../inject.hot-update') 5 | 6 | module.exports = function (chain, cfg) { 7 | injectHtml(chain, cfg) 8 | injectClientSpecifics(chain, cfg) 9 | injectHotUpdate(chain, cfg) 10 | } 11 | -------------------------------------------------------------------------------- /lib/mode/index.js: -------------------------------------------------------------------------------- 1 | const warn = require('../helpers/logger')('app:quasar-mode', 'red') 2 | 3 | module.exports = function (mode) { 4 | if (!['pwa', 'cordova', 'electron', 'ssr'].includes(mode)) { 5 | warn(`⚠️ Unknown mode specified: ${mode}`) 6 | process.exit(1) 7 | } 8 | 9 | const QuasarMode = require(`./mode-${mode}`) 10 | return new QuasarMode() 11 | } 12 | -------------------------------------------------------------------------------- /lib/webpack/inject.hot-update.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = function (chain, cfg) { 4 | if (cfg.ctx.dev && cfg.devServer.hot) { 5 | chain.optimization 6 | .namedModules(true) // HMR shows filenames in console on update 7 | 8 | chain.plugin('hot-module-replacement') 9 | .use(webpack.HotModuleReplacementPlugin) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/app/app.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS FILE IS GENERATED AUTOMATICALLY. 3 | * DO NOT EDIT. 4 | * 5 | * Edit /src/css/themes instead 6 | **/ 7 | 8 | // First we load app's Stylus variables 9 | @import '~quasar-app-variables' 10 | 11 | // Then we load Quasar stylus files 12 | @import '~quasar-styl' 13 | 14 | // We add Quasar addons, if they were requested 15 | @import '~quasar-addon-styl' 16 | -------------------------------------------------------------------------------- /lib/webpack/spa/index.js: -------------------------------------------------------------------------------- 1 | const 2 | injectHtml = require('../inject.html'), 3 | injectClientSpecifics = require('../inject.client-specifics'), 4 | injectHotUpdate = require('../inject.hot-update'), 5 | injectPreload = require('../inject.preload') 6 | 7 | module.exports = function (chain, cfg) { 8 | injectHtml(chain, cfg) 9 | injectClientSpecifics(chain, cfg) 10 | injectHotUpdate(chain, cfg) 11 | injectPreload(chain, cfg) 12 | } 13 | -------------------------------------------------------------------------------- /lib/helpers/on-shutdown.js: -------------------------------------------------------------------------------- 1 | const log = require('./logger')('app:on-shutdown') 2 | 3 | module.exports = function (fn, msg) { 4 | const cleanup = () => { 5 | try { 6 | msg && log(msg) 7 | fn() 8 | } 9 | finally { 10 | process.exit() 11 | } 12 | } 13 | 14 | process.on('exit', cleanup) 15 | process.on('SIGINT', cleanup) 16 | process.on('SIGTERM', cleanup) 17 | process.on('SIGHUP', cleanup) 18 | process.on('SIGBREAK', cleanup) 19 | } 20 | -------------------------------------------------------------------------------- /lib/webpack/pwa/plugin.pwa-manifest.js: -------------------------------------------------------------------------------- 1 | module.exports = class PwaManifest { 2 | constructor (cfg = {}) { 3 | this.manifest = JSON.stringify(cfg.pwa.manifest) 4 | } 5 | 6 | apply (compiler) { 7 | compiler.hooks.emit.tapAsync('manifest.json', (compiler, callback) => { 8 | compiler.assets['manifest.json'] = { 9 | source: () => new Buffer(this.manifest), 10 | size: () => Buffer.byteLength(this.manifest) 11 | } 12 | 13 | callback() 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/webpack/ssr/client.js: -------------------------------------------------------------------------------- 1 | const 2 | injectClientSpecifics = require('../inject.client-specifics'), 3 | injectHotUpdate = require('../inject.hot-update'), 4 | VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 5 | 6 | module.exports = function (chain, cfg) { 7 | injectClientSpecifics(chain, cfg) 8 | injectHotUpdate(chain, cfg) 9 | 10 | chain.plugin('vue-ssr-client') 11 | .use(VueSSRClientPlugin, [{ 12 | filename: '../vue-ssr-client-manifest.json' 13 | }]) 14 | } 15 | -------------------------------------------------------------------------------- /lib/helpers/logger.js: -------------------------------------------------------------------------------- 1 | const 2 | ms = require('ms'), 3 | chalk = require('chalk') 4 | 5 | let prevTime 6 | 7 | module.exports = function (banner, color = 'green') { 8 | return function (msg) { 9 | const 10 | curr = +new Date(), 11 | diff = curr - (prevTime || curr) 12 | 13 | prevTime = curr 14 | 15 | if (msg) { 16 | console.log(` ${chalk[color](banner)} ${msg} ${chalk.green(`+${ms(diff)}`)}`) 17 | } 18 | else { 19 | console.log() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/mode/install-missing.js: -------------------------------------------------------------------------------- 1 | const 2 | logger = require('../helpers/logger'), 3 | log = logger('app:mode'), 4 | warn = logger('app:mode', 'red'), 5 | getMode = require('./index') 6 | 7 | module.exports = function (mode, target) { 8 | const Mode = getMode(mode) 9 | 10 | if (Mode.isInstalled) { 11 | if (mode === 'cordova') { 12 | Mode.addPlatform(target) 13 | } 14 | return 15 | } 16 | 17 | warn(`Quasar ${mode.toUpperCase()} is missing. Installing it...`) 18 | Mode.add(target) 19 | } 20 | -------------------------------------------------------------------------------- /lib/webpack/inject.preload.js: -------------------------------------------------------------------------------- 1 | module.exports = function (chain, cfg) { 2 | if (cfg.ctx.prod && cfg.build.preloadChunks) { 3 | const PreloadPlugin = require('preload-webpack-plugin') 4 | 5 | chain.plugin('preload') 6 | .use(PreloadPlugin, [{ 7 | rel: 'preload', 8 | include: 'initial', 9 | fileBlacklist: [/\.map$/, /hot-update\.js$/] 10 | }]) 11 | chain.plugin('prefetch') 12 | .use(PreloadPlugin, [{ 13 | rel: 'prefetch', 14 | include: 'asyncChunks' 15 | }]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/helpers/cli-error-handling.js: -------------------------------------------------------------------------------- 1 | const pe = require('pretty-error').start() 2 | 3 | pe.skipPackage('regenerator-runtime') 4 | pe.skipPackage('babel-runtime') 5 | pe.skipNodeFiles() 6 | 7 | let ouchInstance 8 | 9 | module.exports.getOuchInstance = function () { 10 | if (ouchInstance) { 11 | return ouchInstance 12 | } 13 | 14 | pe.stop() 15 | 16 | const Ouch = require('ouch') 17 | ouchInstance = (new Ouch()).pushHandler( 18 | new Ouch.handlers.PrettyPageHandler('orange', null, 'sublime') 19 | ) 20 | 21 | return ouchInstance 22 | } 23 | -------------------------------------------------------------------------------- /lib/node-version-check.js: -------------------------------------------------------------------------------- 1 | const 2 | version = process.version.split('.'), 3 | major = parseInt(version[0].replace(/\D/g,''), 10) 4 | minor = parseInt(version[1].replace(/\D/g,''), 10) 5 | 6 | if (major < 8 || (major === 8 && minor < 9)) { 7 | console.warn('\x1b[41m%s\x1b[0m', 'INCOMPATIBLE NODE VERSION') 8 | console.warn('\x1b[33m%s\x1b[0m', 'Quasar CLI requires Node 8.9.0 or superior') 9 | console.warn('') 10 | console.warn('⚠️ You are running Node ' + version) 11 | console.warn('Please install a compatible Node version and try again') 12 | 13 | process.exit(1) 14 | } 15 | -------------------------------------------------------------------------------- /templates/app/babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "modules": false, 6 | "loose": false, 7 | "useBuiltIns": "usage" 8 | } 9 | ], 10 | [ 11 | "@babel/preset-stage-2", { 12 | "modules": false, 13 | "loose": false, 14 | "useBuiltIns": true, 15 | "decoratorsLegacy": true 16 | } 17 | ] 18 | ], 19 | "plugins": [ 20 | [ 21 | "@babel/transform-runtime", { 22 | "polyfill": false, 23 | "regenerator": false 24 | } 25 | ] 26 | ], 27 | "comments": false 28 | } 29 | -------------------------------------------------------------------------------- /lib/webpack/inject.client-specifics.js: -------------------------------------------------------------------------------- 1 | module.exports = function (chain) { 2 | chain.node 3 | .merge({ 4 | // prevent webpack from injecting useless setImmediate polyfill because Vue 5 | // source contains it (although only uses it if it's native). 6 | setImmediate: false, 7 | // process is injected via DefinePlugin, although some 3rd party 8 | // libraries may require a mock to work properly (#934) 9 | process: 'mock', 10 | // prevent webpack from injecting mocks to Node native modules 11 | // that does not make sense for the client 12 | dgram: 'empty', 13 | fs: 'empty', 14 | net: 'empty', 15 | tls: 'empty', 16 | child_process: 'empty' 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /lib/webpack/electron/plugin.electron-package-json.js: -------------------------------------------------------------------------------- 1 | const 2 | appPaths = require('../../app-paths') 3 | 4 | module.exports = class ElectronPackageJson { 5 | apply (compiler) { 6 | compiler.hooks.emit.tapAsync('package.json', (compiler, callback) => { 7 | const pkg = require(appPaths.resolve.app('package.json')) 8 | 9 | // we don't need this (also, faster install time & smaller bundles) 10 | delete pkg.devDependencies 11 | 12 | pkg.main = './electron-main.js' 13 | const source = JSON.stringify(pkg) 14 | 15 | compiler.assets['package.json'] = { 16 | source: () => new Buffer(source), 17 | size: () => Buffer.byteLength(source) 18 | } 19 | 20 | callback() 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bin/quasar-clean: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | parseArgs = require('minimist'), 5 | path = require('path'), 6 | chalk = require('chalk') 7 | 8 | const 9 | appPaths = require('../lib/app-paths'), 10 | log = require('../lib/helpers/logger')('app:clean') 11 | 12 | const argv = parseArgs(process.argv.slice(2), { 13 | alias: { 14 | h: 'help' 15 | }, 16 | boolean: ['h'] 17 | }) 18 | 19 | if (argv.help) { 20 | console.log(` 21 | Description 22 | Cleans all build artifacts 23 | Usage 24 | $ quasar clean 25 | Options 26 | --help, -h Displays this message 27 | `) 28 | process.exit(0) 29 | } 30 | 31 | require('../lib/artifacts').cleanAll() 32 | 33 | console.log() 34 | log(`Done cleaning build artifacts`) 35 | log() 36 | -------------------------------------------------------------------------------- /templates/electron/main-process/electron-main.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | // Install `electron-debug` with `devtron` 9 | require('electron-debug')({ showDevTools: true }) 10 | 11 | // Install `vue-devtools` 12 | require('electron').app.on('ready', () => { 13 | let installExtension = require('electron-devtools-installer') 14 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 15 | .then(() => {}) 16 | .catch(err => { 17 | console.log('Unable to install `vue-devtools`: \n', err) 18 | }) 19 | }) 20 | 21 | // Require `main` process to boot app 22 | require('./electron-main') 23 | -------------------------------------------------------------------------------- /lib/helpers/get-external-ip.js: -------------------------------------------------------------------------------- 1 | const warn = require('./logger')('app:external-ip') 2 | 3 | module.exports = async function () { 4 | const interfaces = await require('./net').getExternalNetworkInterface() 5 | 6 | if (interfaces.length === 0) { 7 | warn(`⚠️ No external IP detected. Can't run without one. Manually specify one?`) 8 | warn() 9 | process.exit(1) 10 | } 11 | 12 | if (interfaces.length === 1) { 13 | const address = interfaces[0].address 14 | warn(`Detected external IP ${address} and using it`) 15 | return address 16 | } 17 | 18 | const answer = await require('inquirer').prompt([{ 19 | type: 'list', 20 | name: 'address', 21 | message: 'What external IP should Quasar use?', 22 | choices: interfaces.map(interface => interface.address) 23 | }]) 24 | 25 | return answer.address 26 | } 27 | -------------------------------------------------------------------------------- /lib/webpack/electron/renderer.js: -------------------------------------------------------------------------------- 1 | const 2 | appPaths = require('../../app-paths'), 3 | injectHtml = require('../inject.html'), 4 | injectClientSpecifics = require('../inject.client-specifics'), 5 | injectHotUpdate = require('../inject.hot-update') 6 | 7 | module.exports = function (chain, cfg) { 8 | if (cfg.ctx.build) { 9 | chain.output 10 | .libraryTarget('commonjs2') 11 | } 12 | 13 | chain.node 14 | .merge({ 15 | __dirname: cfg.ctx.dev, 16 | __filename: cfg.ctx.dev 17 | }) 18 | 19 | chain.resolve.extensions 20 | .add('.node') 21 | 22 | chain.target('electron-renderer') 23 | 24 | chain.module.rule('node') 25 | .test(/\.node$/) 26 | .use('node-loader') 27 | .loader('node-loader') 28 | 29 | injectHtml(chain, cfg) 30 | injectClientSpecifics(chain, cfg) 31 | injectHotUpdate(chain, cfg) 32 | } 33 | -------------------------------------------------------------------------------- /templates/app/variables.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS FILE IS GENERATED AUTOMATICALLY. 3 | * DO NOT EDIT. 4 | * 5 | * Edit /src/css/themes instead 6 | **/ 7 | 8 | // Webpack alias "variables" points to this file. 9 | // So you can import it in your app's *.vue files 10 | // inside the 18 | 19 | // First we load app's Stylus variables 20 | @import '~quasar-app-variables' 21 | 22 | // Then we load Quasar Stylus variables. 23 | // Any variables defined in "app.variables.styl" 24 | // will override Quasar's ones. 25 | // 26 | // NOTICE that we only import Core Quasar Variables 27 | // like colors, media breakpoints, and so. 28 | // No component variable will be included. 29 | @import '~quasar-framework/src/css/core.variables' 30 | -------------------------------------------------------------------------------- /lib/webpack/inject.html.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const 4 | appPaths = require('../app-paths'), 5 | HtmlWebpackPlugin = require('html-webpack-plugin'), 6 | HtmlAddonsPlugin = require('./plugin.html-addons').plugin 7 | 8 | module.exports = function (chain, cfg) { 9 | chain.plugin('html-webpack') 10 | .use(HtmlWebpackPlugin, [ 11 | Object.assign({}, cfg.__html.variables, { 12 | filename: cfg.ctx.dev 13 | ? 'index.html' 14 | : path.join(cfg.build.distDir, cfg.build.htmlFilename), 15 | template: appPaths.resolve.app(cfg.sourceFiles.indexHtmlTemplate), 16 | minify: cfg.__html.minifyOptions, 17 | 18 | chunksSortMode: 'none', 19 | // inject script tags for bundle 20 | inject: true, 21 | cache: true 22 | }) 23 | ]) 24 | 25 | chain.plugin('html-addons') 26 | .use(HtmlAddonsPlugin, [ cfg ]) 27 | } 28 | -------------------------------------------------------------------------------- /templates/ssr/extension.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | * 5 | * All content of this folder will be copied as is to the output folder. So only import: 6 | * 1. node_modules (and yarn/npm install dependencies -- NOT to devDependecies though) 7 | * 2. create files in this folder and import only those with the relative path 8 | * 9 | * Note: This file is used for both PRODUCTION & DEVELOPMENT. 10 | * Note: Changes to this file (but not any file it imports!) are picked up by the 11 | * development server, but such updates are costly since the dev-server needs a reboot. 12 | */ 13 | 14 | module.exports.extendApp = function ({ app }) { 15 | /* 16 | Extend the parts of the express app that you 17 | want to use with development server too. 18 | 19 | Example: app.use(), app.get() etc 20 | */ 21 | } 22 | -------------------------------------------------------------------------------- /lib/mode/mode-pwa.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | fse = require('fs-extra'), 4 | appPaths = require('../app-paths'), 5 | logger = require('../helpers/logger'), 6 | log = logger('app:mode-pwa'), 7 | warn = logger('app:mode-pwa', 'red') 8 | 9 | class Mode { 10 | get isInstalled () { 11 | return fs.existsSync(appPaths.pwaDir) 12 | } 13 | 14 | add (params) { 15 | if (this.isInstalled) { 16 | warn(`PWA support detected already. Aborting.`) 17 | return 18 | } 19 | 20 | log(`Creating PWA source folder...`) 21 | fse.copySync(appPaths.resolve.cli('templates/pwa'), appPaths.pwaDir) 22 | log(`PWA support was added`) 23 | } 24 | 25 | remove () { 26 | if (!this.isInstalled) { 27 | warn(`No PWA support detected. Aborting.`) 28 | return 29 | } 30 | 31 | log(`Removing PWA source folder`) 32 | fse.removeSync(appPaths.pwaDir) 33 | log(`PWA support was removed`) 34 | } 35 | } 36 | 37 | module.exports = Mode 38 | -------------------------------------------------------------------------------- /lib/mode/mode-ssr.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | fse = require('fs-extra'), 4 | appPaths = require('../app-paths'), 5 | logger = require('../helpers/logger'), 6 | log = logger('app:mode-ssr'), 7 | warn = logger('app:mode-ssr', 'red') 8 | 9 | class Mode { 10 | get isInstalled () { 11 | return fs.existsSync(appPaths.ssrDir) 12 | } 13 | 14 | add (params) { 15 | if (this.isInstalled) { 16 | warn(`SSR support detected already. Aborting.`) 17 | return 18 | } 19 | 20 | log(`Creating SSR source folder...`) 21 | fse.copySync(appPaths.resolve.cli('templates/ssr'), appPaths.ssrDir) 22 | log(`SSR support was added`) 23 | } 24 | 25 | remove () { 26 | if (!this.isInstalled) { 27 | warn(`No SSR support detected. Aborting.`) 28 | return 29 | } 30 | 31 | log(`Removing SSR source folder`) 32 | fse.removeSync(appPaths.ssrDir) 33 | log(`SSR support was removed`) 34 | } 35 | } 36 | 37 | module.exports = Mode 38 | -------------------------------------------------------------------------------- /bin/quasar-help: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log() 4 | console.log( 5 | require('fs').readFileSync( 6 | require('path').join(__dirname, '../assets/logo.art'), 7 | 'utf8' 8 | ) 9 | ) 10 | 11 | console.log(` 12 | Example usage 13 | $ quasar 14 | 15 | Help for a command 16 | $ quasar --help 17 | $ quasar -h 18 | 19 | Options 20 | --version, -v Print Quasar CLI version 21 | 22 | Commands 23 | init Create a project folder 24 | dev Start a dev server for your App 25 | build Build your app for production 26 | clean Clean all build artifacts 27 | new Quickly scaffold page/layout/component/... vue file 28 | mode Add/remove Quasar Modes for your App 29 | info Display info about your machine and your App 30 | serve Create an ad-hoc server on App's distributables 31 | help Displays this message 32 | `) 33 | -------------------------------------------------------------------------------- /lists/app-templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest": "default", 3 | "official": [ 4 | { 5 | "name": "default", 6 | "description": "Boilerplate for website (with optional Cordova and Electron wrappers) with Quasar v0.14+" 7 | }, 8 | { 9 | "name": "pwa", 10 | "description": "Boilerplate for PWA with Quasar v0.14+" 11 | }, 12 | { 13 | "name": "default#v0.13", 14 | "description": "Boilerplate for Quasar v0.13", 15 | "deprecated": true 16 | }, 17 | { 18 | "name": "default#webpack1", 19 | "description": "Boilerplate for Quasar v0.8-v0.12 (Webpack 1, Vue 2)", 20 | "deprecated": true 21 | }, 22 | { 23 | "name": "default#vue1", 24 | "description": "Boilerplate for Quasar <= 0.7 (Webpack 1, Vue 1)", 25 | "deprecated": true 26 | } 27 | ], 28 | "community": [ 29 | { 30 | "name": "meteor", 31 | "description": "Boilerplate for Quasar v0.9+ with Meteor" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /templates/electron/main-process/electron-main.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | 3 | /** 4 | * Set `__statics` path to static files in production; 5 | * The reason we are setting it here is that the path needs to be evaluated at runtime 6 | */ 7 | if (process.env.PROD) { 8 | global.__statics = require('path').join(__dirname, 'statics').replace(/\\/g, '\\\\') 9 | } 10 | 11 | let mainWindow 12 | 13 | function createWindow () { 14 | /** 15 | * Initial window options 16 | */ 17 | mainWindow = new BrowserWindow({ 18 | width: 1000, 19 | height: 600, 20 | useContentSize: true 21 | }) 22 | 23 | mainWindow.loadURL(process.env.APP_URL) 24 | 25 | mainWindow.on('closed', () => { 26 | mainWindow = null 27 | }) 28 | } 29 | 30 | app.on('ready', createWindow) 31 | 32 | app.on('window-all-closed', () => { 33 | if (process.platform !== 'darwin') { 34 | app.quit() 35 | } 36 | }) 37 | 38 | app.on('activate', () => { 39 | if (mainWindow === null) { 40 | createWindow() 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /lib/helpers/node-packager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const 4 | appPaths = require('../app-paths'), 5 | spawn = require('cross-spawn').sync, 6 | warn = require('./logger')('app:node-packager', 'red') 7 | 8 | function isInstalled (cmd) { 9 | try { 10 | return spawn(cmd, ['--version']).status === 0 11 | } 12 | catch (err) { 13 | return false 14 | } 15 | } 16 | 17 | function getPackager () { 18 | if (!fs.existsSync(appPaths.resolve.app('node_modules'))) { 19 | warn('⚠️ Please run "yarn" / "npm install" first') 20 | warn() 21 | process.exit(1) 22 | } 23 | 24 | if (fs.existsSync(appPaths.resolve.app('yarn.lock'))) { 25 | return 'yarn' 26 | } 27 | 28 | if (fs.existsSync(appPaths.resolve.app('package-lock.json'))) { 29 | return 'npm' 30 | } 31 | 32 | if (isInstalled('yarn')) { 33 | return 'yarn' 34 | } 35 | 36 | if (isInstalled('npm')) { 37 | return 'npm' 38 | } 39 | 40 | warn('⚠️ Please install Yarn or NPM before running this command.') 41 | warn() 42 | } 43 | 44 | module.exports = getPackager() 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Razvan Stoenescu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/helpers/ensure-deps.js: -------------------------------------------------------------------------------- 1 | const 2 | appPaths = require('../app-paths'), 3 | logger = require('../helpers/logger'), 4 | log = logger('app:ensure-dev-deps'), 5 | warn = logger('app:ensure-dev-deps', 'red'), 6 | spawn = require('./spawn'), 7 | nodePackager = require('./node-packager') 8 | 9 | function needsStripAnsi (pkg) { 10 | if (pkg.devDependencies && pkg.devDependencies['strip-ansi'] && pkg.devDependencies['strip-ansi'].indexOf('3.0.1') > -1) { 11 | return false 12 | } 13 | if (pkg.dependencies && pkg.dependencies['strip-ansi'] && pkg.dependencies['strip-ansi'].indexOf('3.0.1') > -1) { 14 | return false 15 | } 16 | 17 | return true 18 | } 19 | 20 | module.exports = function () { 21 | const pkg = require(appPaths.resolve.app('package.json')) 22 | 23 | if (needsStripAnsi(pkg)) { 24 | const cmdParam = nodePackager === 'npm' 25 | ? ['install', '--save-dev'] 26 | : ['add', '--dev'] 27 | 28 | cmdParam.push('strip-ansi@=3.0.1') 29 | 30 | log(`Pinning strip-ansi dependency...`) 31 | spawn.sync( 32 | nodePackager, 33 | cmdParam, 34 | appPaths.appDir, 35 | () => warn('Failed to install strip-ansi dependency') 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/pwa/register-service-worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is picked up by the build system only 3 | * when building for PRODUCTION 4 | */ 5 | 6 | import { register } from 'register-service-worker' 7 | 8 | register(process.env.SERVICE_WORKER_FILE, { 9 | ready () { 10 | console.log('App is being served from cache by a service worker.') 11 | }, 12 | registered (registration) { // registration -> a ServiceWorkerRegistration instance 13 | console.log('Service worker has been registered.') 14 | }, 15 | cached (registration) { // registration -> a ServiceWorkerRegistration instance 16 | console.log('Content has been cached for offline use.') 17 | }, 18 | updatefound (registration) { // registration -> a ServiceWorkerRegistration instance 19 | console.log('New content is downloading.') 20 | }, 21 | updated (registration) { // registration -> a ServiceWorkerRegistration instance 22 | console.log('New content is available; please refresh.') 23 | }, 24 | offline () { 25 | console.log('No internet connection found. App is running in offline mode.') 26 | }, 27 | error (err) { 28 | console.error('Error during service worker registration:', err) 29 | } 30 | }) 31 | 32 | // ServiceWorkerRegistration: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 33 | -------------------------------------------------------------------------------- /lib/helpers/spawn.js: -------------------------------------------------------------------------------- 1 | const 2 | logger = require('./logger'), 3 | log = logger('app:spawn'), 4 | warn = logger('app:spawn', 'red'), 5 | spawn = require('cross-spawn') 6 | 7 | /* 8 | Returns pid, takes onClose 9 | */ 10 | module.exports = function (cmd, params, cwd, onClose) { 11 | log(`Running "${cmd} ${params.join(' ')}"`) 12 | log() 13 | 14 | const runner = spawn( 15 | cmd, 16 | params, 17 | { stdio: 'inherit', stdout: 'inherit', stderr: 'inherit', cwd } 18 | ) 19 | 20 | runner.on('close', code => { 21 | log() 22 | if (code) { 23 | log(`Command "${cmd}" failed with exit code: ${code}`) 24 | } 25 | 26 | onClose && onClose(code) 27 | }) 28 | 29 | return runner.pid 30 | } 31 | 32 | /* 33 | Returns nothing, takes onFail 34 | */ 35 | module.exports.sync = function (cmd, params, cwd, onFail) { 36 | log(`[sync] Running "${cmd} ${params.join(' ')}"`) 37 | log() 38 | 39 | const runner = spawn.sync( 40 | cmd, 41 | params, 42 | { stdio: 'inherit', stdout: 'inherit', stderr: 'inherit', cwd } 43 | ) 44 | 45 | if (runner.status || runner.error) { 46 | warn() 47 | warn(`⚠️ Command "${cmd}" failed with exit code: ${runner.status}`) 48 | if (runner.status === null) { 49 | warn(`⚠️ Please globally install "${cmd}"`) 50 | } 51 | onFail && onFail() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /bin/quasar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const localFile = require('import-local-file')(__filename) 4 | if (localFile) { 5 | require(localFile) 6 | } 7 | else { 8 | require('../lib/node-version-check') 9 | require('../lib/helpers/cli-error-handling') 10 | 11 | const commands = [ 12 | 'init', 13 | 'dev', 14 | 'build', 15 | 'clean', 16 | 'mode', 17 | 'info', 18 | 'serve', 19 | 'new', 20 | 'help' 21 | ] 22 | 23 | let cmd = process.argv[2] 24 | 25 | if (cmd) { 26 | if (cmd.length === 1) { 27 | const mapToCmd = { 28 | d: 'dev', 29 | b: 'build', 30 | c: 'clean', 31 | m: 'mode', 32 | i: 'info', 33 | s: 'serve', 34 | n: 'new', 35 | h: 'help' 36 | } 37 | cmd = mapToCmd[cmd] 38 | } 39 | 40 | if (commands.includes(cmd)) { 41 | process.argv.splice(2, 1) 42 | } 43 | else { 44 | if (cmd === '-v' || cmd === '--version') { 45 | console.log(require('../package.json').version) 46 | process.exit(0) 47 | } 48 | 49 | const warn = require('../lib/helpers/logger')('app', 'red') 50 | 51 | warn() 52 | warn(`Unknown command "${ cmd }"`) 53 | if (cmd.indexOf('-') === 0) { 54 | warn(`Command must come before the options`) 55 | } 56 | 57 | warn() 58 | cmd = 'help' 59 | } 60 | } 61 | else { 62 | cmd = 'help' 63 | } 64 | 65 | require(`./quasar-${cmd}`) 66 | } 67 | -------------------------------------------------------------------------------- /templates/entry/import-quasar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS FILE IS GENERATED AUTOMATICALLY. 3 | * DO NOT EDIT. 4 | * 5 | * You are probably looking on adding initialization code. 6 | * Use "quasar new plugin " and add it there. 7 | * One plugin per concern. Then reference the file(s) in quasar.conf.js > plugins: 8 | * plugins: ['file', ...] // do not add ".js" extension to it. 9 | **/ 10 | <% 11 | let useStatement = [ `config: ${JSON.stringify(framework.config)}` ] 12 | 13 | if (framework.i18n) { %> 14 | import lang from 'quasar-framework/i18n/<%= framework.i18n %>' 15 | <% 16 | useStatement.push('i18n: lang') 17 | } 18 | 19 | if (framework.iconSet) { %> 20 | import iconSet from 'quasar-framework/icons/<%= framework.iconSet %>' 21 | <% 22 | useStatement.push('iconSet: iconSet') 23 | } 24 | %> 25 | 26 | import Vue from 'vue' 27 | <% if (framework.all) { %> 28 | import Quasar from 'quasar' 29 | <% } else { 30 | let importStatement = [] 31 | 32 | ;['components', 'directives', 'plugins'].forEach(type => { 33 | if (framework[type]) { 34 | let items = framework[type].filter(item => item) 35 | if (items.length > 0) { 36 | useStatement.push(type + ': {' + items.join(',') + '}') 37 | importStatement = importStatement.concat(items) 38 | } 39 | } 40 | }) 41 | 42 | importStatement = '{Quasar' + (importStatement.length ? ',' + importStatement.join(',') : '') + '}' 43 | %> 44 | import <%= importStatement %> from 'quasar' 45 | <% } %> 46 | 47 | Vue.use(Quasar, { <%= useStatement.join(',') %> }) 48 | -------------------------------------------------------------------------------- /lib/app-paths.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | path = require('path'), 4 | resolve = path.resolve, 5 | join = path.join 6 | 7 | function getAppDir () { 8 | let dir = process.cwd() 9 | 10 | while (dir.length && dir[dir.length - 1] !== path.sep) { 11 | if (fs.existsSync(join(dir, 'quasar.conf.js'))) { 12 | return dir 13 | } 14 | 15 | dir = path.normalize(join(dir, '..')) 16 | } 17 | 18 | const 19 | logger = require('./helpers/logger') 20 | warn = logger('app:paths', 'red') 21 | 22 | warn(`⚠️ Error. This command must be executed inside a Quasar v0.15+ project folder.`) 23 | warn(`For Quasar pre v0.15 projects, npm uninstall -g quasar-cli; npm i -g quasar-cli@0.6.5`) 24 | warn() 25 | process.exit(1) 26 | } 27 | 28 | const 29 | appDir = getAppDir(), 30 | cliDir = resolve(__dirname, '..'), 31 | srcDir = resolve(appDir, 'src'), 32 | pwaDir = resolve(appDir, 'src-pwa'), 33 | ssrDir = resolve(appDir, 'src-ssr'), 34 | cordovaDir = resolve(appDir, 'src-cordova'), 35 | electronDir = resolve(appDir, 'src-electron') 36 | 37 | module.exports = { 38 | cliDir, 39 | appDir, 40 | srcDir, 41 | pwaDir, 42 | ssrDir, 43 | cordovaDir, 44 | electronDir, 45 | 46 | resolve: { 47 | cli: dir => join(cliDir, dir), 48 | app: dir => join(appDir, dir), 49 | src: dir => join(srcDir, dir), 50 | pwa: dir => join(pwaDir, dir), 51 | ssr: dir => join(ssrDir, dir), 52 | cordova: dir => join(cordovaDir, dir), 53 | electron: dir => join(electronDir, dir) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | **What kind of change does this PR introduce?** (check at least one) 10 | 11 | - [ ] Bugfix 12 | - [ ] Feature 13 | - [ ] Code style update 14 | - [ ] Refactor 15 | - [ ] Build-related changes 16 | - [ ] Other, please describe: 17 | 18 | **Does this PR introduce a breaking change?** (check one) 19 | 20 | - [ ] Yes 21 | - [ ] No 22 | 23 | If yes, please describe the impact and migration path for existing applications: 24 | 25 | **The PR fulfills these requirements:** 26 | 27 | - [ ] It's submitted to the `dev` branch and _not_ the `master` branch 28 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix: #xxx[,#xxx]`, where "xxx" is the issue number) 29 | - [ ] It's been tested on Windows 30 | - [ ] It's been tested on Linux 31 | - [ ] It's been tested on MacOS 32 | - [ ] Any necessary documentation has been added or updated [in the docs](https://github.com/quasarframework/quasar-framework.org/tree/dev/source) (for faster update click on "Suggest an edit on GitHub" at bottom of page) or explained in the PR's description. 33 | 34 | If adding a **new feature**, the PR's description includes: 35 | - [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it) 36 | 37 | **Other information:** 38 | -------------------------------------------------------------------------------- /lib/webpack/ssr/server.js: -------------------------------------------------------------------------------- 1 | const 2 | nodeExternals = require('webpack-node-externals'), 3 | VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 4 | 5 | module.exports = function (chain, cfg) { 6 | chain.entry('app') 7 | .clear() 8 | .add(appPaths.resolve.app('.quasar/server-entry.js')) 9 | 10 | chain.target('node') 11 | chain.devtool('#source-map') 12 | 13 | chain.output 14 | .filename('server-bundle.js') 15 | .libraryTarget('commonjs2') 16 | 17 | chain.plugin('define') 18 | .tap(args => { 19 | const { 'process.env': env, ...rest } = args[0] 20 | return [{ 21 | 'process.env': Object.assign( 22 | {}, 23 | env, 24 | { CLIENT: false, SERVER: true } 25 | ), 26 | ...rest 27 | }] 28 | }) 29 | 30 | chain.externals(nodeExternals({ 31 | // do not externalize CSS files in case we need to import it from a dep 32 | whitelist: /(\.css$|\.vue$|\?vue&type=style|quasar-framework[\\/])/ 33 | })) 34 | 35 | chain.plugin('vue-ssr-client') 36 | .use(VueSSRServerPlugin, [{ 37 | filename: '../vue-ssr-server-bundle.json' 38 | }]) 39 | 40 | if (cfg.ctx.prod) { 41 | const SsrProdArtifacts = require('./plugin.ssr-prod-artifacts') 42 | chain.plugin('ssr-artifacts') 43 | .use(SsrProdArtifacts, [ cfg ]) 44 | 45 | // copy src-ssr to dist folder in /server 46 | const CopyWebpackPlugin = require('copy-webpack-plugin') 47 | chain.plugin('copy-webpack') 48 | .use(CopyWebpackPlugin, [ 49 | [{ 50 | from: cfg.ssr.__dir, 51 | to: '../server', 52 | ignore: ['.*'] 53 | }] 54 | ]) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bin/quasar-mode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | parseArgs = require('minimist'), 5 | chalk = require('chalk') 6 | 7 | const 8 | log = require('../lib/helpers/logger')('app:mode') 9 | appPaths = require('../lib/app-paths') 10 | 11 | const argv = parseArgs(process.argv.slice(2), { 12 | alias: { 13 | a: 'add', 14 | r: 'remove', 15 | h: 'help' 16 | }, 17 | string: ['a', 'r'], 18 | boolean: ['h'] 19 | }) 20 | 21 | if (argv.help) { 22 | console.log(` 23 | Description 24 | Add/Remove support for PWA / Cordova / Electron modes. 25 | Usage 26 | $ quasar mode -r|-a pwa|ssr|cordova|electron 27 | 28 | # determine what modes are currently installed: 29 | $ quasar mode 30 | Options 31 | --add, -a Add support for mode [pwa|ssr|cordova|electron] 32 | --remove, -r Remove support for mode [pwa|ssr|cordova|electron] 33 | --help, -h Displays this message 34 | `) 35 | process.exit(0) 36 | } 37 | 38 | require('../lib/helpers/ensure-argv')(argv, 'mode') 39 | const 40 | getMode = require('../lib/mode'), 41 | { green, grey } = require('chalk') 42 | 43 | if (argv.add) { 44 | getMode(argv.add).add() 45 | process.exit(0) 46 | } 47 | else if (argv.remove) { 48 | getMode(argv.remove).remove() 49 | process.exit(0) 50 | } 51 | 52 | log(`Detecting installed modes...`) 53 | 54 | const info = [] 55 | ;['pwa', 'ssr', 'cordova', 'electron'].forEach(mode => { 56 | const QuasarMode = getMode(mode) 57 | info.push([ 58 | `Mode ${mode.toUpperCase()}`, 59 | getMode(mode).isInstalled ? green('yes') : grey('no') 60 | ]) 61 | }) 62 | 63 | console.log( 64 | '\n' + 65 | info.map(msg => ' ' + msg[0].padEnd(16, '.') + ' ' + msg[1]).join('\n') + 66 | '\n' 67 | ) 68 | -------------------------------------------------------------------------------- /lib/artifacts.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | path = require('path'), 4 | fse = require('fs-extra') 5 | 6 | const 7 | appPaths = require('./app-paths'), 8 | filePath = appPaths.resolve.app('.quasar/artifacts.json'), 9 | log = require('./helpers/logger')('app:artifacts') 10 | 11 | function exists () { 12 | return fs.existsSync(filePath) 13 | } 14 | 15 | function getArtifacts () { 16 | return exists() 17 | ? require(filePath) 18 | : { folders: [] } 19 | } 20 | 21 | function save (content) { 22 | fse.mkdirp(path.dirname(filePath)) 23 | fs.writeFileSync(filePath, JSON.stringify(content), 'utf-8') 24 | } 25 | 26 | module.exports.add = function (entry) { 27 | const content = getArtifacts() 28 | 29 | if (!content.folders.includes(entry)) { 30 | content.folders.push(entry) 31 | save(content) 32 | log(`Added build artifact "${entry}"`) 33 | } 34 | } 35 | 36 | module.exports.clean = function (folder) { 37 | if (folder.endsWith(path.join('src-cordova', 'www'))) { 38 | fse.emptyDirSync(folder) 39 | } 40 | else { 41 | fse.removeSync(folder) 42 | } 43 | 44 | log(`Cleaned build artifact: "${folder}"`) 45 | } 46 | 47 | module.exports.cleanAll = function () { 48 | getArtifacts().folders.forEach(folder => { 49 | if (folder.endsWith(path.join('src-cordova', 'www'))) { 50 | fse.emptyDirSync(folder) 51 | } 52 | else { 53 | fse.removeSync(folder) 54 | } 55 | 56 | log(`Cleaned build artifact: "${folder}"`) 57 | }) 58 | 59 | let folder = appPaths.resolve.app('.quasar') 60 | fse.removeSync(folder) 61 | log(`Cleaned build artifact: "${folder}"`) 62 | 63 | fse.emptyDirSync(appPaths.resolve.app('dist')) 64 | log(`Emptied dist folder`) 65 | } 66 | -------------------------------------------------------------------------------- /lib/webpack/ssr/template.ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS FILE IS GENERATED AUTOMATICALLY. 3 | * DO NOT EDIT. 4 | **/ 5 | 6 | const 7 | fs = require('fs'), 8 | path = require('path'), 9 | LRU = require('lru-cache'), 10 | { createBundleRenderer } = require('vue-server-renderer') 11 | 12 | const 13 | resolve = file => path.join(__dirname, file), 14 | template = fs.readFileSync(resolve('template.html'), 'utf-8'), 15 | bundle = require('./vue-ssr-server-bundle.json'), 16 | clientManifest = require('./vue-ssr-client-manifest.json') 17 | 18 | const settings = <%= opts %> 19 | 20 | if (process.env.DEBUG) { 21 | settings.debug = true 22 | } 23 | 24 | const rendererOptions = { 25 | template, 26 | clientManifest, 27 | // for component caching 28 | cache: LRU(settings.componentCache), 29 | basedir: __dirname, 30 | // recommended for performance 31 | runInNewContext: false 32 | } 33 | 34 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 35 | let renderer = createBundleRenderer(bundle, rendererOptions) 36 | 37 | module.exports.renderToString = function ({ req, res }, cb) { 38 | const ctx = { 39 | url: req.url, 40 | req, 41 | res 42 | } 43 | <% if (flags.meta) { %> 44 | renderer.renderToString(ctx, (err, html) => { 45 | if (err) { cb(err, html) } 46 | else { cb(err, ctx.$getMetaHTML(html)) } 47 | }) 48 | <% } else { %> 49 | renderer.renderToString(ctx, cb) 50 | <% } %> 51 | } 52 | 53 | module.exports.resolveWWW = function (file) { 54 | return resolve('www/' + file) 55 | } 56 | 57 | module.exports.mergeRendererOptions = function (opts) { 58 | renderer = createBundleRenderer( 59 | bundle, 60 | Object.assign(rendererOptions, opts) 61 | ) 62 | } 63 | 64 | module.exports.settings = settings 65 | -------------------------------------------------------------------------------- /lib/helpers/net.js: -------------------------------------------------------------------------------- 1 | const 2 | os = require('os'), 3 | net = require('net') 4 | 5 | module.exports.getExternalNetworkInterface = function () { 6 | const 7 | networkInterfaces = os.networkInterfaces(), 8 | devices = [] 9 | 10 | for (let deviceName of Object.keys(networkInterfaces)) { 11 | const networkInterface = networkInterfaces[deviceName] 12 | 13 | for (let networkAddress of networkInterface) { 14 | if (!networkAddress.internal && networkAddress.family === 'IPv4') { 15 | devices.push({ deviceName, ...networkAddress }) 16 | } 17 | } 18 | } 19 | 20 | return devices 21 | } 22 | 23 | module.exports.findClosestOpenPort = async function (port, host) { 24 | let portProposal = port 25 | 26 | do { 27 | if (await module.exports.isPortAvailable(portProposal, host)) { 28 | return portProposal 29 | } 30 | portProposal++ 31 | } 32 | while (portProposal < 65535) 33 | 34 | throw new Error('ERROR_NETWORK_PORT_NOT_AVAIL') 35 | } 36 | 37 | module.exports.isPortAvailable = async function (port, host) { 38 | return new Promise((resolve, reject) => { 39 | const tester = net.createServer() 40 | .once('error', err => { 41 | if (err.code === 'EADDRNOTAVAIL') { 42 | reject(new Error('ERROR_NETWORK_ADDRESS_NOT_AVAIL')) 43 | } 44 | else if (err.code === 'EADDRINUSE') { 45 | resolve(false) // host/port in use 46 | } 47 | else { 48 | reject(err) 49 | } 50 | }) 51 | .once('listening', () => { 52 | tester.once('close', () => { 53 | resolve(true) // found available host/port 54 | }) 55 | .close() 56 | }) 57 | .on('error', err => { 58 | reject(err) 59 | }) 60 | .listen(port, host) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /lib/cordova/index.js: -------------------------------------------------------------------------------- 1 | const 2 | log = require('../helpers/logger')('app:cordova'), 3 | CordovaConfig = require('./cordova-config'), 4 | spawn = require('../helpers/spawn'), 5 | onShutdown = require('../helpers/on-shutdown'), 6 | appPaths = require('../app-paths') 7 | 8 | class CordovaRunner { 9 | constructor () { 10 | this.pid = 0 11 | this.config = new CordovaConfig() 12 | 13 | onShutdown(() => { 14 | this.stop() 15 | }) 16 | } 17 | 18 | run (quasarConfig) { 19 | const url = quasarConfig.getBuildConfig().build.APP_URL 20 | 21 | if (this.url === url) { 22 | return 23 | } 24 | 25 | if (this.pid) { 26 | this.stop() 27 | } 28 | 29 | this.url = url 30 | 31 | const 32 | cfg = quasarConfig.getBuildConfig(), 33 | args = ['run', cfg.ctx.targetName] 34 | 35 | if (cfg.ctx.emulator) { 36 | args.push(`--target=${cfg.ctx.emulator}`) 37 | } 38 | 39 | if (cfg.ctx.targetName === 'ios') { 40 | args.push(`--buildFlag=-UseModernBuildSystem=0`) 41 | } 42 | 43 | return this.__runCordovaCommand( 44 | cfg, 45 | args 46 | ) 47 | } 48 | 49 | build (quasarConfig) { 50 | const cfg = quasarConfig.getBuildConfig() 51 | 52 | return this.__runCordovaCommand( 53 | cfg, 54 | ['build', cfg.ctx.debug ? '--debug' : '--release', cfg.ctx.targetName] 55 | ) 56 | } 57 | 58 | stop () { 59 | if (!this.pid) { return } 60 | 61 | log('Shutting down Cordova process...') 62 | process.kill(this.pid) 63 | this.__cleanup() 64 | } 65 | 66 | __runCordovaCommand (cfg, args) { 67 | this.config.prepare(cfg) 68 | 69 | return new Promise((resolve, reject) => { 70 | this.pid = spawn( 71 | 'cordova', 72 | args, 73 | appPaths.cordovaDir, 74 | code => { 75 | this.__cleanup() 76 | resolve(code) 77 | } 78 | ) 79 | }) 80 | } 81 | 82 | __cleanup () { 83 | this.pid = 0 84 | this.config.reset() 85 | } 86 | } 87 | 88 | module.exports = new CordovaRunner() 89 | -------------------------------------------------------------------------------- /lib/webpack/plugin.html-addons.js: -------------------------------------------------------------------------------- 1 | const appPaths = require('../app-paths') 2 | 3 | function makeTag (tagName, attributes, closeTag = false) { 4 | return { 5 | tagName, 6 | attributes, 7 | closeTag 8 | } 9 | } 10 | 11 | function makeScriptTag (innerHTML) { 12 | return { 13 | tagName: 'script', 14 | closeTag: true, 15 | innerHTML 16 | } 17 | } 18 | 19 | function fillHtmlTags (data, cfg) { 20 | if (cfg.build.appBase) { 21 | data.head.push( 22 | makeTag('base', { href: cfg.build.appBase }) 23 | ) 24 | } 25 | } 26 | 27 | module.exports.fillHtmlTags = fillHtmlTags 28 | 29 | module.exports.plugin = class HtmlAddonsPlugin { 30 | constructor (cfg = {}) { 31 | this.cfg = cfg 32 | } 33 | 34 | apply (compiler) { 35 | compiler.hooks.compilation.tap('webpack-plugin-html-addons', compilation => { 36 | compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('webpack-plugin-html-addons', (data, callback) => { 37 | fillHtmlTags(data, this.cfg) 38 | 39 | if (this.cfg.ctx.mode.electron && this.cfg.ctx.dev) { 40 | data.head.push( 41 | makeScriptTag(` 42 | require('module').globalPaths.push('${appPaths.resolve.app('node_modules').replace(/\\/g, '\\\\')}') 43 | `) 44 | ) 45 | } 46 | 47 | if (this.cfg.ctx.mode.cordova) { 48 | data.body.unshift( 49 | makeTag('script', { src: 'cordova.js' }, true) 50 | ) 51 | } 52 | else if (this.cfg.ctx.mode.electron && this.cfg.ctx.prod) { 53 | // set statics path in production; 54 | // the reason we add this is here is because the folder path 55 | // needs to be evaluated at runtime 56 | const bodyScript = ` 57 | window.__statics = require('path').join(__dirname, 'statics').replace(/\\\\/g, '\\\\'); 58 | ` 59 | data.body.push( 60 | makeScriptTag(bodyScript) 61 | ) 62 | } 63 | 64 | // finally, inform Webpack that we're ready 65 | callback(null, data) 66 | }) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /templates/entry/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS FILE IS GENERATED AUTOMATICALLY. 3 | * DO NOT EDIT. 4 | * 5 | * You are probably looking on adding initialization code. 6 | * Use "quasar new plugin " and add it there. 7 | * One plugin per concern. Then reference the file(s) in quasar.conf.js > plugins: 8 | * plugins: ['file', ...] // do not add ".js" extension to it. 9 | **/ 10 | import './import-quasar.js' 11 | 12 | <% if (ctx.mode.ssr) { %> 13 | import <%= framework.all ? 'Quasar' : '{ Quasar }' %> from 'quasar' 14 | <% } %> 15 | 16 | import App from 'app/<%= sourceFiles.rootComponent %>' 17 | 18 | <% if (store) { %> 19 | import createStore from 'app/<%= sourceFiles.store %>' 20 | <% } %> 21 | import createRouter from 'app/<%= sourceFiles.router %>' 22 | 23 | export default function (<%= ctx.mode.ssr ? 'ssrContext' : '' %>) { 24 | // create store and router instances 25 | <% if (store) { %> 26 | const store = typeof createStore === 'function' 27 | ? createStore(<%= ctx.mode.ssr ? '{ ssrContext }' : '' %>) 28 | : createStore 29 | <% } %> 30 | const router = typeof createRouter === 'function' 31 | ? createRouter({<%= ctx.mode.ssr ? 'ssrContext' + (store ? ', ' : '') : '' %><%= store ? 'store' : '' %>}) 32 | : createRouter 33 | <% if (store) { %> 34 | // make router instance available in store 35 | store.$router = router 36 | <% } %> 37 | 38 | // Create the app instantiation Object. 39 | // Here we inject the router, store to all child components, 40 | // making them available everywhere as `this.$router` and `this.$store`. 41 | const app = { 42 | <% if (!ctx.mode.ssr) { %>el: '#q-app',<% } %> 43 | router, 44 | <%= store ? 'store,' : '' %> 45 | render: h => h(App) 46 | } 47 | 48 | <% if (ctx.mode.ssr) { %> 49 | Quasar.ssrUpdate({ app, ssr: ssrContext }) 50 | <% } %> 51 | 52 | // expose the app, the router and the store. 53 | // note we are not mounting the app here, since bootstrapping will be 54 | // different depending on whether we are in a browser or on the server. 55 | return { 56 | app, 57 | <%= store ? 'store,' : '' %> 58 | router 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/webpack/pwa/plugin.html-pwa.js: -------------------------------------------------------------------------------- 1 | function makeTag (tagName, attributes) { 2 | return { 3 | tagName, 4 | attributes, 5 | closeTag: false 6 | } 7 | } 8 | 9 | function fillPwaTags (data, { pwa: { manifest }}) { 10 | data.head.push( 11 | // Add to home screen for Android and modern mobile browsers 12 | makeTag('link', { 13 | rel: 'manifest', 14 | href: 'manifest.json' 15 | }), 16 | makeTag('meta', { 17 | name: 'theme-color', 18 | content: manifest.theme_color 19 | }), 20 | 21 | // Add to home screen for Safari on iOS 22 | makeTag('meta', { 23 | name: 'apple-mobile-web-app-capable', 24 | content: 'yes' 25 | }), 26 | makeTag('meta', { 27 | name: 'apple-mobile-web-app-status-bar-style', 28 | content: manifest.background_color 29 | }), 30 | makeTag('meta', { 31 | name: 'apple-mobile-web-app-title', 32 | content: manifest.name 33 | }), 34 | makeTag('link', { 35 | rel: 'apple-touch-icon', 36 | href: 'statics/icons/apple-icon-152x152.png' 37 | }), 38 | /* makeTag('link', { 39 | rel: 'mask-icon', 40 | href: 'statics/icons/safari-pinned-tab.svg', 41 | color: manifest.theme_color 42 | }), */ 43 | 44 | // Add to home screen for Windows 45 | makeTag('meta', { 46 | name: 'msapplication-TileImage', 47 | content: 'statics/icons/ms-icon-144x144.png' 48 | }), 49 | makeTag('meta', { 50 | name: 'msapplication-TileColor', 51 | content: manifest.background_color 52 | }) 53 | ) 54 | } 55 | 56 | module.exports.fillPwaTags = fillPwaTags 57 | 58 | module.exports.plugin = class HtmlPwaPlugin { 59 | constructor (cfg = {}) { 60 | this.cfg = cfg 61 | } 62 | 63 | apply (compiler) { 64 | compiler.hooks.compilation.tap('webpack-plugin-html-pwa', compilation => { 65 | compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('webpack-plugin-html-pwa', (data, callback) => { 66 | fillPwaTags(data, this.cfg) 67 | 68 | // finally, inform Webpack that we're ready 69 | callback(null, data) 70 | }) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/mode/mode-electron.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | fse = require('fs-extra'), 4 | appPaths = require('../app-paths'), 5 | logger = require('../helpers/logger'), 6 | log = logger('app:mode-electron'), 7 | warn = logger('app:mode-electron', 'red'), 8 | spawn = require('../helpers/spawn'), 9 | nodePackager = require('../helpers/node-packager') 10 | 11 | const 12 | electronDeps = { 13 | 'electron': '4.0.5', 14 | 'electron-debug': '2.1.0', 15 | 'electron-devtools-installer': '2.2.4', 16 | 'devtron': '1.4.0' 17 | } 18 | 19 | class Mode { 20 | get isInstalled () { 21 | return fs.existsSync(appPaths.electronDir) 22 | } 23 | 24 | add (params) { 25 | if (this.isInstalled) { 26 | warn(`Electron support detected already. Aborting.`) 27 | return 28 | } 29 | 30 | const cmdParam = nodePackager === 'npm' 31 | ? ['install', '--save-dev'] 32 | : ['add', '--dev'] 33 | 34 | log(`Installing Electron dependencies...`) 35 | spawn.sync( 36 | nodePackager, 37 | cmdParam.concat(Object.keys(electronDeps).map(dep => { 38 | return `${dep}@${electronDeps[dep]}` 39 | })), 40 | appPaths.appDir, 41 | () => warn('Failed to install Electron dependencies') 42 | ) 43 | 44 | log(`Creating Electron source folder...`) 45 | fse.copySync(appPaths.resolve.cli('templates/electron'), appPaths.electronDir) 46 | log(`Electron support was added`) 47 | } 48 | 49 | remove () { 50 | if (!this.isInstalled) { 51 | warn(`No Electron support detected. Aborting.`) 52 | return 53 | } 54 | 55 | log(`Removing Electron source folder`) 56 | fse.removeSync(appPaths.electronDir) 57 | 58 | const cmdParam = nodePackager === 'npm' 59 | ? ['uninstall', '--save-dev'] 60 | : ['remove', '--dev'] 61 | 62 | log(`Uninstalling Electron dependencies...`) 63 | spawn.sync( 64 | nodePackager, 65 | cmdParam.concat(Object.keys(electronDeps)), 66 | appPaths.appDir, 67 | () => warn('Failed to uninstall Electron dependencies') 68 | ) 69 | 70 | log(`Electron support was removed`) 71 | } 72 | } 73 | 74 | module.exports = Mode 75 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | ### Software version 30 | 31 | OS: 32 | Node: 33 | NPM: 34 | Any other software related to your bug: 35 | 36 | ### What did you get as the error? 37 | 38 | ### What were you expecting? 39 | 40 | ### What steps did you take, to get the error? 41 | -------------------------------------------------------------------------------- /templates/ssr/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | * 5 | * All content of this folder will be copied as is to the output folder. So only import: 6 | * 1. node_modules (and yarn/npm install dependencies -- NOT to devDependecies though) 7 | * 2. create files in this folder and import only those with the relative path 8 | * 9 | * Note: This file is used only for PRODUCTION. It is not picked up while in dev mode. 10 | * If you are looking to add common DEV & PROD logic to the express app, then use 11 | * "src-ssr/extension.js" 12 | */ 13 | 14 | const 15 | express = require('express'), 16 | compression = require('compression') 17 | 18 | const 19 | ssr = require('../ssr'), 20 | extension = require('./extension'), 21 | app = express(), 22 | port = process.env.PORT || 3000 23 | 24 | const serve = (path, cache) => express.static(ssr.resolveWWW(path), { 25 | maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0 26 | }) 27 | 28 | // gzip 29 | app.use(compression({ threshold: 0 })) 30 | 31 | // serve this with no cache, if built with PWA: 32 | if (ssr.settings.pwa) { 33 | app.use('/service-worker.js', serve('service-worker.js')) 34 | } 35 | 36 | // serve "www" folder 37 | app.use('/', serve('.', true)) 38 | 39 | // we extend the custom common dev & prod parts here 40 | extension.extendApp({ app }) 41 | 42 | // this should be last get(), rendering with SSR 43 | app.get('*', (req, res) => { 44 | res.setHeader('Content-Type', 'text/html') 45 | ssr.renderToString({ req, res }, (err, html) => { 46 | if (err) { 47 | if (err.url) { 48 | res.redirect(err.url) 49 | } 50 | else if (err.code === 404) { 51 | res.status(404).send('404 | Page Not Found') 52 | } 53 | else { 54 | // Render Error Page or Redirect 55 | res.status(500).send('500 | Internal Server Error') 56 | if (ssr.settings.debug) { 57 | console.error(`500 on ${req.url}`) 58 | console.error(err) 59 | console.error(err.stack) 60 | } 61 | } 62 | } 63 | else { 64 | res.send(html) 65 | } 66 | }) 67 | }) 68 | 69 | app.listen(port, () => { 70 | console.log(`Server listening at port ${port}`) 71 | }) 72 | -------------------------------------------------------------------------------- /lib/ssr/html-template.js: -------------------------------------------------------------------------------- 1 | const 2 | compileTemplate = require('lodash.template'), 3 | HtmlWebpackPlugin = require('html-webpack-plugin'), 4 | { fillHtmlTags } = require('../webpack/plugin.html-addons'), 5 | { fillPwaTags } = require('../webpack/pwa/plugin.html-pwa') 6 | 7 | function injectSsrInterpolation (html) { 8 | return html 9 | .replace( 10 | /(]*)(>)/i, 11 | (found, start, end) => { 12 | let matches 13 | 14 | matches = found.match(/\sdir\s*=\s*['"]([^'"]*)['"]/i) 15 | if (matches) { 16 | start = start.replace(matches[0], '') 17 | } 18 | 19 | matches = found.match(/\slang\s*=\s*['"]([^'"]*)['"]/i) 20 | if (matches) { 21 | start = start.replace(matches[0], '') 22 | } 23 | 24 | return `${start} {{ Q_HTML_ATTRS }}${end}` 25 | } 26 | ) 27 | .replace( 28 | /(]*)(>)/i, 29 | (found, start, end) => `${start}${end}{{ Q_HEAD_TAGS }}` 30 | ) 31 | .replace( 32 | /(]*)(>)/i, 33 | (found, start, end) => { 34 | let classes = '{{ Q_BODY_CLASSES }}' 35 | 36 | const matches = found.match(/\sclass\s*=\s*['"]([^'"]*)['"]/i) 37 | 38 | if (matches) { 39 | if (matches[1].length > 0) { 40 | classes += ` ${matches[1]}` 41 | } 42 | start = start.replace(matches[0], '') 43 | } 44 | 45 | return `${start} class="${classes.trim()}" {{ Q_BODY_ATTRS }}${end}{{ Q_BODY_TAGS }}` 46 | } 47 | ) 48 | } 49 | 50 | module.exports.getIndexHtml = function (template, cfg) { 51 | const compiled = compileTemplate( 52 | template.replace('
', '') 53 | ) 54 | let html = compiled({ 55 | htmlWebpackPlugin: { 56 | options: cfg.__html.variables 57 | } 58 | }) 59 | 60 | const data = { body: [], head: [] } 61 | 62 | fillHtmlTags(data, cfg) 63 | 64 | if (cfg.ctx.mode.pwa) { 65 | fillPwaTags(data, cfg) 66 | } 67 | 68 | html = HtmlWebpackPlugin.prototype.injectAssetsIntoHtml(html, {}, data) 69 | html = injectSsrInterpolation(html) 70 | 71 | if (cfg.build.minify) { 72 | const { minify } = require('html-minifier') 73 | html = minify(html, Object.assign({}, cfg.__html.minifyOptions, { 74 | ignoreCustomComments: [ /vue-ssr-outlet/ ], 75 | ignoreCustomFragments: [ /{{ [\s\S]*? }}/ ] 76 | })) 77 | } 78 | 79 | return html 80 | } 81 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | fse = require('fs-extra'), 4 | path = require('path'), 5 | compileTemplate = require('lodash.template') 6 | 7 | const 8 | log = require('./helpers/logger')('app:generator') 9 | appPaths = require('./app-paths'), 10 | quasarFolder = appPaths.resolve.app('.quasar') 11 | 12 | class Generator { 13 | constructor (quasarConfig) { 14 | const { ctx, loadingBar, preFetch } = quasarConfig.getBuildConfig() 15 | 16 | this.alreadyGenerated = false 17 | this.quasarConfig = quasarConfig 18 | 19 | const paths = [ 20 | 'app.js', 21 | 'client-entry.js', 22 | 'import-quasar.js' 23 | ] 24 | 25 | if (preFetch) { 26 | paths.push('client-prefetch.js') 27 | } 28 | if (ctx.mode.ssr) { 29 | paths.push('server-entry.js') 30 | } 31 | 32 | this.files = paths.map(file => { 33 | const 34 | content = fs.readFileSync( 35 | appPaths.resolve.cli(`templates/entry/${file}`), 36 | 'utf-8' 37 | ), 38 | filename = path.basename(file) 39 | 40 | return { 41 | filename, 42 | dest: path.join(quasarFolder, filename), 43 | template: compileTemplate(content) 44 | } 45 | }) 46 | } 47 | 48 | prepare () { 49 | const 50 | now = Date.now() / 1000, 51 | then = now - 100, 52 | appVariablesFile = appPaths.resolve.cli('templates/app/variables.styl'), 53 | appStylFile = appPaths.resolve.cli('templates/app/app.styl'), 54 | emptyStylFile = path.join(quasarFolder, 'empty.styl') 55 | 56 | function copy (file) { 57 | const dest = path.join(quasarFolder, path.basename(file)) 58 | fse.copySync(file, dest) 59 | fs.utimes(dest, then, then, function (err) { if (err) throw err }) 60 | } 61 | 62 | copy(appStylFile) 63 | copy(appVariablesFile) 64 | 65 | fs.writeFileSync(emptyStylFile, '', 'utf-8'), 66 | fs.utimes(emptyStylFile, then, then, function (err) { if (err) throw err }) 67 | } 68 | 69 | build () { 70 | log(`Generating Webpack entry point`) 71 | const data = this.quasarConfig.getBuildConfig() 72 | 73 | this.files.forEach(file => { 74 | fs.writeFileSync(file.dest, file.template(data), 'utf-8') 75 | }) 76 | 77 | if (!this.alreadyGenerated) { 78 | const then = Date.now() / 1000 - 120 79 | 80 | this.files.forEach(file => { 81 | fs.utimes(file.dest, then, then, function (err) { if (err) throw err }) 82 | }) 83 | 84 | this.alreadyGenerated = true 85 | } 86 | } 87 | } 88 | 89 | module.exports = Generator 90 | -------------------------------------------------------------------------------- /bin/quasar-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const parseArgs = require('minimist') 4 | 5 | const argv = parseArgs(process.argv.slice(2), { 6 | alias: { 7 | v: 'version', 8 | t: 'type', 9 | h: 'help' 10 | }, 11 | boolean: ['h'], 12 | string: ['v', 't'] 13 | }) 14 | 15 | if (argv.help) { 16 | console.log(` 17 | Description 18 | Create a Quasar App folder from the starter kit. 19 | You need "vue-cli" package installed globally. 20 | Usage 21 | # Install latest starter kit: 22 | $ quasar init 23 | 24 | # Install starter kit for specific Quasar version. 25 | # Only specify the major and minor version (no patch version). 26 | # Good example: 0.15, 1.0, 1.2 27 | # Bad example: 0.15.2, 1.0.1, 1.2.2 28 | $ quasar init -v 0.15 29 | 30 | # Install UMD starter kit 31 | $ quasar init -t umd 32 | 33 | Options 34 | --version, -v Install specific Quasar version 35 | --type, -t Install specific starter kit 36 | --help, -h Displays this message 37 | `) 38 | process.exit(0) 39 | } 40 | 41 | const 42 | logger = require('../lib/helpers/logger'), 43 | log = logger('app:init'), 44 | warn = logger('app:init', 'red'), 45 | spawn = require('cross-spawn'), 46 | resolve = require('path').resolve 47 | 48 | if (argv.type && !['umd'].includes(argv.type)) { 49 | warn(`Specified type ("${argv.type}") is not recognized.`) 50 | warn() 51 | process.exit(0) 52 | } 53 | 54 | if (!argv._[0]) { 55 | warn(`Missing folder name as parameter.`) 56 | warn() 57 | process.exit(0) 58 | } 59 | 60 | const 61 | cliDir = resolve(__dirname, '..') 62 | 63 | let template = `quasarframework/quasar-starter-kit${argv.type ? `-${argv.type}` : ''}` 64 | if (argv.version) { 65 | template += `#v${argv.version}` 66 | } 67 | 68 | try { 69 | console.log(` Running command: vue init '${template}' ${argv._[0]}`) 70 | const child = spawn.sync('vue', [ 71 | 'init', 72 | template, 73 | argv._[0] 74 | ], { stdio: ['inherit', 'inherit', 'inherit'] }) 75 | 76 | if (child.status !== 0) { 77 | warn(`⚠️ Something went wrong... Try running the "vue init" command above manually.`) 78 | warn(`Reasons for failure: Package @vue/cli and @vue/cli-init are not installed globally, specified template is unavailable or it failed to download.`) 79 | warn() 80 | process.exit(1) 81 | } 82 | } 83 | catch (err) { 84 | console.log(err) 85 | warn(`⚠️ Package vue-cli not installed globally.`) 86 | warn('Run "yarn global add @vue/cli @vue/cli-init" or "npm i -g @vue/cli @vue/cli-init" to install Vue CLI and then try again.') 87 | warn() 88 | process.exit(1) 89 | } 90 | -------------------------------------------------------------------------------- /lib/webpack/pwa/index.js: -------------------------------------------------------------------------------- 1 | const 2 | appPaths = require('../../app-paths'), 3 | PwaManifestPlugin = require('./plugin.pwa-manifest') 4 | 5 | module.exports = function (chain, cfg) { 6 | // write manifest.json file 7 | chain.plugin('pwa-manifest') 8 | .use(PwaManifestPlugin, [ cfg ]) 9 | 10 | let defaultOptions 11 | const 12 | WorkboxPlugin = require('workbox-webpack-plugin'), 13 | pluginMode = cfg.pwa.workboxPluginMode, 14 | log = require('../../helpers/logger')('app:workbox') 15 | 16 | if (pluginMode === 'GenerateSW') { 17 | const pkg = require(appPaths.resolve.app('package.json')) 18 | 19 | defaultOptions = { 20 | cacheId: pkg.name || 'quasar-pwa-app' 21 | } 22 | 23 | log('[GenerateSW] Will generate a service-worker file. Ignoring your custom written one.') 24 | } 25 | else { 26 | defaultOptions = { 27 | swSrc: appPaths.resolve.app(cfg.sourceFiles.serviceWorker) 28 | } 29 | 30 | log('[InjectManifest] Using your custom service-worker written file') 31 | } 32 | 33 | let opts = Object.assign( 34 | defaultOptions, 35 | cfg.pwa.workboxOptions 36 | ) 37 | 38 | if (cfg.ctx.mode.ssr) { 39 | if (!opts.directoryIndex) { 40 | opts.directoryIndex = '/' 41 | } 42 | 43 | // if Object form: 44 | if (cfg.ssr.pwa && cfg.ssr.pwa !== true) { 45 | const merge = require('webpack-merge') 46 | opts = merge(opts, cfg.ssr.pwa) 47 | } 48 | 49 | if (pluginMode === 'GenerateSW') { 50 | opts.runtimeCaching = opts.runtimeCaching || [] 51 | if (!opts.runtimeCaching.find(entry => entry.urlPattern === '/')) { 52 | opts.runtimeCaching.unshift({ 53 | urlPattern: '/', 54 | handler: 'networkFirst' 55 | }) 56 | } 57 | } 58 | } 59 | 60 | if (cfg.ctx.dev) { 61 | log('⚠️ Forcing PWA into the network-first approach to not break Hot Module Replacement while developing.') 62 | // forcing network-first strategy 63 | opts.chunks = [ 'quasar-bogus-chunk' ] 64 | if (opts.excludeChunks) { 65 | delete opts.excludeChunks 66 | } 67 | 68 | if (pluginMode === 'GenerateSW') { 69 | opts.runtimeCaching = opts.runtimeCaching || [] 70 | opts.runtimeCaching.push({ 71 | urlPattern: /^http/, 72 | handler: 'networkFirst' 73 | }) 74 | } 75 | } 76 | 77 | opts.swDest = 'service-worker.js' 78 | 79 | chain.plugin('workbox') 80 | .use(WorkboxPlugin[pluginMode], [ opts ]) 81 | 82 | if (!cfg.ctx.mode.ssr) { 83 | const HtmlPwaPlugin = require('./plugin.html-pwa').plugin 84 | chain.plugin('html-pwa') 85 | .use(HtmlPwaPlugin, [ cfg ]) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/electron/bundler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const 3 | appPath = require('../app-paths'), 4 | packagerVersion = '13.1.0', 5 | log = require('../helpers/logger')('app:electron-bundle') 6 | 7 | function isValidName (bundlerName) { 8 | return ['packager', 'builder'].includes(bundlerName) 9 | } 10 | 11 | function installBundler (bundlerName) { 12 | const 13 | spawn = require('../helpers/spawn'), 14 | nodePackager = require('../helpers/node-packager'), 15 | version = bundlerName === 'packager' ? `^${packagerVersion}` : 'latest', 16 | cmdParam = nodePackager === 'npm' 17 | ? ['install', '--save-dev'] 18 | : ['add', '--dev'] 19 | 20 | log(`Installing required Electron bundler (electron-${bundlerName})...`) 21 | spawn.sync( 22 | nodePackager, 23 | cmdParam.concat([`electron-${bundlerName}@${version}`]), 24 | appPaths.appDir, 25 | () => warn(`Failed to install electron-${bundlerName}`) 26 | ) 27 | } 28 | 29 | function isInstalled (bundlerName) { 30 | return fs.existsSync(appPath.resolve.app(`node_modules/electron-${bundlerName}`)) 31 | } 32 | 33 | module.exports.ensureInstall = function (bundlerName) { 34 | if (!isValidName(bundlerName)) { 35 | warn(`⚠️ Unknown bundler "${ bundlerName }" for Electron`) 36 | warn() 37 | process.exit(1) 38 | } 39 | 40 | if (bundlerName === 'packager') { 41 | if (isInstalled('packager')) { 42 | const 43 | semver = require('semver'), 44 | pkg = require(appPath.resolve.app(`node_modules/electron-${bundlerName}/package.json`)) 45 | 46 | if (semver.satisfies(pkg.version, `>= ${packagerVersion}`)) { 47 | return 48 | } 49 | } 50 | } 51 | else if (isInstalled('builder')) { 52 | return 53 | } 54 | 55 | installBundler(bundlerName) 56 | } 57 | 58 | module.exports.getDefaultName = function () { 59 | if (isInstalled('packager')) { 60 | return 'packager' 61 | } 62 | 63 | if (isInstalled('builder')) { 64 | return 'builder' 65 | } 66 | 67 | return 'packager' 68 | } 69 | 70 | module.exports.getBundler = function (bundlerName) { 71 | return require(appPath.resolve.app(`node_modules/electron-${bundlerName}`)) 72 | } 73 | 74 | module.exports.ensureBuilderCompatibility = function () { 75 | if (fs.existsSync(appPaths.resolve.electron('icons/linux-256x256.png'))) { 76 | console.log() 77 | console.log(`\n⚠️ electron-builder requires a change to your src-electron/icons folder: 78 | * replace linux-256x256.png with a 512x512 px png file named "linux-512x512.png" 79 | * make sure to delete the old linux-256x256.png file 80 | `) 81 | console.log() 82 | process.exit(1) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /templates/app/layout.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 99 | 100 | 102 | -------------------------------------------------------------------------------- /lib/helpers/ensure-argv.js: -------------------------------------------------------------------------------- 1 | const warn = require('./logger')('app:ensure-argv', 'red') 2 | 3 | module.exports = function (argv, cmd) { 4 | if (cmd === 'mode') { 5 | if (![undefined, 'pwa', 'cordova', 'electron', 'ssr'].includes(argv.add)) { 6 | warn(`⚠️ Unknown mode "${ argv.add }" to add`) 7 | warn() 8 | process.exit(1) 9 | } 10 | if (![undefined, 'pwa', 'cordova', 'electron', 'ssr'].includes(argv.remove)) { 11 | warn(`⚠️ Unknown mode "${ argv.remove }" to remove`) 12 | warn() 13 | process.exit(1) 14 | } 15 | 16 | return 17 | } 18 | 19 | if (!['spa', 'pwa', 'cordova', 'electron', 'ssr'].includes(argv.mode)) { 20 | warn(`⚠️ Unknown mode "${ argv.mode }"`) 21 | warn() 22 | process.exit(1) 23 | } 24 | 25 | if (!['mat', 'ios'].includes(argv.theme)) { 26 | warn(`⚠️ Unknown theme "${ argv.theme }"`) 27 | warn() 28 | process.exit(1) 29 | } 30 | 31 | if (argv.mode === 'cordova') { 32 | const targets = ['android', 'ios', 'blackberry10', 'browser', 'osx', 'ubuntu', 'webos', 'windows'] 33 | if (!argv.target) { 34 | warn(`⚠️ Please also specify a target (-T <${targets.join('|')}>)`) 35 | warn() 36 | process.exit(1) 37 | } 38 | if (!targets.includes(argv.target)) { 39 | warn(`⚠️ Unknown target "${ argv.target }" for Cordova`) 40 | warn() 41 | process.exit(1) 42 | } 43 | } 44 | 45 | if (cmd === 'build' && argv.mode === 'electron') { 46 | if (![undefined, 'packager', 'builder'].includes(argv.bundler)) { 47 | warn(`⚠️ Unknown bundler "${ argv.bundler }" for Electron`) 48 | warn() 49 | process.exit(1) 50 | } 51 | 52 | if ([undefined, 'packager'].includes(argv.bundler)) { 53 | if (![undefined, 'all', 'darwin', 'win32', 'linux', 'mas'].includes(argv.target)) { 54 | warn(`⚠️ Unknown target "${ argv.target }" for electron-packager`) 55 | warn() 56 | process.exit(1) 57 | } 58 | if (![undefined, 'ia32', 'x64', 'armv7l', 'arm64', 'mips64el', 'all'].includes(argv.arch)) { 59 | warn(`⚠️ Unknown architecture "${ argv.arch }" for electron-packager`) 60 | warn() 61 | process.exit(1) 62 | } 63 | } 64 | else { // electron-builder bundler 65 | if (![undefined, 'all', 'darwin', 'mac', 'win32', 'win', 'linux'].includes(argv.target)) { 66 | warn(`⚠️ Unknown target "${ argv.target }" for electron-builder`) 67 | warn() 68 | process.exit(1) 69 | } 70 | if (![undefined, 'ia32', 'x64', 'armv7l', 'arm64', 'all'].includes(argv.arch)) { 71 | warn(`⚠️ Unknown architecture "${ argv.arch }" for electron-builder`) 72 | warn() 73 | process.exit(1) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/webpack/inject.style-rules.js: -------------------------------------------------------------------------------- 1 | const ExtractLoader = require('mini-css-extract-plugin').loader 2 | 3 | const 4 | appPaths = require('../app-paths'), 5 | postCssConfig = require(appPaths.resolve.app('.postcssrc.js')) 6 | 7 | function injectRule ({ chain, pref }, lang, test, loader, options) { 8 | const baseRule = chain.module.rule(lang).test(test) 9 | 10 | // rules for