├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── assets └── logo.art ├── bin ├── quasar ├── quasar-build ├── quasar-clean ├── quasar-dev ├── quasar-help ├── quasar-info ├── quasar-init ├── quasar-mode ├── quasar-new └── quasar-serve ├── lib ├── app-paths.js ├── artifacts.js ├── cordova │ ├── cordova-config.js │ └── index.js ├── dev-server.js ├── electron │ ├── bundler.js │ └── index.js ├── generator.js ├── helpers │ ├── animations.js │ ├── banner.js │ ├── cli-error-handling.js │ ├── ensure-argv.js │ ├── ensure-deps.js │ ├── get-external-ip.js │ ├── is-minimal-terminal.js │ ├── logger.js │ ├── net.js │ ├── node-packager.js │ ├── on-shutdown.js │ └── spawn.js ├── legacy-validations.js ├── mode │ ├── index.js │ ├── install-missing.js │ ├── mode-cordova.js │ ├── mode-electron.js │ ├── mode-pwa.js │ └── mode-ssr.js ├── node-version-check.js ├── quasar-config.js ├── ssr │ └── html-template.js └── webpack │ ├── cordova │ └── index.js │ ├── create-chain.js │ ├── electron │ ├── main.js │ ├── plugin.electron-package-json.js │ └── renderer.js │ ├── index.js │ ├── inject.client-specifics.js │ ├── inject.hot-update.js │ ├── inject.html.js │ ├── inject.preload.js │ ├── inject.style-rules.js │ ├── plugin.html-addons.js │ ├── plugin.progress.js │ ├── pwa │ ├── index.js │ ├── plugin.html-pwa.js │ └── plugin.pwa-manifest.js │ ├── spa │ └── index.js │ └── ssr │ ├── client.js │ ├── plugin.ssr-prod-artifacts.js │ ├── server.js │ └── template.ssr.js ├── lists └── app-templates.json ├── package.json ├── templates ├── app │ ├── app.styl │ ├── babelrc │ ├── component.vue │ ├── layout.vue │ ├── page.vue │ ├── plugin.js │ ├── store │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ └── variables.styl ├── electron │ ├── icons │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── linux-512x512.png │ └── main-process │ │ ├── electron-main.dev.js │ │ └── electron-main.js ├── entry │ ├── app.js │ ├── client-entry.js │ ├── client-prefetch.js │ ├── import-quasar.js │ └── server-entry.js ├── pwa │ ├── custom-service-worker.js │ └── register-service-worker.js └── ssr │ ├── extension.js │ └── index.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Quasar Contributing Guide 2 | 3 | Hi! I’m really excited that you are interested in contributing to Quasar. Before submitting your contribution though, please make sure to take a moment and read through the following guidelines. 4 | 5 | - [Code of Conduct](https://github.com/quasarframework/quasar/blob/dev/.github/CODE_OF_CONDUCT.md) 6 | - [Issue Reporting Guidelines](#issue-reporting-guidelines) 7 | - [Pull Request Guidelines](#pull-request-guidelines) 8 | - [Development Setup](#development-setup) 9 | - [Project Structure](#project-structure) 10 | 11 | ## Issue Reporting Guidelines 12 | 13 | - The issue list of this repo is **exclusively** for bug reports and feature requests. Non-conforming issues will be closed immediately. 14 | 15 | - For simple beginner questions, you can get quick answers from the [Quasar Discord chat room](http://chat.quasar-framework.org). 16 | 17 | - For more complicated questions, you can use [the official forum](https://forum.quasar-framework.org/). Make sure to provide enough information when asking your questions - this makes it easier for others to help you! 18 | 19 | - Try to search for your issue, it may have already been answered or even fixed in the development branch (`dev`). 20 | 21 | - Check if the issue is reproducible with the latest stable version of Quasar. If you are using a pre-release, please indicate the specific version you are using. 22 | 23 | - It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable. 24 | 25 | - Use only the minimum amount of code necessary to reproduce the unexpected behavior. A good bug report should isolate specific methods that exhibit unexpected behavior and precisely define how expectations were violated. What did you expect the method or methods to do, and how did the observed behavior differ? The more precisely you isolate the issue, the faster we can investigate. 26 | 27 | - Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. 28 | 29 | - If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. 30 | 31 | - Most importantly, we beg your patience: the team must balance your request against many other responsibilities — fixing other bugs, answering other questions, new features, new documentation, etc. The issue list is not paid support and we cannot make guarantees about how fast your issue can be resolved. 32 | 33 | ## Pull Request Guidelines 34 | 35 | - The `master` branch is basically just a snapshot of the latest stable release. All development should be done in dedicated branches. **Do not submit PRs against the `master` branch.** 36 | 37 | - Checkout a topic branch from the relevant branch, e.g. `dev`, and merge back against that branch. 38 | 39 | - It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging. 40 | 41 | - If adding new feature: 42 | - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it green-lighted before working on it. 43 | 44 | - If fixing a bug: 45 | - If you are resolving a special issue, add `(fix: #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `fix: update entities encoding/decoding (fix #3899)`. 46 | - Provide detailed description of the bug in the PR. Live demo preferred. 47 | 48 | ## Development Setup 49 | 50 | You will need [Node.js](http://nodejs.org) **version 8.9+** along [Yarn](https://yarnpkg.com/) or [NPM](https://docs.npmjs.com/getting-started/installing-node). Read `package.json` and take notice of the scripts you can use. 51 | 52 | After cloning the repo, run: 53 | 54 | ``` bash 55 | $ yarn # or: npm install 56 | ``` 57 | 58 | ## Project Structure 59 | 60 | - **`bin`**: executables 61 | 62 | - **`quasar`**: entry point for CLI 63 | 64 | - **`quasar-*`**: entry point for CLI command 65 | 66 | - **`lib`**: core 67 | 68 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Thumbs.db 3 | node_modules/ 4 | ssl-server.pem 5 | npm-debug.log 6 | *.sublime* 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .github 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Quasar Framework logo](https://cdn.rawgit.com/quasarframework/quasar-art/863c14bd/dist/svg/quasar-logo-full-inline.svg) 2 | 3 | # Quasar CLI 4 | > CLI for Quasar Framework. Start a project, build it, optimize it. 5 | 6 | 7 | 8 | # Quasar Framework 9 | > Build responsive Single Page Apps, **SSR Apps**, PWAs, Hybrid Mobile Apps and Electron Apps, all using the same codebase!, powered with Vue. 10 | 11 | 12 | 13 | ## Supporting Quasar 14 | Quasar Framework is an MIT-licensed open source project. Its ongoing development is made possible thanks to the support by these awesome [backers](https://github.com/rstoenescu/quasar-framework/blob/dev/backers.md). If you'd like to join them, check out [Quasar Framework's Patreon campaign](https://www.patreon.com/quasarframework). 15 | 16 | ## Quickstart 17 | 18 | ### Installing 19 | 20 | `$ npm install -g quasar-cli` 21 | 22 | ### TODO Command overview 23 | 24 | Display list of commands: 25 | 26 | `$ quasar` 27 | 28 | - `init` - create app from template 29 | - `dev` - run dev server for your app 30 | - `build` - build for production 31 | - `clean` - clean build assets 32 | - `new` - create app assets (component, pages, layouts, store module) 33 | - `serve` - start a live reload HTTP server on a folder (like `/dist`) 34 | 35 | ### Help 36 | 37 | `$ quasar [command name] --help` 38 | 39 | 80 | 81 | ## Documentation 82 | 83 | Head on to the Quasar Framework official website for help on CLI commands: [http://quasar-framework.org](http://quasar-framework.org/guide/quasar-cli.html) 84 | 85 | ## Community Forum 86 | 87 | Head on to the official community forum: [http://forum.quasar-framework.org](http://forum.quasar-framework.org) 88 | 89 | ## Quasar Repositories 90 | 91 | * [Quasar Framework](https://github.com/rstoenescu/quasar-framework) 92 | * [Quasar CLI](https://github.com/rstoenescu/quasar-cli) 93 | * [Quasar Play App](https://github.com/rstoenescu/quasar-play) 94 | 95 | ## Contributing 96 | 97 | I'm excited if you want to contribute to Quasar under any form (report bugs, write a plugin, fix an issue, write a new feature). 98 | 99 | ### Issue Reporting Guidelines 100 | 101 | **Please use the appropriate Github repo to report issues. See "Related Components" above.** For example, a bug related to CLI should be reported to the CLI repo, one related to build issues to Quasar Framework Templates repo and so on. 102 | 103 | - The issue list of the repository is **exclusively** for bug reports and feature requests. For anything else please use the [Community Forum](http://forum.quasar-framework.org). 104 | 105 | - Try to search for your issue, it may have already been fixed in the development branch or it may have a resolution. 106 | 107 | - Check if the issue is reproducible with the latest stable version of Quasar. If you are using a pre-release, please indicate the specific version you are using. 108 | 109 | - It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. 110 | 111 | - If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. 112 | 113 | Read more [here](http://quasar-framework.org/guide/contributing.html). 114 | 115 | ## License 116 | 117 | Copyright (c) 2016-present Razvan Stoenescu 118 | 119 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) 120 | -------------------------------------------------------------------------------- /assets/logo.art: -------------------------------------------------------------------------------- 1 | ___ 2 | / _ \ _ _ __ _ ___ __ _ _ __ 3 | | | | | | | |/ _` / __|/ _` | '__| 4 | | |_| | |_| | (_| \__ \ (_| | | 5 | \__\_\\__,_|\__,_|___/\__,_|_| 6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/quasar-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const parseArgs = require('minimist') 4 | 5 | const argv = parseArgs(process.argv.slice(2), { 6 | alias: { 7 | t: 'theme', 8 | m: 'mode', 9 | T: 'target', 10 | A: 'arch', 11 | b: 'bundler', 12 | d: 'debug', 13 | h: 'help' 14 | }, 15 | boolean: ['h', 'd'], 16 | string: ['t', 'm', 'T'], 17 | default: { 18 | t: 'mat', 19 | m: 'spa' 20 | } 21 | }) 22 | 23 | if (argv.help) { 24 | console.log(` 25 | Description 26 | Builds distributables of your app. 27 | Usage 28 | $ quasar build -p 29 | Options 30 | --theme, -t App theme (default: mat) 31 | --mode, -m App mode [spa|ssr|pwa|cordova|electron] (default: spa) 32 | --target, -T App target 33 | - Cordova (default: all installed) 34 | [android|ios|blackberry10|browser|osx|ubuntu|webos|windows] 35 | - Electron with default "electron-packager" bundler (default: yours) 36 | [darwin|win32|linux|mas|all] 37 | - Electron with "electron-builder" bundler (default: yours) 38 | [darwin|mac|win32|win|linux|all] 39 | --debug, -d Build for debugging purposes 40 | --help, -h Displays this message 41 | 42 | ONLY for Electron mode: 43 | --bundler, -b Bundler (electron-packager or electron-builder) 44 | [packager|builder] 45 | --arch, -A App architecture (default: yours) 46 | - with default "electron-packager" bundler: 47 | [ia32|x64|armv7l|arm64|mips64el|all] 48 | - with "electron-builder" bundler: 49 | [ia32|x64|armv7l|arm64|all] 50 | `) 51 | process.exit(0) 52 | } 53 | 54 | const chalk = require('chalk') 55 | 56 | const 57 | logger = require('../lib/helpers/logger'), 58 | log = logger('app:build'), 59 | warn = logger('app:build', 'red'), 60 | banner = require('../lib/helpers/banner') 61 | 62 | require('../lib/helpers/ensure-argv')(argv, 'build') 63 | banner(argv, 'build') 64 | 65 | if (argv.mode !== 'spa') { 66 | require('../lib/mode/install-missing')(argv.mode, argv.target) 67 | } 68 | 69 | const 70 | path = require('path'), 71 | webpack = require('webpack') 72 | 73 | const 74 | QuasarConfig = require('../lib/quasar-config'), 75 | Generator = require('../lib/generator'), 76 | artifacts = require('../lib/artifacts'), 77 | ensureDeps = require('../lib/helpers/ensure-deps') 78 | 79 | function parseWebpackConfig (webpackConfig, mode) { 80 | if (mode === 'ssr') { 81 | return [ webpackConfig.server, webpackConfig.client ] 82 | } 83 | if (mode === 'electron') { 84 | return [ webpackConfig.renderer, webpackConfig.main ] 85 | } 86 | return webpackConfig 87 | } 88 | 89 | function finalize (mode, quasarConfig) { 90 | if (mode === 'cordova') { 91 | return require('../lib/cordova').build(quasarConfig) 92 | } 93 | if (mode === 'electron') { 94 | return require('../lib/electron').build(quasarConfig) 95 | } 96 | 97 | return Promise.resolve() 98 | } 99 | 100 | async function build () { 101 | const quasarConfig = new QuasarConfig({ 102 | theme: argv.theme, 103 | mode: argv.mode, 104 | target: argv.target, 105 | arch: argv.arch, 106 | bundler: argv.bundler, 107 | debug: argv.debug, 108 | prod: true 109 | }) 110 | 111 | try { 112 | await quasarConfig.prepare() 113 | } 114 | catch (e) { 115 | console.log(e) 116 | warn(`⚠️ [FAIL] quasar.conf.js has JS errors`) 117 | process.exit(1) 118 | } 119 | 120 | quasarConfig.compile() 121 | 122 | const 123 | generator = new Generator(quasarConfig), 124 | webpackConfig = quasarConfig.getWebpackConfig(), 125 | buildConfig = quasarConfig.getBuildConfig(), 126 | mode = argv.mode.toUpperCase(), 127 | outputFolder = buildConfig.build.packagedElectronDist || buildConfig.build.distDir 128 | 129 | artifacts.clean(outputFolder) 130 | generator.prepare() 131 | generator.build() 132 | 133 | log(chalk.bold(`Building...`)) 134 | 135 | webpack(parseWebpackConfig(webpackConfig, argv.mode), (err, stats) => { 136 | if (err) { throw err } 137 | 138 | artifacts.add(outputFolder) 139 | 140 | statsArray = stats.stats || [ stats ] 141 | statsArray.forEach(stat => { 142 | process.stdout.write('\n\n' + stat.toString({ 143 | colors: true, 144 | performance: false, 145 | hash: false, 146 | assets: true, 147 | chunks: false, 148 | chunkModules: false, 149 | chunkOrigins: false, 150 | modules: false, 151 | nestedModules: false, 152 | moduleAssets: false, 153 | children: false 154 | }) + '\n\n') 155 | }) 156 | 157 | statsArray.forEach(stat => { 158 | if (stat.hasErrors()) { 159 | warn() 160 | warn(chalk.red('[FAIL] Build failed with errors. Check log above.')) 161 | warn() 162 | 163 | process.exit(1) 164 | } 165 | }) 166 | 167 | finalize(argv.mode, quasarConfig).then(() => { 168 | banner(argv, 'build', { 169 | outputFolder: argv.mode === 'cordova' 170 | ? path.join(outputFolder, '..') 171 | : outputFolder 172 | }) 173 | }) 174 | }) 175 | } 176 | 177 | ensureDeps() 178 | build() 179 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/quasar-dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | parseArgs = require('minimist'), 5 | chalk = require('chalk') 6 | 7 | const 8 | logger = require('../lib/helpers/logger'), 9 | log = logger('app:dev'), 10 | warn = logger('app:dev', 'red'), 11 | appPaths = require('../lib/app-paths') 12 | 13 | const argv = parseArgs(process.argv.slice(2), { 14 | alias: { 15 | t: 'theme', 16 | m: 'mode', 17 | T: 'target', // cordova-mode only 18 | e: 'emulator', // cordova-mode only 19 | p: 'port', 20 | H: 'hostname', 21 | h: 'help' 22 | }, 23 | boolean: ['h'], 24 | string: ['t', 'm', 'T', 'H'], 25 | default: { 26 | t: 'mat', 27 | m: 'spa' 28 | } 29 | }) 30 | 31 | if (argv.help) { 32 | console.log(` 33 | Description 34 | Starts the app in development mode (hot-code reloading, error 35 | reporting, etc) 36 | Usage 37 | $ quasar dev -p 38 | Options 39 | --theme, -t App theme (default: mat) 40 | --mode, -m App mode [spa|ssr|pwa|cordova|electron] (default: spa) 41 | --port, -p A port number on which to start the application 42 | --hostname, -H A hostname to use for serving the application 43 | --help, -h Displays this message 44 | 45 | Only for Cordova mode: 46 | --target, -T (required) App target 47 | [android|ios|blackberry10|browser|osx|ubuntu|webos|windows] 48 | --emulator, -e (optional) Emulator name 49 | Example: iPhone-7, iPhone-X 50 | `) 51 | process.exit(0) 52 | } 53 | 54 | require('../lib/helpers/ensure-argv')(argv, 'dev') 55 | require('../lib/helpers/banner')(argv, 'dev') 56 | 57 | if (argv.mode !== 'spa') { 58 | require('../lib/mode/install-missing')(argv.mode, argv.target) 59 | } 60 | 61 | const 62 | ensureDeps = require('../lib/helpers/ensure-deps'), 63 | findPort = require('../lib/helpers/net').findClosestOpenPort 64 | 65 | async function parseAddress ({ host, port }) { 66 | if (this.chosenHost) { 67 | host = this.chosenHost 68 | } 69 | else { 70 | if (host && ['localhost', '127.0.0.1', '::1'].includes(host.toLowerCase())) { 71 | host = '0.0.0.0' 72 | } 73 | if (argv.mode === 'cordova' && (!host || host === '0.0.0.0')) { 74 | const getExternalIP = require('../lib/helpers/get-external-ip') 75 | host = await getExternalIP() 76 | this.chosenHost = host 77 | } 78 | } 79 | 80 | log(`Checking listening address availability (${host}:${port})...`) 81 | 82 | try { 83 | const openPort = await findPort(port, host) 84 | if (port !== openPort) { 85 | warn() 86 | warn(`Setting port to closest one available: ${openPort}`) 87 | warn() 88 | 89 | port = openPort 90 | } 91 | } 92 | catch (e) { 93 | warn() 94 | 95 | if (e.message === 'ERROR_NETWORK_PORT_NOT_AVAIL') { 96 | warn(`⚠️ Could not find an open port. Please configure a lower one to start searching with.`) 97 | } 98 | else if (e.message === 'ERROR_NETWORK_ADDRESS_NOT_AVAIL') { 99 | warn(`⚠️ Invalid host specified. No network address matches. Please specify another one.`) 100 | } 101 | else { 102 | warn(`⚠️ Unknown network error occured`) 103 | console.log(e) 104 | } 105 | 106 | warn() 107 | 108 | if (!this.running) { 109 | process.exit(1) 110 | } 111 | 112 | return null 113 | } 114 | 115 | this.running = true 116 | return { host, port } 117 | } 118 | 119 | async function goLive () { 120 | ensureDeps() 121 | 122 | const 123 | DevServer = require('../lib/dev-server'), 124 | QuasarConfig = require('../lib/quasar-config'), 125 | Generator = require('../lib/generator') 126 | 127 | const 128 | quasarConfig = new QuasarConfig({ 129 | theme: argv.theme, 130 | mode: argv.mode, 131 | target: argv.target, 132 | emulator: argv.emulator, 133 | port: argv.port, 134 | host: argv.hostname, 135 | dev: true, 136 | onAddress: parseAddress, 137 | onBuildChange () { 138 | log(`Build changes detected. Rebuilding app...`) 139 | dev = dev.then(startDev) 140 | }, 141 | onAppChange () { 142 | log(`App changes detected. Updating app...`) 143 | generator.build() 144 | } 145 | }) 146 | 147 | try { 148 | await quasarConfig.prepare() 149 | } 150 | catch (e) { 151 | console.log(e) 152 | warn(`[FAIL] quasar.conf.js has JS errors`) 153 | process.exit(1) 154 | } 155 | 156 | quasarConfig.compile() 157 | 158 | const 159 | generator = new Generator(quasarConfig), 160 | Cordova = argv.mode === 'cordova' ? require('../lib/cordova') : false, 161 | Electron = argv.mode === 'electron' ? require('../lib/electron') : false 162 | 163 | generator.prepare() 164 | 165 | function startDev (oldDevServer) { 166 | let devServer 167 | 168 | return Promise.resolve() 169 | .then(() => devServer = new DevServer(quasarConfig)) 170 | .then(() => oldDevServer && oldDevServer.stop()) 171 | .then(() => generator.build()) // Update generated files 172 | .then(() => devServer.listen()) // Start listening 173 | .then(() => Electron && Electron.run(quasarConfig)) 174 | .then(() => Cordova && Cordova.run(quasarConfig)) 175 | .then(() => devServer) // Pass new builder to watch chain 176 | } 177 | 178 | let dev = startDev() 179 | } 180 | 181 | goLive() 182 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/quasar-info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | parseArgs = require('minimist'), 5 | chalk = require('chalk') 6 | 7 | const 8 | appPaths = require('../lib/app-paths') 9 | 10 | const argv = parseArgs(process.argv.slice(2), { 11 | alias: { 12 | h: 'help' 13 | }, 14 | boolean: ['h'] 15 | }) 16 | 17 | if (argv.help) { 18 | console.log(` 19 | Description 20 | Displays information about your machine and your Quasar App. 21 | Usage 22 | $ quasar info 23 | Options 24 | --help, -h Displays this message 25 | `) 26 | process.exit(0) 27 | } 28 | 29 | const 30 | os = require('os'), 31 | spawn = require('cross-spawn').sync 32 | 33 | function getSpawnOutput (command) { 34 | try { 35 | const child = spawn(command, ['--version']) 36 | return child.status === 0 37 | ? chalk.green(String(child.output[1]).trim()) 38 | : chalk.red('Not installed') 39 | } 40 | catch (err) { 41 | return chalk.red('Not installed') 42 | } 43 | } 44 | 45 | function safePkgInfo (pkg) { 46 | try { 47 | const content = require(appPaths.resolve.app(`node_modules/${pkg}/package.json`)) 48 | return { 49 | key: ` ${String(content.name).trim()}`, 50 | value: `${chalk.green(String(content.version).trim())}${content.description ? `\t(${content.description})` : ''}` 51 | } 52 | } 53 | catch (err) { 54 | return { 55 | key: ` ${pkg}`, 56 | value: chalk.red('Not installed') 57 | } 58 | } 59 | } 60 | 61 | const 62 | getExternalIPs = require('../lib/helpers/net').getExternalNetworkInterface, 63 | output = [ 64 | { key: 'Operating System', value: chalk.green(`${os.type()}(${os.release()}) - ${os.platform()}/${os.arch()}`), section: true }, 65 | { key: 'NodeJs', value: chalk.green(process.version.slice(1)) }, 66 | { key: 'Global packages', section: true }, 67 | { key: ' NPM', value: getSpawnOutput('npm') }, 68 | { key: ' yarn', value: getSpawnOutput('yarn') }, 69 | { key: ' quasar-cli', value: getSpawnOutput('quasar') }, 70 | { key: ' vue-cli', value: getSpawnOutput('vue') }, 71 | { key: ' cordova', value: getSpawnOutput('cordova') }, 72 | { key: 'Important local packages', section: true } 73 | ] 74 | 75 | ;[ 76 | 'quasar-cli', 77 | 'quasar-framework', 78 | 'quasar-extras', 79 | 'vue', 80 | 'vue-router', 81 | 'vuex', 82 | 'electron', 83 | 'electron-packager', 84 | 'electron-builder', 85 | '@babel/core', 86 | 'webpack', 87 | 'webpack-dev-server', 88 | 'workbox-webpack-plugin', 89 | 'register-service-worker' 90 | ].forEach(pkg => output.push(safePkgInfo(pkg))) 91 | 92 | output.push( 93 | { key: 'Networking', section: true }, 94 | { key: ' Host', value: chalk.green(os.hostname()) } 95 | ) 96 | getExternalIPs().forEach(intf => { 97 | output.push({ 98 | key: ` ${ intf.deviceName }`, 99 | value: chalk.green(intf.address) 100 | }) 101 | }) 102 | 103 | const spaces = output.reduce((acc, v) => Math.max(acc, v.key.length), 0) 104 | console.log( 105 | output 106 | .map(m => `${m.section ? '\n' : ''}${ m.key }${' '.repeat(spaces - m.key.length)}\t${ m.value === undefined ? '' : m.value }`).join('\n') 107 | ) 108 | console.log() 109 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/quasar-new: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const parseArgs = require('minimist') 4 | 5 | const argv = parseArgs(process.argv.slice(2), { 6 | alias: { 7 | h: 'help' 8 | }, 9 | boolean: ['h'] 10 | }) 11 | 12 | function showHelp () { 13 | console.log(` 14 | Description 15 | Quickly scaffold a page/layout/component/store module. 16 | 17 | Usage 18 | $ quasar new [p|page] 19 | $ quasar new [l|layout] 20 | $ quasar new [c|component] 21 | $ quasar new plugin 22 | $ quasar new [s|store] 23 | 24 | # Examples: 25 | 26 | # Create src/pages/MyNewPage.vue: 27 | $ quasar new p MyNewPage 28 | 29 | # Create src/pages/MyNewPage.vue and src/pages/OtherPage.vue: 30 | $ quasar new p MyNewPage OtherPage 31 | 32 | # Create src/layouts/shop/Checkout.vue 33 | $ quasar new layout shop/Checkout.vue 34 | 35 | Options 36 | --help, -h Displays this message 37 | `) 38 | process.exit(0) 39 | } 40 | 41 | if (argv.help) { 42 | showHelp() 43 | } 44 | 45 | const 46 | logger = require('../lib/helpers/logger'), 47 | log = logger('app:new'), 48 | warn = logger('app:new', 'red'), 49 | appPaths = require('../lib/app-paths'), 50 | path = require('path'), 51 | fs = require('fs'), 52 | fse = require('fs-extra') 53 | 54 | if (argv._.length < 2) { 55 | console.log() 56 | warn(`⚠️ Wrong number of parameters (${argv._.length}).`) 57 | showHelp() 58 | process.exit(1) 59 | } 60 | 61 | let [ type, ...names ] = argv._ 62 | 63 | if (!['p', 'page', 'l', 'layout', 'c', 'component', 's', 'store', 'plugin'].includes(type)) { 64 | console.log() 65 | warn(`⚠️ Invalid asset type: ${type}`) 66 | showHelp() 67 | } 68 | 69 | if (type.length === 1) { 70 | const fullCmd = { 71 | p: 'page', 72 | l: 'layout', 73 | c: 'component', 74 | s: 'store' 75 | } 76 | type = fullCmd[type] 77 | } 78 | 79 | function getPaths (asset, names) { 80 | return names.map(name => { 81 | const 82 | hasExtension = !asset.ext || (asset.ext && name.endsWith(asset.ext)), 83 | ext = hasExtension ? '' : asset.ext 84 | 85 | return appPaths.resolve.src(path.join(asset.folder, name + ext)) 86 | }) 87 | } 88 | 89 | function createFile (asset, file) { 90 | const relativePath = path.relative(appPaths.appDir, file) 91 | 92 | if (fs.existsSync(file)) { 93 | warn(`[SKIPPED] ${relativePath} already exists.`) 94 | console.log() 95 | return 96 | } 97 | 98 | fse.mkdirp(path.dirname(file)) 99 | fse.copy( 100 | appPaths.resolve.cli(path.join('templates/app', type + (asset.ext || ''))), 101 | file, 102 | err => { 103 | 104 | if (err) { 105 | console.warn(err) 106 | warn('[FAIL] Could not generate ${relativePath}.') 107 | return 108 | } 109 | 110 | log(`Generated ${type}: ${relativePath}`) 111 | if (asset.reference) { 112 | log(`Make sure to reference it in ${asset.reference}`) 113 | } 114 | log() 115 | } 116 | ) 117 | } 118 | 119 | const 120 | mapping = { 121 | page: { 122 | folder: 'pages', 123 | ext: '.vue', 124 | reference: 'src/router/routes.js' 125 | }, 126 | layout: { 127 | folder: 'layouts', 128 | ext: '.vue', 129 | reference: 'src/router/routes.js' 130 | }, 131 | component: { 132 | folder: 'components', 133 | ext: '.vue' 134 | }, 135 | store: { 136 | folder: 'store', 137 | reference: 'src/store/index.js' 138 | }, 139 | plugin: { 140 | folder: 'plugins', 141 | ext: '.js', 142 | reference: 'quasar.conf.js > plugins' 143 | } 144 | }, 145 | asset = mapping[type] 146 | 147 | const filesToCreate = getPaths(asset, names) 148 | 149 | filesToCreate.forEach(file => { 150 | createFile(asset, file) 151 | }) 152 | -------------------------------------------------------------------------------- /bin/quasar-serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const parseArgs = require('minimist') 4 | 5 | const argv = parseArgs(process.argv.slice(2), { 6 | alias: { 7 | p: 'port', 8 | H: 'hostname', 9 | g: 'gzip', 10 | s: 'silent', 11 | colors: 'colors', 12 | o: 'open', 13 | c: 'cache', 14 | m: 'micro', 15 | history: 'history', 16 | https: 'https', 17 | C: 'cert', 18 | K: 'key', 19 | P: 'proxy', 20 | h: 'help' 21 | }, 22 | boolean: ['g', 'https', 'colors', 'S', 'history', 'h'], 23 | string: ['H', 'C', 'K'], 24 | default: { 25 | p: process.env.PORT || 4000, 26 | H: process.env.HOSTNAME || '0.0.0.0', 27 | g: true, 28 | c: 24 * 60 * 60, 29 | m: 1, 30 | colors: true 31 | } 32 | }) 33 | 34 | if (argv.help) { 35 | console.log(` 36 | Description 37 | Start a HTTP(S) server on a folder. 38 | 39 | Usage 40 | $ quasar serve [path] 41 | $ quasar serve . # serve current folder 42 | 43 | If you serve a SSR folder built with the CLI then 44 | control is yielded to /index.js and params have no effect. 45 | 46 | Options 47 | --port, -p Port to use (default: 4000) 48 | --hostname, -H Address to use (default: 0.0.0.0) 49 | --gzip, -g Compress content (default: true) 50 | --silent, -s Supress log message 51 | --colors Log messages with colors (default: true) 52 | --open, -o Open browser window after starting 53 | --cache, -c Cache time (max-age) in seconds; 54 | Does not apply to /service-worker.js 55 | (default: 86400 - 24 hours) 56 | --micro, -m Use micro-cache (default: 1 second) 57 | --history Use history api fallback; 58 | All requests fallback to index.html 59 | --https Enable HTTPS 60 | --cert, -C [path] Path to SSL cert file (Optional) 61 | --key, -K [path] Path to SSL key file (Optional) 62 | --proxy Proxy specific requests defined in file; 63 | File must export Array ({ path, rule }) 64 | See example below. "rule" is defined at: 65 | https://github.com/chimurai/http-proxy-middleware 66 | --help, -h Displays this message 67 | 68 | Proxy file example 69 | module.exports = [ 70 | { 71 | path: '/api', 72 | rule: { target: 'http://www.example.org' } 73 | } 74 | ] 75 | --> will be transformed into app.use(path, httpProxyMiddleware(rule)) 76 | `) 77 | process.exit(0) 78 | } 79 | 80 | const 81 | fs = require('fs'), 82 | path = require('path') 83 | 84 | const root = getAbsolutePath(argv._[0] || '.') 85 | const resolve = p => path.resolve(root, p) 86 | 87 | function getAbsolutePath (pathParam) { 88 | return path.isAbsolute(pathParam) 89 | ? pathParam 90 | : path.join(process.cwd(), pathParam) 91 | } 92 | 93 | const 94 | pkgFile = resolve('package.json'), 95 | indexFile = resolve('index.js') 96 | 97 | let ssrDetected = false 98 | 99 | if (fs.existsSync(pkgFile) && fs.existsSync(indexFile)) { 100 | const pkg = require(pkgFile) 101 | if (pkg.quasar && pkg.quasar.ssr) { 102 | console.log('Quasar SSR folder detected.') 103 | console.log('Yielding control to its own webserver.') 104 | console.log() 105 | ssrDetected = true 106 | require(indexFile) 107 | } 108 | } 109 | 110 | if (ssrDetected === false) { 111 | let green, grey, red 112 | 113 | if (argv.colors) { 114 | const chalk = require('chalk') 115 | green = chalk.green 116 | grey = chalk.grey 117 | red = chalk.red 118 | } 119 | else { 120 | green = grey = red = text => text 121 | } 122 | 123 | const 124 | express = require('express'), 125 | microCacheSeconds = argv.micro 126 | ? parseInt(argv.micro, 10) 127 | : false 128 | 129 | function serve (path, cache) { 130 | return express.static(resolve(path), { 131 | maxAge: cache ? parseInt(argv.cache, 10) * 1000 : 0 132 | }) 133 | } 134 | 135 | const app = express() 136 | 137 | if (!argv.silent) { 138 | app.get('*', (req, res, next) => { 139 | console.log( 140 | `GET ${green(req.url)} ${grey('[' + req.ip + ']')} ${new Date()}` 141 | ) 142 | next() 143 | }) 144 | } 145 | 146 | if (argv.gzip) { 147 | const compression = require('compression') 148 | app.use(compression({ threshold: 0 })) 149 | } 150 | 151 | const serviceWorkerFile = resolve('service-worker.js') 152 | if (fs.existsSync(serviceWorkerFile)) { 153 | app.use('/service-worker.js', serve('service-worker.js')) 154 | } 155 | 156 | if (argv.history) { 157 | const history = require('connect-history-api-fallback') 158 | app.use(history()) 159 | } 160 | 161 | app.use('/', serve('.', true)) 162 | 163 | if (microCacheSeconds) { 164 | const microcache = require('route-cache') 165 | app.use( 166 | microcache.cacheSeconds( 167 | microCacheSeconds, 168 | req => req.originalUrl 169 | ) 170 | ) 171 | } 172 | 173 | if (argv.proxy) { 174 | let file = argv.proxy = getAbsolutePath(argv.proxy) 175 | if (!fs.existsSync(file)) { 176 | console.error('Proxy definition file not found! ' + file) 177 | process.exit(1) 178 | } 179 | file = require(file) 180 | 181 | const proxy = require('http-proxy-middleware') 182 | file.forEach(entry => { 183 | app.use(entry.path, proxy(entry.rule)) 184 | }) 185 | } 186 | 187 | app.get('*', (req, res) => { 188 | res.setHeader('Content-Type', 'text/html') 189 | res.status(404).send('404 | Page Not Found') 190 | if (!argv.silent) { 191 | console.log(red(` 404 on ${req.url}`)) 192 | } 193 | }) 194 | 195 | function getHostname (host) { 196 | return host === '0.0.0.0' 197 | ? 'localhost' 198 | : host 199 | } 200 | 201 | getServer(app).listen(argv.port, argv.hostname, () => { 202 | const 203 | url = `http${argv.https ? 's' : ''}://${getHostname(argv.hostname)}:${argv.port}`, 204 | { version } = require('../package.json') 205 | 206 | const info = [ 207 | ['Quasar CLI', `v${version}`], 208 | ['Listening at', url], 209 | ['Web server root', root], 210 | argv.https ? ['HTTPS', 'enabled'] : '', 211 | argv.gzip ? ['Gzip', 'enabled'] : '', 212 | ['Cache (max-age)', argv.cache || 'disabled'], 213 | microCacheSeconds ? ['Micro-cache', microCacheSeconds + 's'] : '', 214 | argv.history ? ['History mode', 'enabled'] : '', 215 | argv.proxy ? ['Proxy definitions', argv.proxy] : '' 216 | ] 217 | .filter(msg => msg) 218 | .map(msg => ' ' + msg[0].padEnd(20, '.') + ' ' + green(msg[1])) 219 | 220 | console.log('\n' + info.join('\n') + '\n') 221 | 222 | if (argv.open) { 223 | const isMinimalTerminal = require('../lib/helpers/is-minimal-terminal') 224 | if (!isMinimalTerminal) { 225 | const opn = require('opn') 226 | opn(url) 227 | } 228 | } 229 | }) 230 | 231 | function getServer (app) { 232 | if (!argv.https) { 233 | return app 234 | } 235 | 236 | let fakeCert, key, cert 237 | 238 | if (argv.key && argv.cert) { 239 | key = getAbsolutePath(argv.key) 240 | cert = getAbsolutePath(argv.cert) 241 | 242 | if (fs.existsSync(key)) { 243 | key = fs.readFileSync(key) 244 | } 245 | else { 246 | console.error('SSL key file not found!' + key) 247 | process.exit(1) 248 | } 249 | 250 | if (fs.existsSync(cert)) { 251 | cert = fs.readFileSync(cert) 252 | } 253 | else { 254 | console.error('SSL cert file not found!' + cert) 255 | process.exit(1) 256 | } 257 | } 258 | else { 259 | // Use a self-signed certificate if no certificate was configured. 260 | // Cycle certs every 24 hours 261 | const certPath = path.join(__dirname, '../ssl-server.pem') 262 | let certExists = fs.existsSync(certPath) 263 | 264 | if (certExists) { 265 | const certStat = fs.statSync(certPath) 266 | const certTtl = 1000 * 60 * 60 * 24 267 | const now = new Date() 268 | 269 | // cert is more than 30 days old 270 | if ((now - certStat.ctime) / certTtl > 30) { 271 | console.log(' SSL Certificate is more than 30 days old. Removing.') 272 | const { removeSync } = require('fs-extra') 273 | removeSync(certPath) 274 | certExists = false 275 | } 276 | } 277 | 278 | if (!certExists) { 279 | console.log(' Generating self signed SSL Certificate...') 280 | console.log(' DO NOT use this self-signed certificate in production!') 281 | 282 | const selfsigned = require('selfsigned') 283 | const pems = selfsigned.generate( 284 | [{ name: 'commonName', value: 'localhost' }], 285 | { 286 | algorithm: 'sha256', 287 | days: 30, 288 | keySize: 2048, 289 | extensions: [{ 290 | name: 'basicConstraints', 291 | cA: true 292 | }, { 293 | name: 'keyUsage', 294 | keyCertSign: true, 295 | digitalSignature: true, 296 | nonRepudiation: true, 297 | keyEncipherment: true, 298 | dataEncipherment: true 299 | }, { 300 | name: 'subjectAltName', 301 | altNames: [ 302 | { 303 | // type 2 is DNS 304 | type: 2, 305 | value: 'localhost' 306 | }, 307 | { 308 | type: 2, 309 | value: 'localhost.localdomain' 310 | }, 311 | { 312 | type: 2, 313 | value: 'lvh.me' 314 | }, 315 | { 316 | type: 2, 317 | value: '*.lvh.me' 318 | }, 319 | { 320 | type: 2, 321 | value: '[::1]' 322 | }, 323 | { 324 | // type 7 is IP 325 | type: 7, 326 | ip: '127.0.0.1' 327 | }, 328 | { 329 | type: 7, 330 | ip: 'fe80::1' 331 | } 332 | ] 333 | }] 334 | } 335 | ) 336 | 337 | try { 338 | fs.writeFileSync(certPath, pems.private + pems.cert, { encoding: 'utf-8' }) 339 | } 340 | catch (err) { 341 | console.error(' Cannot write certificate file ' + certPath) 342 | console.error(' Aborting...') 343 | process.exit(1) 344 | } 345 | } 346 | 347 | fakeCert = fs.readFileSync(certPath) 348 | } 349 | 350 | return require('https').createServer({ 351 | key: key || fakeCert, 352 | cert: cert || fakeCert 353 | }, app) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/cordova/cordova-config.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | et = require('elementtree') 4 | 5 | const 6 | appPaths = require('../app-paths'), 7 | logger = require('../helpers/logger'), 8 | log = logger('app:cordova-conf') 9 | warn = logger('app:cordova-conf', 'red') 10 | 11 | const filePath = appPaths.resolve.cordova('config.xml') 12 | 13 | function setFields (root, cfg) { 14 | Object.keys(cfg).forEach(key => { 15 | const 16 | el = root.find(key), 17 | values = cfg[key], 18 | isObject = Object(values) === values 19 | 20 | if (!el) { 21 | if (isObject) { 22 | et.SubElement(root, key, values) 23 | } 24 | else { 25 | let entry = et.SubElement(root, key) 26 | entry.text = values 27 | } 28 | } 29 | else { 30 | if (isObject) { 31 | Object.keys(values).forEach(key => { 32 | el.set(key, values[key]) 33 | }) 34 | } 35 | else { 36 | el.text = values 37 | } 38 | } 39 | }) 40 | } 41 | 42 | class CordovaConfig { 43 | prepare (cfg) { 44 | this.doc = et.parse(fs.readFileSync(filePath, 'utf-8')) 45 | this.pkg = require(appPaths.resolve.app('package.json')) 46 | this.APP_URL = cfg.build.APP_URL 47 | 48 | const root = this.doc.getroot() 49 | 50 | root.set('id', cfg.cordova.id || this.pkg.cordovaId || 'org.quasar.cordova.app') 51 | root.set('version', cfg.cordova.version || this.pkg.version) 52 | 53 | if (cfg.cordova.androidVersionCode) { 54 | root.set('android-versionCode', cfg.cordova.androidVersionCode) 55 | } 56 | 57 | setFields(root, { 58 | content: { src: this.APP_URL }, 59 | description: cfg.cordova.description || this.pkg.description 60 | }) 61 | 62 | if (this.APP_URL !== 'index.html' && !root.find(`allow-navigation[@href='${this.APP_URL}']`)) { 63 | et.SubElement(root, 'allow-navigation', { href: this.APP_URL }) 64 | 65 | if (cfg.devServer.https && cfg.ctx.targetName === 'ios') { 66 | const node = root.find('name') 67 | if (node) { 68 | const filePath = appPaths.resolve.cordova( 69 | `platforms/ios/${node.text}/Classes/AppDelegate.m` 70 | ) 71 | 72 | if (!fs.existsSync(filePath)) { 73 | warn() 74 | warn() 75 | warn() 76 | warn() 77 | warn(`AppDelegate.m not found. Your App will revoke the devserver's SSL certificate.`) 78 | warn(`Please report the cordova CLI version and cordova-ios package that you are using.`) 79 | warn(`Also, disable HTTPS from quasar.conf.js > devServer > https`) 80 | warn() 81 | warn() 82 | warn() 83 | warn() 84 | } 85 | else { 86 | this.iosDelegateFilePath = filePath 87 | this.iosDelegateOriginal = fs.readFileSync(this.iosDelegateFilePath, 'utf-8') 88 | 89 | // required for allowing devserver's SSL certificate on iOS 90 | if (this.iosDelegateOriginal.indexOf('allowsAnyHTTPSCertificateForHost') === -1) { 91 | this.iosDelegateNew = this.iosDelegateOriginal + ` 92 | 93 | @implementation NSURLRequest(DataController) 94 | + (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host 95 | { 96 | return YES; 97 | } 98 | @end 99 | ` 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | this.__save() 107 | } 108 | 109 | reset () { 110 | if (!this.APP_URL || this.APP_URL === 'index.html') { 111 | return 112 | } 113 | 114 | const root = this.doc.getroot() 115 | 116 | root.find('content').set('src', 'index.html') 117 | 118 | const nav = root.find(`allow-navigation[@href='${this.APP_URL}']`) 119 | if (nav) { 120 | root.remove(nav) 121 | } 122 | 123 | if (this.iosDelegateOriginal && this.iosDelegateNew) { 124 | this.iosDelegateNew = this.iosDelegateOriginal 125 | } 126 | 127 | this.__save() 128 | } 129 | 130 | __save () { 131 | const content = this.doc.write({ indent: 4 }) 132 | fs.writeFileSync(filePath, content, 'utf8') 133 | log('Updated Cordova config.xml') 134 | 135 | if (this.iosDelegateFilePath && this.iosDelegateNew) { 136 | fs.writeFileSync(this.iosDelegateFilePath, this.iosDelegateNew, 'utf8') 137 | log('Updated AppDelegate.m') 138 | } 139 | } 140 | } 141 | 142 | module.exports = CordovaConfig 143 | -------------------------------------------------------------------------------- /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/dev-server.js: -------------------------------------------------------------------------------- 1 | const 2 | webpack = require('webpack'), 3 | WebpackDevServer = require('webpack-dev-server') 4 | 5 | const 6 | appPaths = require('./app-paths'), 7 | log = require('./helpers/logger')('app:dev-server') 8 | 9 | let alreadyNotified = false 10 | 11 | function openBrowser (url) { 12 | const opn = require('opn') 13 | opn(url) 14 | } 15 | 16 | module.exports = class DevServer { 17 | constructor (quasarConfig) { 18 | this.quasarConfig = quasarConfig 19 | } 20 | 21 | async listen () { 22 | const 23 | webpackConfig = this.quasarConfig.getWebpackConfig(), 24 | cfg = this.quasarConfig.getBuildConfig() 25 | 26 | log(`Booting up...`) 27 | log() 28 | 29 | return new Promise(resolve => ( 30 | cfg.ctx.mode.ssr 31 | ? this.listenSSR(webpackConfig, cfg, resolve) 32 | : this.listenCSR(webpackConfig, cfg, resolve) 33 | )) 34 | } 35 | 36 | listenCSR (webpackConfig, cfg, resolve) { 37 | const compiler = webpack(webpackConfig.renderer || webpackConfig) 38 | 39 | compiler.hooks.done.tap('done-compiling', compiler => { 40 | if (this.__started) { return } 41 | 42 | // start dev server if there are no errors 43 | if (compiler.compilation.errors && compiler.compilation.errors.length > 0) { 44 | return 45 | } 46 | 47 | this.__started = true 48 | 49 | server.listen(cfg.devServer.port, cfg.devServer.host, () => { 50 | resolve() 51 | 52 | if (alreadyNotified) { return } 53 | alreadyNotified = true 54 | 55 | if (cfg.devServer.open && ['spa', 'pwa'].includes(cfg.ctx.modeName)) { 56 | openBrowser(cfg.build.APP_URL) 57 | } 58 | }) 59 | }) 60 | 61 | // start building & launch server 62 | const server = new WebpackDevServer(compiler, cfg.devServer) 63 | 64 | this.__cleanup = () => { 65 | this.__cleanup = null 66 | return new Promise(resolve => { 67 | server.close(resolve) 68 | }) 69 | } 70 | } 71 | 72 | listenSSR (webpackConfig, cfg, resolve) { 73 | const 74 | fs = require('fs'), 75 | LRU = require('lru-cache'), 76 | express = require('express'), 77 | chokidar = require('chokidar'), 78 | { createBundleRenderer } = require('vue-server-renderer'), 79 | ouchInstance = require('./helpers/cli-error-handling').getOuchInstance() 80 | 81 | let renderer 82 | 83 | function createRenderer (bundle, options) { 84 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 85 | return createBundleRenderer(bundle, Object.assign(options, { 86 | // for component caching 87 | cache: LRU({ 88 | max: 1000, 89 | maxAge: 1000 * 60 * 15 90 | }), 91 | // recommended for performance 92 | runInNewContext: false 93 | })) 94 | } 95 | 96 | function render (req, res) { 97 | const startTime = Date.now() 98 | 99 | res.setHeader('Content-Type', 'text/html') 100 | 101 | const handleError = err => { 102 | if (err.url) { 103 | res.redirect(err.url) 104 | } 105 | else if (err.code === 404) { 106 | res.status(404).send('404 | Page Not Found') 107 | } 108 | else { 109 | ouchInstance.handleException(err, req, res, output => { 110 | console.error(`${req.url} -> error during render`) 111 | console.error(err.stack) 112 | }) 113 | } 114 | } 115 | 116 | const context = { 117 | url: req.url, 118 | req, 119 | res 120 | } 121 | 122 | renderer.renderToString(context, (err, html) => { 123 | if (err) { 124 | handleError(err) 125 | return 126 | } 127 | if (cfg.__meta) { 128 | html = context.$getMetaHTML(html) 129 | } 130 | console.log(`${req.url} -> request took: ${Date.now() - startTime}ms`) 131 | res.send(html) 132 | }) 133 | } 134 | 135 | let 136 | bundle, 137 | template, 138 | clientManifest, 139 | pwa 140 | 141 | let ready 142 | const readyPromise = new Promise(r => { ready = r }) 143 | function update () { 144 | if (bundle && clientManifest) { 145 | renderer = createRenderer(bundle, { 146 | template, 147 | clientManifest, 148 | basedir: appPaths.resolve.app('.') 149 | }) 150 | ready() 151 | } 152 | } 153 | 154 | // read template from disk and watch 155 | const 156 | { getIndexHtml } = require('./ssr/html-template'), 157 | templatePath = appPaths.resolve.app(cfg.sourceFiles.indexHtmlTemplate) 158 | 159 | function getTemplate () { 160 | return getIndexHtml(fs.readFileSync(templatePath, 'utf-8'), cfg) 161 | } 162 | 163 | template = getTemplate() 164 | const htmlWatcher = chokidar.watch(templatePath).on('change', () => { 165 | template = getTemplate() 166 | console.log('index.template.html template updated.') 167 | update() 168 | }) 169 | 170 | const 171 | serverCompiler = webpack(webpackConfig.server), 172 | clientCompiler = webpack(webpackConfig.client) 173 | 174 | serverCompiler.hooks.done.tapAsync('done-compiling', ({ compilation: { errors, warnings, assets }}, cb) => { 175 | errors.forEach(err => console.error(err)) 176 | warnings.forEach(err => console.warn(err)) 177 | 178 | if (errors.length > 0) { 179 | cb() 180 | return 181 | } 182 | 183 | bundle = JSON.parse(assets['../vue-ssr-server-bundle.json'].source()) 184 | update() 185 | 186 | cb() 187 | }) 188 | 189 | clientCompiler.hooks.done.tapAsync('done-compiling', ({ compilation: { errors, warnings, assets }}, cb) => { 190 | errors.forEach(err => console.error(err)) 191 | warnings.forEach(err => console.warn(err)) 192 | 193 | if (errors.length > 0) { 194 | cb() 195 | return 196 | } 197 | 198 | if (cfg.ctx.mode.pwa) { 199 | pwa = { 200 | manifest: assets['manifest.json'].source(), 201 | serviceWorker: assets['service-worker.js'].source() 202 | } 203 | } 204 | 205 | clientManifest = JSON.parse(assets['../vue-ssr-client-manifest.json'].source()) 206 | update() 207 | 208 | cb() 209 | }) 210 | 211 | const serverCompilerWatcher = serverCompiler.watch({}, () => {}) 212 | 213 | // start building & launch server 214 | const server = new WebpackDevServer(clientCompiler, Object.assign( 215 | { 216 | after: app => { 217 | if (cfg.ctx.mode.pwa) { 218 | app.use('/manifest.json', (req, res) => { 219 | res.setHeader('Content-Type', 'application/json') 220 | res.send(pwa.manifest) 221 | }) 222 | app.use('/service-worker.js', (req, res) => { 223 | res.setHeader('Content-Type', 'text/javascript') 224 | res.send(pwa.serviceWorker) 225 | }) 226 | } 227 | 228 | app.use('/statics', express.static(appPaths.resolve.src('statics'), { 229 | maxAge: 0 230 | })) 231 | 232 | cfg.__ssrExtension.extendApp({ app }) 233 | 234 | app.get('*', render) 235 | } 236 | }, 237 | cfg.devServer 238 | )) 239 | 240 | readyPromise.then(() => { 241 | server.listen(cfg.devServer.port, cfg.devServer.host, () => { 242 | resolve() 243 | if (cfg.devServer.open) { 244 | openBrowser(cfg.build.APP_URL) 245 | } 246 | }) 247 | }) 248 | 249 | this.__cleanup = () => { 250 | this.__cleanup = null 251 | htmlWatcher.close() 252 | return Promise.all([ 253 | new Promise(resolve => { server.close(resolve) }), 254 | new Promise(resolve => { serverCompilerWatcher.close(resolve) }) 255 | ]) 256 | } 257 | } 258 | 259 | stop () { 260 | if (this.__cleanup) { 261 | log(`Shutting down`) 262 | return this.__cleanup() 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/electron/index.js: -------------------------------------------------------------------------------- 1 | const 2 | spawn = require('../helpers/spawn'), 3 | webpack = require('webpack'), 4 | logger = require('../helpers/logger'), 5 | log = logger('app:electron'), 6 | warn = logger('app:electron', 'red'), 7 | path = require('path'), 8 | fse = require('fs-extra'), 9 | appPaths = require('../app-paths'), 10 | nodePackager = require('../helpers/node-packager') 11 | 12 | class ElectronRunner { 13 | constructor () { 14 | this.pid = 0 15 | this.watcher = null 16 | } 17 | 18 | async run (quasarConfig) { 19 | const url = quasarConfig.getBuildConfig().build.APP_URL 20 | 21 | if (this.pid) { 22 | if (this.url !== url) { 23 | await this.stop() 24 | } 25 | else { 26 | return 27 | } 28 | } 29 | 30 | this.url = url 31 | 32 | const compiler = webpack(quasarConfig.getWebpackConfig().main) 33 | 34 | return new Promise((resolve, reject) => { 35 | log(`Building main Electron process...`) 36 | this.watcher = compiler.watch({}, async (err, stats) => { 37 | if (err) { 38 | console.log(err) 39 | return 40 | } 41 | 42 | log(`Webpack built Electron main process`) 43 | log() 44 | process.stdout.write(stats.toString({ 45 | colors: true, 46 | modules: false, 47 | children: false, 48 | chunks: false, 49 | chunkModules: false 50 | }) + '\n') 51 | log() 52 | 53 | if (stats.hasErrors()) { 54 | warn(`Electron main build failed with errors`) 55 | return 56 | } 57 | 58 | await this.__stopElectron() 59 | this.__startElectron() 60 | 61 | resolve() 62 | }) 63 | }) 64 | } 65 | 66 | build (quasarConfig) { 67 | const cfg = quasarConfig.getBuildConfig() 68 | 69 | return new Promise((resolve, reject) => { 70 | spawn( 71 | nodePackager, 72 | [ 'install', '--production' ], 73 | cfg.build.distDir, 74 | code => { 75 | if (code) { 76 | warn(`⚠️ [FAIL] ${nodePackager} failed installing dependencies`) 77 | process.exit(1) 78 | } 79 | resolve() 80 | } 81 | ) 82 | }).then(() => { 83 | return new Promise(async (resolve, reject) => { 84 | if (typeof cfg.electron.beforePackaging === 'function') { 85 | log('Running beforePackaging()') 86 | log() 87 | 88 | const result = cfg.electron.beforePackaging({ 89 | appPaths, 90 | unpackagedDir: cfg.build.distDir 91 | }) 92 | 93 | if (result && result.then) { 94 | await result 95 | } 96 | 97 | log() 98 | log('[SUCCESS] Done running beforePackaging()') 99 | } 100 | resolve() 101 | }) 102 | }).then(() => { 103 | const 104 | bundlerName = cfg.electron.bundler, 105 | bundlerConfig = cfg.electron[bundlerName], 106 | bundler = require('./bundler').getBundler(bundlerName), 107 | pkgName = `electron-${bundlerName}` 108 | 109 | return new Promise((resolve, reject) => { 110 | log(`Bundling app with electron-${bundlerName}...`) 111 | log() 112 | 113 | const bundlePromise = bundlerName === 'packager' 114 | ? bundler(bundlerConfig) 115 | : bundler.build(bundlerConfig) 116 | 117 | bundlePromise 118 | .then(() => { 119 | log() 120 | log(`[SUCCESS] ${pkgName} built the app`) 121 | log() 122 | resolve() 123 | }) 124 | .catch(err => { 125 | log() 126 | warn(`⚠️ [FAIL] ${pkgName} could not build`) 127 | log() 128 | console.error(err + '\n') 129 | reject() 130 | }) 131 | }) 132 | }) 133 | } 134 | 135 | stop () { 136 | return new Promise((resolve, reject) => { 137 | const finalize = () => { 138 | this.__stopElectron().then(resolve) 139 | } 140 | 141 | if (this.watcher) { 142 | this.watcher.close(finalize) 143 | this.watcher = null 144 | return 145 | } 146 | 147 | finalize() 148 | }) 149 | } 150 | 151 | __startElectron () { 152 | log(`Booting up Electron process...`) 153 | this.pid = spawn( 154 | require(appPaths.resolve.app('node_modules/electron')), 155 | [ 156 | '--inspect=5858', 157 | appPaths.resolve.app('.quasar/electron/electron-main.js') 158 | ], 159 | appPaths.appDir, 160 | code => { 161 | if (code) { 162 | warn() 163 | warn(`⚠️ Electron process ended with error code: ${code}`) 164 | warn() 165 | process.exit(1) 166 | } 167 | 168 | if (this.killPromise) { 169 | this.killPromise() 170 | this.killPromise = null 171 | } 172 | else { // else it wasn't killed by us 173 | warn() 174 | warn('Electron process was killed. Exiting...') 175 | warn() 176 | process.exit(0) 177 | } 178 | } 179 | ) 180 | } 181 | 182 | __stopElectron () { 183 | const pid = this.pid 184 | 185 | if (!pid) { 186 | return Promise.resolve() 187 | } 188 | 189 | log('Shutting down Electron process...') 190 | this.pid = 0 191 | return new Promise((resolve, reject) => { 192 | this.killPromise = resolve 193 | process.kill(pid) 194 | }) 195 | } 196 | } 197 | 198 | module.exports = new ElectronRunner() 199 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/helpers/banner.js: -------------------------------------------------------------------------------- 1 | const { green, grey, underline } = require('chalk') 2 | const 3 | appPaths = require('../app-paths'), 4 | { 5 | version, 6 | dependencies: { 'quasar-framework': quasarVersion } 7 | } = require(appPaths.resolve.cli('package.json')) 8 | 9 | module.exports = function (argv, cmd, details) { 10 | let banner = '' 11 | 12 | if (details) { 13 | banner += ` 14 | ${underline('Build succeeded')} 15 | ` 16 | } 17 | 18 | banner += ` 19 | ${cmd === 'dev' ? 'Dev mode..........' : 'Build mode........'} ${green(argv.mode)} 20 | Quasar theme...... ${green(argv.theme)} 21 | Quasar CLI........ ${green('v' + version)} 22 | Quasar Framework.. ${green('v' + quasarVersion)} 23 | Debugging......... ${cmd === 'dev' || argv.debug ? green('enabled') : grey('no')}` 24 | 25 | if (details) { 26 | banner += ` 27 | ================== 28 | Output folder..... ${green(details.outputFolder)}` 29 | 30 | if (argv.mode === 'ssr') { 31 | banner += ` 32 | 33 | Tip: The output folder must be yarn/npm installed before using it, 34 | except when it is run inside your already yarn/npm installed project folder. 35 | 36 | Tip: Notice the package.json generated, where there's a script defined: 37 | "start": "node index.js" 38 | Running "$ yarn start" or "$ npm run start" from the output folder will 39 | start the webserver. Alternatively you can call "$ node index.js" 40 | yourself.` 41 | } 42 | else if (argv.mode === 'cordova') { 43 | banner += ` 44 | 45 | Tip: "src-cordova" is a Cordova project folder, so everything you know 46 | about Cordova applies to it. Quasar CLI only generates the content 47 | for "src-cordova/www" folder and then Cordova takes over and builds 48 | the mobile app. 49 | 50 | Tip: Feel free to use Cordova CLI or change any files in "src-cordova", 51 | except for "www" folder which must be built by Quasar CLI.` 52 | } 53 | else if (['spa', 'pwa'].includes(argv.mode)) { 54 | banner += ` 55 | 56 | Tip: Built files are meant to be served over an HTTP server 57 | Opening index.html over file:// won't work 58 | 59 | Tip: You can use "$ quasar serve" command to create a web server, 60 | both for testing or production. Type "$ quasar serve -h" for 61 | parameters. Also, an npm script (usually named "start") can 62 | be added for deployment environments. 63 | If you're using Vue Router "history" mode, don't forget to 64 | specify the "--history" parameter: "$ quasar serve --history"` 65 | } 66 | } 67 | 68 | console.log(banner + '\n') 69 | } 70 | 71 | module.exports.devCompilationSuccess = function (ctx, url) { 72 | return `App URL........... ${green(url)} 73 | Dev mode.......... ${green(ctx.modeName + (ctx.mode.ssr && ctx.mode.pwa ? ' + pwa' : ''))} 74 | Quasar theme...... ${green(ctx.themeName)} 75 | Quasar CLI........ ${green('v' + version)} 76 | Quasar Framework.. ${green('v' + quasarVersion)} 77 | ` 78 | } 79 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /lib/legacy-validations.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | fse = require('fs-extra'), 4 | { green } = require('chalk') 5 | 6 | const 7 | appPaths = require('./app-paths') 8 | 9 | module.exports = function legacyValidations (cfg) { 10 | let file, content, error = false 11 | 12 | file = appPaths.resolve.app(cfg.sourceFiles.indexHtmlTemplate) 13 | if (!fs.existsSync(file)) { 14 | console.log('⚠️ Missing /src/index.template.html file...') 15 | console.log() 16 | error = true 17 | } 18 | content = fs.readFileSync(file, 'utf-8') 19 | if (content.indexOf(' -1) { 20 | console.log(`⚠️ Your newer Quasar CLI requires a minor change to /src/index.template.html 21 | Please remove this tag completely: 22 | 23 | `) 24 | console.log() 25 | error = true 26 | } 27 | 28 | if (content.indexOf(`chunk.initial ? 'preload' : 'prefetch'`) > -1) { 29 | console.log(`\n⚠️ Your newer Quasar CLI requires a minor change to /src/index.template.html 30 | Please remove this section completely: 31 | 32 | 36 | <% if (!['cordova', 'electron'].includes(htmlWebpackPlugin.options.ctx.modeName) && htmlWebpackPlugin.options.ctx.prod) { 37 | for (var chunk of webpack.chunks) { 38 | for (var file of chunk.files) { 39 | if (file.match(/\.(js|css)$/)) { %> 40 | 41 | <% }}}} %> 42 | `) 43 | console.log() 44 | error = true 45 | } 46 | 47 | if (content.indexOf(' -1) { 48 | console.log(`\n⚠️ Your newer Quasar CLI requires a minor change to /src/index.template.html 49 | Please remove this section completely: 50 | 51 | <% if (htmlWebpackPlugin.options.ctx.mode.pwa) { %> 52 | 53 | ..... 54 | <% } %> 55 | `) 56 | console.log() 57 | error = true 58 | } 59 | 60 | if (content.indexOf('htmlWebpackPlugin.options.headScripts') > -1) { 61 | console.log(`\n⚠️ Your newer Quasar CLI requires a minor change to /src/index.template.html 62 | Please remove this section completely: 63 | 64 | <%= htmlWebpackPlugin.options.headScripts %> 65 | `) 66 | console.log() 67 | error = true 68 | } 69 | 70 | if (content.indexOf('htmlWebpackPlugin.options.bodyScripts') > -1) { 71 | console.log(`\n⚠️ Your newer Quasar CLI requires a minor change to /src/index.template.html 72 | Please remove this section completely: 73 | 74 | <%= htmlWebpackPlugin.options.bodyScripts %> 75 | `) 76 | console.log() 77 | error = true 78 | } 79 | 80 | file = appPaths.resolve.app(cfg.sourceFiles.rootComponent) 81 | content = fs.readFileSync(file, 'utf-8') 82 | if (content.indexOf('q-app') === -1) { 83 | console.log(`\n⚠️ Quasar CLI requires a minor change to the root component: 84 | ${file} 85 | 86 | Please add: id="q-app" (or write #q-app if using Pug) 87 | to the outermost HTML element of the template. 88 | 89 | ${green('Example:')} 90 | 95 | `) 96 | error = true 97 | } 98 | 99 | if (error) { 100 | process.exit(1) 101 | } 102 | 103 | file = appPaths.resolve.app(cfg.sourceFiles.router) 104 | content = fs.readFileSync(file, 'utf-8') 105 | if (cfg.ctx.mode.ssr && content.indexOf('export default function') === -1) { 106 | console.log(`\n⚠️ In order to build with SSR mode you need a minor change to the ROUTER file 107 | This won't break other build modes after you change it. 108 | 109 | ${file} 110 | 111 | You need to have a default export set to "function ({ store })" which returns a new 112 | instance of Router instead of default exporting the Router instance itself. 113 | 114 | ${green('OLD WAY:')} 115 | import Vue from 'vue' 116 | import VueRouter from 'vue-router' 117 | import routes from './routes' 118 | Vue.use(VueRouter) 119 | 120 | // in the new way, we'll wrap the instantiation into: 121 | // export default function ({ store }) --> store is optional 122 | const Router = new VueRouter({ 123 | scrollBehavior: () => ({ y: 0 }), 124 | routes, 125 | // Leave these as they are and change from quasar.conf.js instead! 126 | mode: process.env.VUE_ROUTER_MODE, 127 | base: process.env.VUE_ROUTER_BASE, 128 | }) 129 | 130 | // in the new way, this will be no more 131 | export default Router 132 | 133 | ${green('NEW WAY:')} 134 | import Vue from 'vue' 135 | import VueRouter from 'vue-router' 136 | import routes from './routes' 137 | Vue.use(VueRouter) 138 | 139 | // DO NOT import the store here as you will receive it as 140 | // parameter in the default exported function: 141 | 142 | export default function (/* { store } */) { 143 | // IMPORTANT! Instantiate Router inside this function 144 | 145 | const Router = new VueRouter({ 146 | scrollBehavior: () => ({ y: 0 }), 147 | routes, 148 | // Leave these as they are and change from quasar.conf.js instead! 149 | mode: process.env.VUE_ROUTER_MODE, 150 | base: process.env.VUE_ROUTER_BASE, 151 | }) 152 | 153 | return Router 154 | } 155 | `) 156 | console.log() 157 | process.exit(1) 158 | } 159 | 160 | if (cfg.store && cfg.ctx.mode.ssr) { 161 | file = appPaths.resolve.app(cfg.sourceFiles.store) 162 | content = fs.readFileSync(file, 'utf-8') 163 | if (content.indexOf('export default function') === -1) { 164 | console.log(`\n⚠️ In order to build with SSR mode you need a minor change to the STORE file 165 | This won't break other build modes after you change it. 166 | 167 | ${file} 168 | 169 | You need to have a default export set to "function ()" which returns a new 170 | instance of Vuex Store instead of default exporting the Store instance itself. 171 | 172 | ${green('OLD WAY:')} 173 | import Vue from 'vue' 174 | import Vuex from 'vuex' 175 | import example from './module-example' 176 | Vue.use(Vuex) 177 | 178 | // in the new way, we'll wrap the instantiation into: 179 | // export default function () 180 | const store = new Vuex.Store({ 181 | modules: { 182 | example 183 | } 184 | }) 185 | 186 | // in the new way, this will be no more 187 | export default store 188 | 189 | ${green('NEW WAY:')} 190 | import Vue from 'vue' 191 | import Vuex from 'vuex' 192 | import example from './module-example' 193 | Vue.use(Vuex) 194 | 195 | export default function () { 196 | // IMPORTANT! Instantiate Store inside this function 197 | 198 | const Store = new Vuex.Store({ 199 | modules: { 200 | example 201 | } 202 | }) 203 | 204 | return Store 205 | } 206 | `) 207 | console.log() 208 | process.exit(1) 209 | } 210 | } 211 | 212 | file = appPaths.resolve.app('.babelrc') 213 | if (!fs.existsSync(file)) { 214 | console.log('⚠️ Missing .babelrc file...') 215 | console.log() 216 | process.exit(1) 217 | } 218 | content = fs.readFileSync(file, 'utf-8') 219 | if (content.indexOf('"transform-runtime"') > -1) { 220 | console.log() 221 | console.log(' ⚠️ WARNING') 222 | console.log(` Your newer Quasar CLI requires a change to .babelrc file.`) 223 | console.log(` Doing it automatically. Please review the changes.`) 224 | console.log() 225 | 226 | fse.copySync( 227 | appPaths.resolve.cli('templates/app/babelrc'), 228 | appPaths.resolve.app('.babelrc') 229 | ) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /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/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/mode/mode-cordova.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-cordova'), 7 | warn = logger('app:mode-cordova', 'red'), 8 | spawn = require('../helpers/spawn') 9 | 10 | function ensureNpmInstalled () { 11 | if (fs.existsSync(appPaths.resolve.cordova('node_modules'))) { 12 | return 13 | } 14 | 15 | log('Installing dependencies in /src-cordova') 16 | spawn.sync( 17 | 'npm', 18 | [ 'install' ], 19 | appPaths.cordovaDir, 20 | () => { 21 | warn(`⚠️ [FAIL] npm failed installing dependencies in /src-cordova`) 22 | process.exit(1) 23 | } 24 | ) 25 | } 26 | 27 | class Mode { 28 | get isInstalled () { 29 | return fs.existsSync(appPaths.cordovaDir) 30 | } 31 | 32 | add (target) { 33 | if (this.isInstalled) { 34 | warn(`Cordova support detected already. Aborting.`) 35 | return 36 | } 37 | 38 | const 39 | pkg = require(appPaths.resolve.app('package.json')), 40 | appName = pkg.productName || pkg.name || 'Quasar App' 41 | 42 | log('Creating Cordova source folder...') 43 | 44 | spawn.sync( 45 | 'cordova', 46 | ['create', 'src-cordova', pkg.cordovaId || 'org.quasar.cordova.app', appName], 47 | appPaths.appDir, 48 | () => { 49 | warn(`⚠️ There was an error trying to install Cordova support`) 50 | process.exit(1) 51 | } 52 | ) 53 | 54 | log(`Cordova support was installed`) 55 | log(`App name was taken from package.json: "${appName}"`) 56 | log() 57 | warn(`If you want a different App name then remove Cordova support, edit productName field from package.json then add Cordova support again.`) 58 | warn() 59 | 60 | if (!target) { 61 | log(`Please manually add Cordova platforms using Cordova CLI from the newly created "src-cordova" folder.`) 62 | log() 63 | return 64 | } 65 | 66 | this.addPlatform(target) 67 | } 68 | 69 | hasPlatform (target) { 70 | return fs.existsSync(appPaths.resolve.cordova(`platforms/${target}`)) 71 | } 72 | 73 | addPlatform (target) { 74 | fse.ensureDir(appPaths.resolve.cordova(`www`)) 75 | 76 | if (this.hasPlatform(target)) { 77 | ensureNpmInstalled() 78 | return 79 | } 80 | 81 | log(`Adding Cordova platform "${target}"`) 82 | spawn.sync( 83 | 'cordova', 84 | ['platform', 'add', target], 85 | appPaths.cordovaDir, 86 | () => { 87 | warn(`⚠️ There was an error trying to install Cordova platform "${target}"`) 88 | process.exit(1) 89 | } 90 | ) 91 | } 92 | 93 | remove () { 94 | if (!this.isInstalled) { 95 | warn(`No Cordova support detected. Aborting.`) 96 | return 97 | } 98 | 99 | fse.removeSync(appPaths.cordovaDir) 100 | log(`Cordova support was removed`) 101 | } 102 | } 103 | 104 | module.exports = Mode 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/quasar-config.js: -------------------------------------------------------------------------------- 1 | const 2 | path = require('path'), 3 | fs = require('fs'), 4 | merge = require('webpack-merge'), 5 | chokidar = require('chokidar'), 6 | debounce = require('lodash.debounce') 7 | 8 | const 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 | 15 | function 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 | 48 | function encode (obj) { 49 | return JSON.stringify(obj, (key, value) => { 50 | return typeof value === 'function' 51 | ? `/fn(${value.toString()})` 52 | : value 53 | }) 54 | } 55 | 56 | function 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 | 69 | function 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 | 88 | class 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 | 745 | module.exports = QuasarConfig 746 | -------------------------------------------------------------------------------- /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/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/webpack/create-chain.js: -------------------------------------------------------------------------------- 1 | const 2 | path = require('path'), 3 | webpack = require('webpack'), 4 | WebpackChain = require('webpack-chain'), 5 | VueLoaderPlugin = require('vue-loader/lib/plugin'), 6 | WebpackProgress = require('./plugin.progress') 7 | 8 | const 9 | appPaths = require('../app-paths'), 10 | injectStyleRules = require('./inject.style-rules') 11 | 12 | module.exports = function (cfg, configName) { 13 | const 14 | chain = new WebpackChain(), 15 | needsHash = !cfg.ctx.dev && !['electron', 'cordova'].includes(cfg.ctx.modeName), 16 | fileHash = needsHash ? '.[hash:8]' : '', 17 | chunkHash = needsHash ? '.[contenthash:8]' : '', 18 | resolveModules = [ 19 | 'node_modules', 20 | appPaths.resolve.app('node_modules'), 21 | appPaths.resolve.cli('node_modules') 22 | ] 23 | 24 | chain.entry('app').add(appPaths.resolve.app('.quasar/client-entry.js')) 25 | chain.mode(cfg.ctx.dev ? 'development' : 'production') 26 | chain.devtool(cfg.build.sourceMap ? cfg.build.devtool : false) 27 | 28 | if (cfg.ctx.prod || cfg.ctx.mode.ssr) { 29 | chain.output 30 | .path( 31 | cfg.ctx.mode.ssr 32 | ? path.join(cfg.build.distDir, 'www') 33 | : cfg.build.distDir 34 | ) 35 | .publicPath(cfg.build.publicPath) 36 | .filename(`js/[name]${fileHash}.js`) 37 | .chunkFilename(`js/[name]${chunkHash}.js`) 38 | } 39 | 40 | chain.resolve.symlinks(false) 41 | 42 | chain.resolve.extensions 43 | .merge([ `.${cfg.ctx.themeName}.js`, '.js', '.vue' ]) 44 | 45 | chain.resolve.modules 46 | .merge(resolveModules) 47 | 48 | chain.resolve.alias 49 | .merge({ 50 | quasar: cfg.framework.all !== true 51 | ? `quasar-framework` 52 | : appPaths.resolve.app(`node_modules/quasar-framework/dist/quasar.${cfg.ctx.themeName}.esm.js`), 53 | src: appPaths.srcDir, 54 | app: appPaths.appDir, 55 | components: appPaths.resolve.src(`components`), 56 | layouts: appPaths.resolve.src(`layouts`), 57 | pages: appPaths.resolve.src(`pages`), 58 | assets: appPaths.resolve.src(`assets`), 59 | plugins: appPaths.resolve.src(`plugins`), 60 | variables: appPaths.resolve.app(`.quasar/variables.styl`), 61 | 62 | // CLI using these ones: 63 | 'quasar-app-styl': appPaths.resolve.app(`.quasar/app.styl`), 64 | 'quasar-app-variables': appPaths.resolve.src(`css/themes/variables.${cfg.ctx.themeName}.styl`), 65 | 'quasar-styl': `quasar-framework/dist/quasar.${cfg.ctx.themeName}.styl`, 66 | 'quasar-addon-styl': cfg.framework.cssAddon 67 | ? `quasar-framework/src/css/flex-addon.styl` 68 | : appPaths.resolve.app(`.quasar/empty.styl`) 69 | }) 70 | 71 | if (cfg.build.vueCompiler) { 72 | chain.resolve.alias.set('vue$', 'vue/dist/vue.esm.js') 73 | } 74 | 75 | chain.resolveLoader.modules 76 | .merge(resolveModules) 77 | 78 | chain.module.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/) 79 | 80 | chain.module.rule('vue') 81 | .test(/\.vue$/) 82 | .use('vue-loader') 83 | .loader('vue-loader') 84 | .options({ 85 | productionMode: cfg.ctx.prod, 86 | compilerOptions: { 87 | preserveWhitespace: false 88 | }, 89 | transformAssetUrls: { 90 | video: 'src', 91 | source: 'src', 92 | img: 'src', 93 | image: 'xlink:href' 94 | } 95 | }) 96 | 97 | chain.module.rule('babel') 98 | .test(/\.jsx?$/) 99 | .exclude 100 | .add(filepath => { 101 | // always transpile js(x) in Vue files 102 | if (/\.vue\.jsx?$/.test(filepath)) { 103 | return false 104 | } 105 | 106 | if (cfg.build.transpileDependencies.some(dep => filepath.match(dep))) { 107 | return false 108 | } 109 | 110 | // Don't transpile anything else in node_modules 111 | return /[\\/]node_modules[\\/]/.test(filepath) 112 | }) 113 | .end() 114 | .use('babel-loader') 115 | .loader('babel-loader') 116 | .options({ 117 | extends: appPaths.resolve.app('.babelrc'), 118 | plugins: cfg.framework.all !== true ? [ 119 | [ 120 | 'transform-imports', { 121 | quasar: { 122 | transform: `quasar-framework/dist/babel-transforms/imports.${cfg.ctx.themeName}.js`, 123 | preventFullImport: true 124 | } 125 | } 126 | ] 127 | ] : [] 128 | }) 129 | 130 | chain.module.rule('images') 131 | .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) 132 | .use('url-loader') 133 | .loader('url-loader') 134 | .options({ 135 | limit: 10000, 136 | name: `img/[name]${fileHash}.[ext]` 137 | }) 138 | 139 | chain.module.rule('fonts') 140 | .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/) 141 | .use('url-loader') 142 | .loader('url-loader') 143 | .options({ 144 | limit: 10000, 145 | name: `fonts/[name]${fileHash}.[ext]` 146 | }) 147 | 148 | chain.module.rule('media') 149 | .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/) 150 | .use('url-loader') 151 | .loader('url-loader') 152 | .options({ 153 | limit: 10000, 154 | name: `media/[name]${fileHash}.[ext]` 155 | }) 156 | 157 | injectStyleRules(chain, { 158 | rtl: cfg.build.rtl, 159 | sourceMap: cfg.build.sourceMap, 160 | extract: cfg.build.extractCSS, 161 | minify: cfg.build.minify 162 | ? !cfg.build.extractCSS 163 | : false 164 | }) 165 | 166 | chain.plugin('vue-loader') 167 | .use(VueLoaderPlugin) 168 | 169 | chain.plugin('define') 170 | .use(webpack.DefinePlugin, [ cfg.build.env ]) 171 | 172 | if (cfg.build.showProgress) { 173 | chain.plugin('progress') 174 | .use(WebpackProgress, [{ name: configName }]) 175 | } 176 | 177 | chain.performance 178 | .hints(false) 179 | .maxAssetSize(500000) 180 | 181 | // DEVELOPMENT build 182 | if (cfg.ctx.dev) { 183 | const 184 | FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'), 185 | { devCompilationSuccess } = require('../helpers/banner') 186 | 187 | chain.optimization 188 | .noEmitOnErrors(true) 189 | 190 | chain.plugin('friendly-errors') 191 | .use(FriendlyErrorsPlugin, [{ 192 | clearConsole: true, 193 | compilationSuccessInfo: ['spa', 'pwa', 'ssr'].includes(cfg.ctx.modeName) 194 | ? { notes: [ devCompilationSuccess(cfg.ctx, cfg.build.APP_URL) ] } 195 | : undefined 196 | }]) 197 | } 198 | // PRODUCTION build 199 | else { 200 | // keep module.id stable when vendor modules does not change 201 | chain.plugin('hashed-module-ids') 202 | .use(webpack.HashedModuleIdsPlugin, [{ 203 | hashDigest: 'hex' 204 | }]) 205 | 206 | // keep chunk ids stable so async chunks have consistent hash 207 | const hash = require('hash-sum') 208 | chain 209 | .plugin('named-chunks') 210 | .use(webpack.NamedChunksPlugin, [ 211 | chunk => chunk.name || hash( 212 | Array.from(chunk.modulesIterable, m => m.id).join('_') 213 | ) 214 | ]) 215 | 216 | if (configName !== 'Server') { 217 | const 218 | add = cfg.vendor.add, 219 | rem = cfg.vendor.remove, 220 | regex = /[\\/]node_modules[\\/]/ 221 | 222 | chain.optimization 223 | .splitChunks({ 224 | cacheGroups: { 225 | vendors: { 226 | name: 'vendor', 227 | chunks: 'initial', 228 | priority: -10, 229 | // a module is extracted into the vendor chunk if... 230 | test: add || rem 231 | ? module => { 232 | if (module.resource) { 233 | if (add && add.test(module.resource)) { return true } 234 | if (rem && rem.test(module.resource)) { return false } 235 | } 236 | return regex.test(module.resource) 237 | } 238 | : module => regex.test(module.resource) 239 | }, 240 | common: { 241 | name: `chunk-common`, 242 | minChunks: 2, 243 | priority: -20, 244 | chunks: 'initial', 245 | reuseExistingChunk: true 246 | } 247 | } 248 | }) 249 | 250 | // extract webpack runtime and module manifest to its own file in order to 251 | // prevent vendor hash from being updated whenever app bundle is updated 252 | if (cfg.build.webpackManifest) { 253 | chain.optimization.runtimeChunk('single') 254 | } 255 | 256 | // copy statics to dist folder 257 | const CopyWebpackPlugin = require('copy-webpack-plugin') 258 | chain.plugin('copy-webpack') 259 | .use(CopyWebpackPlugin, [ 260 | [{ 261 | from: appPaths.resolve.src('statics'), 262 | to: 'statics', 263 | ignore: ['.*'] 264 | }] 265 | ]) 266 | } 267 | 268 | // Scope hoisting ala Rollupjs 269 | if (cfg.build.scopeHoisting) { 270 | chain.optimization 271 | .concatenateModules(true) 272 | } 273 | 274 | if (cfg.ctx.debug) { 275 | // reset default webpack 4 minimizer 276 | chain.optimization.minimizer([]) 277 | } 278 | else if (cfg.build.minify) { 279 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 280 | 281 | chain.optimization 282 | .minimizer([ 283 | new UglifyJSPlugin({ 284 | uglifyOptions: cfg.build.uglifyOptions, 285 | cache: true, 286 | parallel: true, 287 | sourceMap: cfg.build.sourceMap 288 | }) 289 | ]) 290 | } 291 | 292 | // configure CSS extraction & optimize 293 | if (cfg.build.extractCSS) { 294 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 295 | 296 | // extract css into its own file 297 | chain.plugin('mini-css-extract') 298 | .use(MiniCssExtractPlugin, [{ 299 | filename: 'css/[name].[contenthash:8].css' 300 | }]) 301 | 302 | // dedupe & minify CSS (only if extracted) 303 | if (cfg.build.minify) { 304 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 305 | 306 | const cssProcessorOptions = { 307 | parser: require('postcss-safe-parser'), 308 | autoprefixer: { disable: true }, 309 | mergeLonghand: false 310 | } 311 | if (cfg.build.sourceMap) { 312 | cssProcessorOptions.map = { inline: false } 313 | } 314 | 315 | // We are using this plugin so that possible 316 | // duplicated CSS = require(different components) can be deduped. 317 | chain.plugin('optimize-css') 318 | .use(OptimizeCSSPlugin, [{ 319 | canPrint: false, 320 | cssProcessor: require('cssnano'), 321 | cssProcessorOptions 322 | }]) 323 | } 324 | } 325 | 326 | if (configName !== 'Server') { 327 | // also produce a gzipped version 328 | if (cfg.build.gzip) { 329 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 330 | chain.plugin('compress-webpack') 331 | .use(CompressionWebpackPlugin, [ cfg.build.gzip ]) 332 | } 333 | 334 | if (cfg.build.analyze) { 335 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 336 | chain.plugin('bundle-analyzer') 337 | .use(BundleAnalyzerPlugin, [ Object.assign({}, cfg.build.analyze) ]) 338 | } 339 | } 340 | } 341 | 342 | return chain 343 | } 344 | -------------------------------------------------------------------------------- /lib/webpack/electron/main.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | path = require('path'), 4 | chalk = require('chalk'), 5 | webpack = require('webpack'), 6 | WebpackChain = require('webpack-chain'), 7 | WebpackProgress = require('../plugin.progress') 8 | 9 | const 10 | appPaths = require('../../app-paths') 11 | 12 | module.exports = function (cfg, configName) { 13 | const 14 | { dependencies:appDeps = {} } = require(appPaths.resolve.cli('package.json')), 15 | { dependencies:cliDeps = {} } = require(appPaths.resolve.app('package.json')) 16 | 17 | const chain = new WebpackChain() 18 | const resolveModules = [ 19 | appPaths.resolve.app('node_modules'), 20 | appPaths.resolve.cli('node_modules') 21 | ] 22 | 23 | chain.target('electron-main') 24 | chain.mode(cfg.ctx.dev ? 'development' : 'production') 25 | chain.node 26 | .merge({ 27 | __dirname: cfg.ctx.dev, 28 | __filename: cfg.ctx.dev 29 | }) 30 | 31 | chain.entry('electron-main') 32 | .add(appPaths.resolve.app( 33 | cfg.ctx.dev ? cfg.sourceFiles.electronMainDev : cfg.sourceFiles.electronMainProd 34 | )) 35 | 36 | chain.output 37 | .filename('electron-main.js') 38 | .libraryTarget('commonjs2') 39 | .path( 40 | cfg.ctx.dev 41 | ? appPaths.resolve.app('.quasar/electron') 42 | : cfg.build.distDir 43 | ) 44 | 45 | chain.externals([ 46 | ...Object.keys(cliDeps), 47 | ...Object.keys(appDeps) 48 | ]) 49 | 50 | chain.module.rule('babel') 51 | .test(/\.js$/) 52 | .exclude 53 | .add(/node_modules/) 54 | .end() 55 | .use('babel-loader') 56 | .loader('babel-loader') 57 | .options({ 58 | extends: appPaths.resolve.app('.babelrc') 59 | }) 60 | 61 | chain.module.rule('node') 62 | .test(/\.node$/) 63 | .use('node-loader') 64 | .loader('node-loader') 65 | 66 | chain.resolve.modules 67 | .merge(resolveModules) 68 | 69 | chain.resolve.extensions 70 | .merge([ '.js', '.json', '.node' ]) 71 | 72 | chain.resolveLoader.modules 73 | .merge(resolveModules) 74 | 75 | chain.optimization 76 | .noEmitOnErrors(true) 77 | 78 | chain.plugin('progress') 79 | .use(WebpackProgress, [{ name: configName }]) 80 | 81 | chain.plugin('define') 82 | .use(webpack.DefinePlugin, [ cfg.build.env ]) 83 | 84 | if (cfg.ctx.prod) { 85 | if (cfg.build.minify) { 86 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 87 | 88 | chain.optimization 89 | .minimizer([ 90 | new UglifyJSPlugin({ 91 | uglifyOptions: cfg.build.uglifyOptions, 92 | cache: true, 93 | parallel: true, 94 | sourceMap: cfg.build.sourceMap 95 | }) 96 | ]) 97 | } 98 | 99 | const ElectronPackageJson = require('./plugin.electron-package-json') 100 | 101 | // write package.json file 102 | chain.plugin('package-json') 103 | .use(ElectronPackageJson) 104 | } 105 | 106 | return chain 107 | } 108 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/webpack/index.js: -------------------------------------------------------------------------------- 1 | const 2 | createChain = require('./create-chain'), 3 | log = require('../helpers/logger')('app:webpack') 4 | 5 | function getWebpackConfig (chain, cfg, { 6 | name, 7 | hot, 8 | cfgExtendBase = cfg.build, 9 | invokeParams 10 | }) { 11 | if (typeof cfgExtendBase.chainWebpack === 'function') { 12 | log(`Chaining ${name ? name + ' ' : ''}Webpack config`) 13 | cfgExtendBase.chainWebpack(chain, invokeParams) 14 | } 15 | 16 | const webpackConfig = chain.toConfig() 17 | 18 | if (typeof cfgExtendBase.extendWebpack === 'function') { 19 | log(`Extending ${name ? name + ' ' : ''}Webpack config`) 20 | cfgExtendBase.extendWebpack(webpackConfig, invokeParams) 21 | } 22 | 23 | if (hot && cfg.ctx.dev && cfg.devServer.hot) { 24 | // tap entries for HMR 25 | require('webpack-dev-server').addDevServerEntrypoints(webpackConfig, cfg.devServer) 26 | } 27 | 28 | return webpackConfig 29 | } 30 | 31 | function getSPA (cfg) { 32 | const chain = createChain(cfg, 'SPA') 33 | require('./spa')(chain, cfg) 34 | return getWebpackConfig(chain, cfg, { 35 | name: 'SPA', 36 | hot: true, 37 | invokeParams: { isClient: true, isServer: false } 38 | }) 39 | } 40 | 41 | function getPWA (cfg) { 42 | const chain = createChain(cfg, 'PWA') 43 | require('./spa')(chain, cfg) // extending a SPA 44 | require('./pwa')(chain, cfg) 45 | return getWebpackConfig(chain, cfg, { 46 | name: 'PWA', 47 | hot: true, 48 | invokeParams: { isClient: true, isServer: false } 49 | }) 50 | } 51 | 52 | function getCordova (cfg) { 53 | const chain = createChain(cfg, 'Cordova') 54 | require('./cordova')(chain, cfg) 55 | return getWebpackConfig(chain, cfg, { 56 | name: 'Cordova', 57 | hot: true, 58 | invokeParams: { isClient: true, isServer: false } 59 | }) 60 | } 61 | 62 | function getElectron (cfg) { 63 | const 64 | rendererChain = createChain(cfg, 'Renderer process'), 65 | mainChain = require('./electron/main')(cfg, 'Main process') 66 | 67 | require('./electron/renderer')(rendererChain, cfg) 68 | 69 | return { 70 | renderer: getWebpackConfig(rendererChain, cfg, { 71 | name: 'Renderer process', 72 | hot: true, 73 | invokeParams: { isClient: true, isServer: false } 74 | }), 75 | main: getWebpackConfig(mainChain, cfg, { 76 | name: 'Main process', 77 | cfgExtendBase: cfg.electron 78 | }) 79 | } 80 | } 81 | 82 | function getSSR (cfg) { 83 | const 84 | client = createChain(cfg, 'Client'), 85 | server = createChain(cfg, 'Server') 86 | 87 | require('./ssr/client')(client, cfg) 88 | if (cfg.ctx.mode.pwa) { 89 | require('./pwa')(client, cfg) // extending a PWA 90 | } 91 | 92 | require('./ssr/server')(server, cfg) 93 | 94 | return { 95 | client: getWebpackConfig(client, cfg, { 96 | name: 'Client', 97 | hot: true, 98 | invokeParams: { isClient: true, isServer: false } 99 | }), 100 | server: getWebpackConfig(server, cfg, { 101 | name: 'Server', 102 | invokeParams: { isClient: false, isServer: true } 103 | }) 104 | } 105 | } 106 | 107 | module.exports = function (cfg) { 108 | const mode = cfg.ctx.mode 109 | 110 | if (mode.ssr) { 111 | return getSSR(cfg) 112 | } 113 | else if (mode.electron) { 114 | return getElectron(cfg) 115 | } 116 | else if (mode.cordova) { 117 | return getCordova(cfg) 118 | } 119 | else if (mode.pwa) { 120 | return getPWA(cfg) 121 | } 122 | else { 123 | return getSPA(cfg) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 16 | -------------------------------------------------------------------------------- /templates/app/layout.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 99 | 100 | 102 | -------------------------------------------------------------------------------- /templates/app/page.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /templates/app/store/mutations.js: -------------------------------------------------------------------------------- 1 | /* 2 | export function someMutation (state) { 3 | } 4 | */ 5 | -------------------------------------------------------------------------------- /templates/app/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/electron/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarframework/quasar-cli/7c9d139e89a2ab4852fa9d9e7335f592252a1e9c/templates/electron/icons/icon.icns -------------------------------------------------------------------------------- /templates/electron/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarframework/quasar-cli/7c9d139e89a2ab4852fa9d9e7335f592252a1e9c/templates/electron/icons/icon.ico -------------------------------------------------------------------------------- /templates/electron/icons/linux-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarframework/quasar-cli/7c9d139e89a2ab4852fa9d9e7335f592252a1e9c/templates/electron/icons/linux-512x512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/entry/client-entry.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 | <% if (supportIE) { %> 11 | import 'quasar-framework/dist/quasar.ie.polyfills.js' 12 | <% } %> 13 | 14 | <% extras && extras.filter(asset => asset).forEach(asset => { %> 15 | import 'quasar-extras/<%= asset %>/<%= asset %>.css' 16 | <% }) %> 17 | 18 | <% animations && animations.filter(asset => asset).forEach(asset => { %> 19 | import 'quasar-extras/animate/<%= asset %>.css' 20 | <% }) %> 21 | 22 | import 'quasar-app-styl' 23 | 24 | <% css && css.forEach(asset => { %> 25 | import '<%= asset %>' 26 | <% }) %> 27 | 28 | import Vue from 'vue' 29 | import createApp from './app.js' 30 | 31 | <% if (ctx.mode.pwa) { %> 32 | import 'app/<%= sourceFiles.registerServiceWorker %>' 33 | <% } %> 34 | 35 | <% 36 | const pluginNames = [] 37 | if (plugins) { 38 | function hash (str) { 39 | const name = str.replace(/\W+/g, '') 40 | return name.charAt(0).toUpperCase() + name.slice(1) 41 | } 42 | plugins.filter(asset => asset.path !== 'boot' && asset.client !== false).forEach(asset => { 43 | let importName = 'p' + hash(asset.path) 44 | pluginNames.push(importName) 45 | %> 46 | import <%= importName %> from 'src/plugins/<%= asset.path %>' 47 | <% }) } %> 48 | 49 | <% if (preFetch) { %> 50 | import { addPreFetchHooks } from './client-prefetch.js' 51 | <% } %> 52 | 53 | <% 54 | const needsFastClick = ctx.mode.pwa || (ctx.mode.cordova && ctx.target.ios) 55 | if (needsFastClick) { 56 | %> 57 | import FastClick from 'fastclick' 58 | <% } %> 59 | 60 | <% if (ctx.mode.electron) { %> 61 | import electron from 'electron' 62 | Vue.prototype.$q.electron = electron 63 | <% } %> 64 | 65 | <% 66 | let hasBootPlugin = false 67 | if (!ctx.mode.ssr) { 68 | hasBootPlugin = plugins && plugins.find(asset => asset.path === 'boot') 69 | 70 | if (hasBootPlugin) { %> 71 | import boot from 'src/plugins/boot.js' 72 | <% } } %> 73 | 74 | <% if (ctx.dev) { %> 75 | Vue.config.devtools = true 76 | Vue.config.productionTip = false 77 | <% } %> 78 | 79 | <% if (ctx.dev) { %> 80 | console.info('[Quasar] Running <%= ctx.modeName.toUpperCase() + (ctx.mode.ssr && ctx.mode.pwa ? ' + PWA' : '') %> with <%= ctx.themeName.toUpperCase() %> theme.') 81 | <% if (ctx.mode.pwa) { %>console.info('[Quasar] Forcing PWA into the network-first approach to not break Hot Module Replacement while developing.')<% } %> 82 | <% } %> 83 | 84 | const { app, <%= store ? 'store, ' : '' %>router } = createApp() 85 | 86 | <% if (needsFastClick) { %> 87 | <% if (ctx.mode.pwa) { %> 88 | // Needed only for iOS PWAs 89 | if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream && window.navigator.standalone) { 90 | <% } %> 91 | document.addEventListener('DOMContentLoaded', () => { 92 | FastClick.attach(document.body) 93 | }, false) 94 | <% if (ctx.mode.pwa) { %> 95 | } 96 | <% } %> 97 | <% } %> 98 | 99 | <% if (pluginNames.length > 0) { %> 100 | ;[<%= pluginNames.join(',') %>].forEach(plugin => { 101 | plugin({ 102 | app, 103 | router, 104 | <%= store ? 'store,' : '' %> 105 | Vue, 106 | ssrContext: null 107 | }) 108 | }) 109 | <% } %> 110 | 111 | <% if (ctx.mode.ssr) { %> 112 | 113 | // prime the store with server-initialized state. 114 | // the state is determined during SSR and inlined in the page markup. 115 | <% if (store) { %> 116 | if (window.__INITIAL_STATE__) { 117 | store.replaceState(window.__INITIAL_STATE__) 118 | } 119 | <% } %> 120 | 121 | const appInstance = new Vue(app) 122 | 123 | // wait until router has resolved all async before hooks 124 | // and async components... 125 | router.onReady(() => { 126 | <% if (preFetch) { %> 127 | addPreFetchHooks(router<%= store ? ', store' : '' %>) 128 | <% } %> 129 | appInstance.$mount('#q-app') 130 | }) 131 | 132 | <% } else { // not SSR %> 133 | 134 | <% if (preFetch) { %> 135 | addPreFetchHooks(router<%= store ? ', store' : '' %>) 136 | <% } %> 137 | 138 | <% if (ctx.mode.cordova) { %> 139 | document.addEventListener('deviceready', () => { 140 | Vue.prototype.$q.cordova = window.cordova 141 | <% } %> 142 | 143 | <% if (hasBootPlugin) { %> 144 | boot({ app, router,<% if (store) { %> store,<% } %> Vue }) 145 | <% } else { %> 146 | new Vue(app) 147 | <% } %> 148 | 149 | <% if (ctx.mode.cordova) { %> 150 | }, false) // on deviceready 151 | <% } %> 152 | 153 | 154 | <% } // end of Non SSR %> 155 | -------------------------------------------------------------------------------- /templates/entry/client-prefetch.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 App from 'app/<%= sourceFiles.rootComponent %>' 11 | 12 | <% if (__loadingBar) { %> 13 | import { LoadingBar } from 'quasar' 14 | <% } %> 15 | 16 | <% if (!ctx.mode.ssr) { %> 17 | let appPrefetch = typeof App.preFetch === 'function' 18 | <% } %> 19 | 20 | function getMatchedComponents (to, router) { 21 | const route = to 22 | ? (to.matched ? to : router.resolve(to).route) 23 | : router.currentRoute 24 | 25 | if (!route) { return [] } 26 | return [].concat.apply([], route.matched.map(m => { 27 | return Object.keys(m.components).map(key => { 28 | return { 29 | path: m.path, 30 | c: m.components[key] 31 | } 32 | }) 33 | })) 34 | } 35 | 36 | export function addPreFetchHooks (router<%= store ? ', store' : '' %>) { 37 | // Add router hook for handling preFetch. 38 | // Doing it after initial route is resolved so that we don't double-fetch 39 | // the data that we already have. Using router.beforeResolve() so that all 40 | // async components are resolved. 41 | router.beforeResolve((to, from, next) => { 42 | const 43 | matched = getMatchedComponents(to, router), 44 | prevMatched = getMatchedComponents(from, router) 45 | 46 | let diffed = false 47 | const components = matched 48 | .filter((m, i) => { 49 | return diffed || (diffed = ( 50 | !prevMatched[i] || 51 | prevMatched[i].c !== m.c || 52 | m.path.indexOf('/:') > -1 // does it has params? 53 | )) 54 | }) 55 | .filter(m => m.c && typeof m.c.preFetch === 'function') 56 | .map(m => m.c) 57 | 58 | <% if (!ctx.mode.ssr) { %> 59 | if (appPrefetch) { 60 | appPrefetch = false 61 | components.unshift(App) 62 | } 63 | <% } %> 64 | 65 | if (!components.length) { return next() } 66 | 67 | let routeUnchanged = true 68 | const redirect = url => { 69 | routeUnchanged = false 70 | next(url) 71 | } 72 | const proceed = () => { 73 | <% if (__loadingBar) { %> 74 | LoadingBar.stop() 75 | <% } %> 76 | if (routeUnchanged) { next() } 77 | } 78 | 79 | <% if (__loadingBar) { %> 80 | LoadingBar.start() 81 | <% } %> 82 | 83 | components 84 | .filter(c => c && c.preFetch) 85 | .reduce( 86 | (promise, c) => promise.then(() => routeUnchanged && c.preFetch({ 87 | <% if (store) { %>store,<% } %> 88 | currentRoute: to, 89 | previousRoute: from, 90 | redirect 91 | })), 92 | Promise.resolve() 93 | ) 94 | .then(proceed) 95 | .catch(e => { 96 | console.error(e) 97 | proceed() 98 | }) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/entry/server-entry.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 | <% extras && extras.filter(asset => asset).forEach(asset => { %> 11 | import 'quasar-extras/<%= asset %>/<%= asset %>.css' 12 | <% }) %> 13 | 14 | <% animations && animations.filter(asset => asset).forEach(asset => { %> 15 | import 'quasar-extras/animate/<%= asset %>.css' 16 | <% }) %> 17 | 18 | import 'quasar-app-styl' 19 | 20 | <% css && css.forEach(asset => { %> 21 | import '<%= asset %>' 22 | <% }) %> 23 | 24 | import createApp from './app.js' 25 | import Vue from 'vue' 26 | <% if (preFetch) { %> 27 | import App from 'app/<%= sourceFiles.rootComponent %>' 28 | <% } %> 29 | 30 | <% 31 | const pluginNames = [] 32 | if (plugins) { 33 | function hash (str) { 34 | const name = str.replace(/\W+/g, '') 35 | return name.charAt(0).toUpperCase() + name.slice(1) 36 | } 37 | plugins.filter(asset => asset.path !== 'boot' && asset.server !== false).forEach(asset => { 38 | let importName = 'plugin' + hash(asset.path) 39 | pluginNames.push(importName) 40 | %> 41 | import <%= importName %> from 'src/plugins/<%= asset.path %>' 42 | <% }) } %> 43 | 44 | // This exported function will be called by `bundleRenderer`. 45 | // This is where we perform data-prefetching to determine the 46 | // state of our application before actually rendering it. 47 | // Since data fetching is async, this function is expected to 48 | // return a Promise that resolves to the app instance. 49 | export default context => { 50 | return new Promise(async (resolve, reject) => { 51 | const { app, <%= store ? 'store, ' : '' %>router } = createApp(context) 52 | 53 | <% if (pluginNames.length > 0) { %> 54 | ;[<%= pluginNames.join(',') %>].forEach(plugin => { 55 | plugin({ 56 | app, 57 | router, 58 | <%= store ? 'store,' : '' %> 59 | Vue, 60 | ssrContext: context 61 | }) 62 | }) 63 | <% } %> 64 | 65 | const 66 | { url } = context, 67 | { fullPath } = router.resolve(url).route 68 | 69 | if (fullPath !== url) { 70 | return reject({ url: fullPath }) 71 | } 72 | 73 | // set router's location 74 | router.push(url) 75 | 76 | // wait until router has resolved possible async hooks 77 | router.onReady(() => { 78 | const matchedComponents = router.getMatchedComponents() 79 | // no matched routes 80 | if (!matchedComponents.length) { 81 | return reject({ code: 404 }) 82 | } 83 | 84 | <% if (preFetch) { %> 85 | 86 | let routeUnchanged = true 87 | const redirect = url => { 88 | routeUnchanged = false 89 | reject({ url }) 90 | } 91 | App.preFetch && matchedComponents.unshift(App) 92 | 93 | // Call preFetch hooks on components matched by the route. 94 | // A preFetch hook dispatches a store action and returns a Promise, 95 | // which is resolved when the action is complete and store state has been 96 | // updated. 97 | matchedComponents 98 | .filter(c => c && c.preFetch) 99 | .reduce( 100 | (promise, c) => promise.then(() => routeUnchanged && c.preFetch({ 101 | <% if (store) { %>store,<% } %> 102 | ssrContext: context, 103 | currentRoute: router.currentRoute, 104 | redirect 105 | })), 106 | Promise.resolve() 107 | ) 108 | .then(() => { 109 | if (!routeUnchanged) { return } 110 | 111 | <% if (store) { %>context.state = store.state<% } %> 112 | 113 | <% if (__meta) { %> 114 | const App = new Vue(app) 115 | context.$getMetaHTML = App.$getMetaHTML(App) 116 | resolve(App) 117 | <% } else { %> 118 | resolve(new Vue(app)) 119 | <% } %> 120 | }) 121 | .catch(reject) 122 | 123 | <% } else { %> 124 | 125 | <% if (store) { %>context.state = store.state<% } %> 126 | 127 | <% if (__meta) { %> 128 | const App = new Vue(app) 129 | context.$getMetaHTML = App.$getMetaHTML(App) 130 | resolve(App) 131 | <% } else { %> 132 | resolve(new Vue(app)) 133 | <% } %> 134 | 135 | <% } %> 136 | }, reject) 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------