├── .editorconfig ├── .eslintrc.cjs ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── eslint.config.js ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── icon-256.ico ├── icon.icns ├── icon.ico └── icon.png ├── screenshot.png ├── server.cjs ├── src ├── App.vue ├── assets │ ├── base.css │ ├── logo.svg │ ├── main.css │ └── nw.png ├── components │ ├── ExternalLink.vue │ └── ResourceGroup.vue ├── helpers │ ├── applyPrototypes.js │ └── constants.js ├── main.js ├── router │ └── index.js ├── stores │ └── counter.js └── views │ ├── FsExample.vue │ ├── HelloWorld.vue │ ├── PiniaDemo.vue │ └── ResourceLinks.vue ├── tests └── unit │ ├── App.test.js │ ├── __snapshots__ │ └── App.test.js.snap │ ├── components │ └── ExternalLink.test.js │ ├── main.test.js │ ├── setup.js │ ├── testHelpers.js │ └── views │ ├── FsExample.test.js │ ├── HelloWorld.test.js │ ├── PiniaDemo.test.js │ └── __snapshots__ │ ├── FsExample.test.js.snap │ ├── HelloWorld.test.js.snap │ └── PiniaDemo.test.js.snap ├── vite.config.js └── waitOnConfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # Top-most EditorConfig file 4 | root = true 5 | 6 | # defaults for all files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # Markdown files uses two trailing spaces to indicate a
16 | [*.{md,snap}] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Due to a bug in the ESLint import plugin, this file is required 2 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: volta-cli/action@v4 20 | - run: node -v 21 | - run: npm -v 22 | - run: npm ci 23 | - run: npm run build:vue 24 | - run: npm run lint 25 | - run: npm t 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-vue 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # "No Ideologies" Code of Conduct 2 | 3 | The following are the guidelines we expect our community members and maintainers to follow. 4 | 5 | * * * 6 | 7 | ## Terminology and Scope 8 | 9 | **What defines a "maintainer"?** 10 | 11 | * A maintainer is anyone that interacts with the community on behalf of this project. Amount of code written is not a qualifier. A maintainer may include those who solely help in support roles such as in resolving issues, improving documentation, administrating or moderating forums/chatrooms, or any other non-coding specific roles. Maintainers also include those that are responsible for the building and upkeep of the project. 12 | 13 | **What defines a "community member"?** 14 | 15 | * Anyone interacting with this project directly, including maintainers. 16 | 17 | **What is the scope of these guidelines?** 18 | 19 | * These guidelines apply only to this project and forms of communication directly related to it, such as issue trackers, forums, chatrooms, and in person events specific to this project. If a member is violating these guidelines outside of this project or on other platforms, that is beyond our scope and any grievances should be handled on those platforms. 20 | 21 | **Discussing the guidelines:** 22 | 23 | * Discussions around these guidelines, improving, updating, or altering them, is permitted so long as the discussions do not violate any existing guidelines. 24 | 25 | * * * 26 | 27 | ## Guidelines 28 | 29 | ### Guidelines for community members 30 | 31 | This project is technical in nature and not based around any particular non-technical ideology. As such, communication that is based primarily around ideologies unrelated to the technologies used by this repository are not permitted. 32 | 33 | Any discussion or communication that is primarily focused around an ideology, be it about race, gender, politics, religion, or anything else non-technical, is not allowed. Everyone has their own ideological preferences, beliefs, and opinions. We do not seek to marginalize, exclude, or judge anyone for their ideologies. To prevent conflict between those with differing or opposing ideologies, all communication on these subjects are prohibited. Some discussions around these topics may be important, however this project is not the proper channel for these discussions. 34 | 35 | ### Guidelines for maintainers 36 | 37 | * Maintainers must abide by the same rules as all other community members mentioned above. However, in addition, maintainers are held to a higher standard, explained below. 38 | * Maintainers should answer all questions politely. 39 | * If someone is upset or angry about something, it's probably because it's difficult to use, so thank them for bringing it to your attention and address ways to solve the problem. Maintainers should focus on the content of the message, and not on how it was delivered. 40 | * A maintainer should seek to update members when an issue they brought up is resolved. 41 | 42 | * * * 43 | 44 | ## Appropriate response to violations 45 | 46 | How to respond to a community member or maintainer violating a guideline. 47 | 48 | 1. If an issue is created that violates a guideline a maintainer should close and lock the issue, explaining "This issue is in violation of our code of conduct. Please review it before posting again." with a link to this document. 49 | 1. If a member repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. 50 | 1. If a maintainer is in violation of a guideline, they should be informed of such with a link to this document. If additional actions are required of the maintainer but not taken, then other maintainers should be informed of these inactions. 51 | 1. If a maintainer repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. In addition, future contributions to this project may be ignored as well. 52 | 53 | * * * 54 | 55 | Based on version 1.0.3 from https://github.com/CodifiedConduct/coc-no-ideologies 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Jared Wilcurt 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NW.js + Vue 3 Desktop App Boilerplate 2 | 3 | The easiest, quickest, and best option for building Desktop Apps with Vue. 4 | 5 | 100% test coverage. Vue-DevTools built in. 6 | 7 | All you do is `npm install && npm start` and you got a desktop app and web app development environment with Vue-DevTools built-in. 8 | 9 | **Does this work for web or just desktop?** 10 | 11 | **Both.** This repo will build both for web and desktop and includes a simple `this.isDesktop` flag so you can add desktop specific features that won't show on the web. This repo has 100% test coverage including tests for both web and desktop builds. You could even theoretically add NativeScript-Vue into the mix and build for native mobile as well (though that is not set up in this repo). 12 | 13 | Run `npm run build` and you're ready to ship/deploy: Web App, Windows Installer, OSX and Linux apps. 14 | 15 | ![A screenshot of the default app running on Windows](screenshot.png) 16 | 17 | 18 | ## Technology colophon 19 | 20 | * **Vue 3** - Frontend framework 21 | * **NW.js** - Desktop App runtime environment 22 | * Chromium 137 23 | * Node.js 23.11.0 24 | * **Vue-DevTools** - Official Vite plugin 25 | * **Vite** - Dev Server/Bundler/App build tool 26 | * **Pinia** - Global State Management 27 | * **Vue-Router** - Frontend routing/navigation 28 | * **Options API** - [Component organizational structure](https://user-images.githubusercontent.com/4629794/204181213-6c9bcece-62fb-4790-8fc2-7df546ca7df5.png) 29 | * **Constants Plugin** - Gives you a [dedicated place to store constants](https://github.com/TheJaredWilcurt/vue-options-api-constants-plugin) in a component 30 | * **NW-Builder-Phoenix** - Automated desktop builds 31 | * **ESLint** - Code error prevention and stylistic consistency 32 | * Vue Linting 33 | * Accessibility Linting 34 | * Test Linting 35 | * Import Linting 36 | * **Vitest** - Vite-based Unit/integration/behavioral testing 37 | * 100% test coverage examples 38 | * @vue/test-utils - Test helpers for interacting with Vue in tests 39 | * vue3-snapshot-serializer - Dramatically improved snapshot formatting 40 | * @pinia/testing - Initialize Pinia in tests 41 | 42 | 43 | ## Documentation 44 | 45 | In all `.vue` components, you have access to `nw`, `global`, `process`, `require`, and the boolean `isDesktop`: 46 | 47 | ```js 48 | methods: { 49 | example: function () { 50 | if (this.isDesktop) { 51 | console.log('Your OS is ' + this.process.platform); 52 | console.log('Your AppData location is ' + this.nw.App.dataPath); 53 | // Sets a value on Node's global, meaning other windows have access to this data. 54 | this.global.cow = 'moo'; 55 | // The contents of the current directory 56 | console.log(this.require('fs').readdirSync('.')); 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | Or even directly from the template (with some slight changes to work within the Vue context): 63 | ```html 64 |
65 | Your OS is {{ process.platform }}. 66 | Your AppData location is {{ nw.App.dataPath }}. 67 | 71 | The contents of the current directory are {{ nw.require('fs').readdirSync('.') }}. 72 |
73 | ``` 74 | 75 | 76 | ## Running locally 77 | 78 | 1. Download, Fork, or Clone this repo 79 | 1. Install [Volta](https://volta.sh) 80 | * It will handle using the correct Node version based on the `package.json` 81 | 1. Run `npm install` 82 | 1. Run `npm start` 83 | 84 | 85 | ## Lint 86 | 87 | Uses rules in `./eslint.json` 88 | 89 | 1. `npm run lint` to see linting errors 90 | 1. `npm run fix` to auto-fix linting errors (where possible) 91 | 92 | 93 | ## Tests 94 | 95 | 1. `npm t` runs all unit tests and shows coverage output 96 | 1. `npm t -- -u` runs all unit tests, updating snapshots (use with care) 97 | 98 | 99 | ## Building for distribution 100 | 101 | 1. `npm run build:clean` will delete your `./dist` and `./dist-vue` folders 102 | 1. `npm run build:vue` will build just your Vue app for web distribution (`./dist-vue`) 103 | 1. `npm run build:nw` will build just your NW.js app (`./dist`) for all supported platforms (Windows, OSX, Linux 32-Bit, Linux 64-Bit) 104 | 1. `npm run build` is your all-in-one command. It will clean out the old dist folders and build your Vue and NW.js app 105 | 106 | 107 | ## IMPORTANT NOTE ABOUT BUILDS 108 | 109 | They take a long time. If you do `npm run build` expect it to take 10-30 minutes. This can be adjusted by changing the build params in the `package.json`. The more platforms and build types, the longer it takes. You can also remove the `--concurrent` from the `build:nw` script to see a status of what has been completed. This will allow individual pieces to finish faster, but the entire build will take longer. 110 | 111 | 112 | ## Removing Pinia 113 | 114 | I set up Pinia in this project to save you time (and because it's amazing). If you don't need global state management for your project, you can remove Pinia by doing the following: 115 | 116 | * Delete the `/src/store` folder and its contents 117 | * Delete `/src/views/PiniaDemo.vue` 118 | * Delete `/tests/unit/views/PiniaDemo.test.js` 119 | * Remove the Pinia Demo `RouterLink` from `/src/App.vue` 120 | * Remove import and route object relating to "PiniaDemo" from `/src/router/index.js` 121 | * Remove the lines of code from `/tests/unit/testHelpers.js` that say "pinia" 122 | * Remove the lines of code from `/package.json` that say "pinia" 123 | * `npm install && npm t -- -u` 124 | * `git add -A && git commit -m "Removed Pinia"` 125 | 126 | 127 | ## Alternatives 128 | 129 | * [Vue 3 Desktop and Mobile](https://github.com/rigor789/nw-vue3-boilerplate) - Fork of this repo but with NativeScript added to also build for Android and iOS. 130 | * [nw-vue-cli-example](https://github.com/nwutils/nw-vue-cli-example) - Uses Vue-CLI (WebPack), has Vue 2 and Vue 3 branches. 131 | * [nwjs-vue](https://github.com/elegantweb/nwjs-vue) - Uses Vue-CLI 2 132 | * [vue-desktop-basic](https://github.com/TheJaredWilcurt/vue-desktop-basic) - Does not use a build system at all, all `.vue` files run directly in the browser context 133 | * [nw-vue3-typescript-pinia](https://github.com/codeh2o/nw-vue3-typescript-pinia) - Old fork of this repo but with TS added 134 | 135 | 136 | ## Updating to newer NW.js versions 137 | 138 | 1. When updating the version of NW.js devDependency, also update these: 139 | * `package.json` version, devDeps, build nwVersion, volta 140 | * `build.target` in `vite.config.js` 141 | * Update the Chromium/Node version numbers at the top of the README 142 | 1. Bump the version number, and all the npm scripts that reference the version number 143 | 1. Run `npm run regression` after updating dependencies or other major changes to verify builds still work correctly 144 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import pluginJs from '@eslint/js'; 4 | import tjwBase from 'eslint-config-tjw-base'; 5 | import tjwImport from 'eslint-config-tjw-import'; 6 | import tjwJest from 'eslint-config-tjw-jest'; 7 | import tjwVue from 'eslint-config-tjw-vue'; 8 | import pluginImport from 'eslint-plugin-import'; 9 | import pluginJest from 'eslint-plugin-jest'; 10 | import pluginVue from 'eslint-plugin-vue'; 11 | import pluginVueA11y from 'eslint-plugin-vuejs-accessibility'; 12 | 13 | const __dirname = import.meta.dirname; 14 | const vue3Recommended = pluginVue.configs['flat/recommended']; 15 | const vueA11yRecommended = pluginVueA11y.configs['flat/recommended']; 16 | 17 | export default [ 18 | pluginJs.configs.recommended, 19 | pluginImport.flatConfigs.recommended, 20 | pluginJest.configs['flat/recommended'], 21 | ...vue3Recommended, 22 | ...vueA11yRecommended, 23 | tjwBase.configs.recommended, 24 | tjwImport, 25 | tjwJest.configs.recommended, 26 | tjwVue, 27 | { 28 | // project specific rules/settings 29 | languageOptions: { 30 | globals: { 31 | vi: true 32 | } 33 | }, 34 | rules: { 35 | 'import/no-unused-modules': 'off', 36 | 'import/no-extraneous-dependencies': 'off', 37 | 38 | // If this is not turned off, linting throws because it can't find 'jest' install 39 | 'jest/no-deprecated-functions': 'off', 40 | 'vuejs-accessibility/label-has-for': [ 41 | 'error', 42 | { 43 | components: ['Label'], 44 | required: { 45 | some: ['nesting', 'id'] 46 | }, 47 | allowChildren: false 48 | } 49 | ] 50 | }, 51 | settings: { 52 | 'import/resolver': { 53 | vite: { 54 | viteConfig: { 55 | resolve: { 56 | alias: { 57 | '@': path.resolve(__dirname, 'src'), 58 | '@@': path.resolve(__dirname, 'tests'), 59 | '@@@': path.resolve(__dirname, 'docs') 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | ]; 68 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Your App Name Here 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"], 6 | "@@/*": ["./tests/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "ManifestComments": [ 3 | "Only add dependencies that you want shipped to the end user, for everything else, use devDependencies, including things that will be bundled by Vite.", 4 | "NW.js requires a name and a main, everything else is optional.", 5 | "The build section is used by nwjs-builder-phoenix, see its documentation for more info", 6 | "To turn spell checking off, remove it from the chromium-args in this file", 7 | "Unit tests inaccurately report 1 line of code not being covered because this bug: vitejs/vite-plugin-vue #368" 8 | ], 9 | "name": "your-app-name", 10 | "version": "1.1.0", 11 | "main": "http://localhost:4175", 12 | "node-remote": "http://localhost:4175", 13 | "node-main": "", 14 | "window": { 15 | "width": 960, 16 | "height": 600, 17 | "min_width": 800, 18 | "min_height": 500, 19 | "icon": "public/icon.png" 20 | }, 21 | "private": true, 22 | "scripts": { 23 | "postinstall": "npx --yes base-volta-off-of-nwjs@latest", 24 | "start": "concurrently -k npm:dev:web npm:dev:desktop", 25 | "dev:web": "vite --port 4175", 26 | "dev:desktop": "wait-on -c waitOnConfig.json http-get://localhost:4175 && nw .", 27 | "build": "npm run build:clean && npm run build:vue && npm run build:nw", 28 | "build:clean": "rimraf ./dist-vue ./dist", 29 | "build:vue": "vite build", 30 | "build:nw": "build --concurrent --tasks win-x86,linux-x86,linux-x64,mac-x64 --mirror https://dl.nwjs.io/ .", 31 | "build:win": "npm run build:win:clean && npm run build:vue && build --tasks win-x86 --mirror https://dl.nwjs.io/ .", 32 | "build:lin": "npm run build:lin:clean && npm run build:vue && build --tasks linux-x64 --mirror https://dl.nwjs.io/ .", 33 | "build:win:clean": "rimraf ./dist-vue ./dist/your-app-name-1.1.0-win-x86 ./dist/your-app-name-1.1.0-win-x86.zip ./dist/your-app-name-1.1.0-win-x86.7z ./dist/your-app-name-1.1.0-win-x86-Setup.exe", 34 | "build:lin:clean": "rimraf ./dist-vue ./dist/your-app-name-1.1.0-linux-x64 ./dist/your-app-name-1.1.0-linux-x64.zip", 35 | "run:win": "dist\\your-app-name-1.1.0-win-x86\\your-app-name.exe", 36 | "run:lin": "./dist/your-app-name-1.1.0-linux-x64/your-app-name", 37 | "regression": "rd /s /q node_modules & rd /s /q node_modules & rd /s /q node_modules & npm install && npm run lint && npm test && npm run build:win && npm run run:win", 38 | "regression:lin": "rm -r -f node_modules && npm install && npm run lint && npm test && npm run build:lin && npm run run:lin", 39 | "lint": "eslint --ext .js,.vue src tests", 40 | "fix": "npm run lint -- --fix", 41 | "test": "vitest run --coverage" 42 | }, 43 | "dependencies": { 44 | "express": "^5.1.0" 45 | }, 46 | "devDependencies": { 47 | "@eslint/js": "^9.27.0", 48 | "@pinia/testing": "^1.0.1", 49 | "@stylistic/eslint-plugin": "^4.4.0", 50 | "@vitejs/plugin-vue": "^5.2.4", 51 | "@vitest/coverage-v8": "^3.1.4", 52 | "@vue/test-utils": "^2.4.6", 53 | "concurrently": "^9.1.0", 54 | "eslint": "^9.27.0", 55 | "eslint-config-tjw-base": "^4.2.0", 56 | "eslint-config-tjw-import": "^2.0.0", 57 | "eslint-config-tjw-jest": "^3.0.0", 58 | "eslint-config-tjw-vue": "^4.0.0", 59 | "eslint-import-resolver-vite": "^2.1.0", 60 | "eslint-plugin-import": "^2.31.0", 61 | "eslint-plugin-jest": "^28.11.0", 62 | "eslint-plugin-vue": "^10.1.0", 63 | "eslint-plugin-vuejs-accessibility": "^2.4.1", 64 | "globals": "^16.2.0", 65 | "happy-dom": "^17.4.7", 66 | "nw": "0.100.0-sdk", 67 | "nwjs-builder-phoenix": "^1.15.0", 68 | "pinia": "^3.0.2", 69 | "rimraf": "^6.0.1", 70 | "vite": "^6.3.5", 71 | "vite-plugin-vue-devtools": "^7.7.6", 72 | "vitest": "^3.1.4", 73 | "vue": "^3.5.15", 74 | "vue-options-api-constants-plugin": "^1.0.3", 75 | "vue-router": "^4.5.1", 76 | "vue3-snapshot-serializer": "^2.10.0", 77 | "wait-on": "^8.0.3" 78 | }, 79 | "chromium-args": "--enable-spell-checking --disable-features=ProcessPerSiteUpToMainFrameThreshold", 80 | "build": { 81 | "nwVersion": "v0.100.0", 82 | "nwFlavor": "normal", 83 | "targets": [ 84 | "zip", 85 | "nsis7z" 86 | ], 87 | "files": [ 88 | "**/*" 89 | ], 90 | "excludes": [ 91 | "src/**/*", 92 | "tests/**/*", 93 | "public/**/*", 94 | "dist-vue/**/*.js.map", 95 | "dist-vue/**/*.css.map", 96 | "dist-vue/**/*-legacy*", 97 | ".editorconfig", 98 | ".eslintignore", 99 | ".eslintrc", 100 | ".gitignore", 101 | "CODE_OF_CONDUCT.md", 102 | "index.html", 103 | "jsconfig.json", 104 | "package-lock.json", 105 | "screenshot.png", 106 | "vite.config.js", 107 | "waitOnConfig.json" 108 | ], 109 | "strippedProperties": [ 110 | "ManifestComments", 111 | "scripts", 112 | "devDependencies", 113 | "build", 114 | "volta" 115 | ], 116 | "overriddenProperties": { 117 | "main": "http://localhost:4185", 118 | "node-remote": "http://localhost:4185", 119 | "node-main": "server.cjs", 120 | "chromium-args": "--enable-spell-checking", 121 | "window": { 122 | "width": 960, 123 | "height": 600, 124 | "min_width": 700, 125 | "min_height": 500, 126 | "icon": "dist-vue/icon.png" 127 | } 128 | }, 129 | "win": { 130 | "icon": "public/icon-256.ico" 131 | }, 132 | "mac": { 133 | "icon": "public/icon.icns" 134 | }, 135 | "nsis": { 136 | "icon": "public/icon-256.ico", 137 | "unIcon": "public/icon.ico", 138 | "languages": [ 139 | "English" 140 | ], 141 | "diffUpdaters": false, 142 | "hashCalculation": true 143 | } 144 | }, 145 | "type": "module", 146 | "volta": { 147 | "node": "23.11.0" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/public/icon-256.ico -------------------------------------------------------------------------------- /public/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/public/icon.icns -------------------------------------------------------------------------------- /public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/public/icon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/public/icon.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/screenshot.png -------------------------------------------------------------------------------- /server.cjs: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | app.use(express.static('./dist-vue')); 5 | app.listen(4185); 6 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 32 | 33 | 56 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from https://github.com/vuejs/theme */ 2 | :root { 3 | --vt-c-white: #FFFFFF; 4 | --vt-c-black: #181818; 5 | --vt-c-text-light-1: var(--vt-c-indigo); 6 | --vt-c-text-dark-1: var(--vt-c-white); 7 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 8 | } 9 | 10 | /* semantic color variables for this project */ 11 | :root { 12 | --color-background: var(--vt-c-white); 13 | --color-heading: var(--vt-c-text-light-1); 14 | --color-text: var(--vt-c-text-light-1); 15 | --section-gap: 160px; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | :root { 20 | --color-background: var(--vt-c-black); 21 | --color-heading: var(--vt-c-text-dark-1); 22 | --color-text: var(--vt-c-text-dark-2); 23 | } 24 | } 25 | 26 | *, 27 | *::before, 28 | *::after { 29 | position: relative; 30 | margin: 0; 31 | font-weight: normal; 32 | box-sizing: border-box; 33 | } 34 | 35 | body { 36 | min-height: 100vh; 37 | background: var(--color-background); 38 | color: var(--color-text); 39 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 40 | font-size: 15px; 41 | line-height: 1.6; 42 | text-rendering: optimizeLegibility; 43 | -webkit-font-smoothing: antialiased; 44 | -moz-osx-font-smoothing: grayscale; 45 | transition: color 0.5s, background-color 0.5s; 46 | } 47 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0px auto; 6 | padding: 2rem; 7 | font-weight: normal; 8 | } 9 | 10 | a, 11 | .green { 12 | color: hsla(160, 100%, 37%, 1); 13 | text-decoration: none; 14 | transition: 0.4s; 15 | } 16 | 17 | button { 18 | background: hsla(160, 100%, 37%, 1); 19 | border: 0px; 20 | border-radius: 3px; 21 | padding: 4px 6px; 22 | } 23 | button:hover { 24 | background: hsla(160, 100%, 40%, 1); 25 | } 26 | button:active { 27 | background: hsla(160, 100%, 43%, 1); 28 | } 29 | 30 | strong { 31 | font-weight: bold; 32 | } 33 | 34 | hr { 35 | width: 100%; 36 | background: var(--color-text); 37 | margin: 15px 0px; 38 | } 39 | 40 | textarea { 41 | width: 100%; 42 | min-height: 15rem; 43 | background: #333; 44 | border: 0px; 45 | margin-top: 1rem; 46 | padding: 0.5rem; 47 | color: #CCC; 48 | } 49 | 50 | .flex { 51 | display: flex; 52 | } 53 | .container { 54 | display: inline-block; 55 | min-width: 200px; 56 | margin-right: 2rem; 57 | vertical-align: top; 58 | } 59 | 60 | .mt-1 { 61 | margin-top: 1rem; 62 | } 63 | 64 | .mb-2 { 65 | margin-bottom: 2rem; 66 | } 67 | 68 | .center { 69 | text-align: center; 70 | } 71 | 72 | @media (hover: hover) { 73 | a:hover { 74 | background-color: hsla(160, 100%, 37%, 0.2); 75 | } 76 | } 77 | 78 | @media (min-width: 1024px) { 79 | body { 80 | display: flex; 81 | flex-direction: row; 82 | justify-content: center; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/nw-vue3-boilerplate/5ddc8fc4bad195c23ab6d047e92d54616473263c/src/assets/nw.png -------------------------------------------------------------------------------- /src/components/ExternalLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | -------------------------------------------------------------------------------- /src/components/ResourceGroup.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 43 | -------------------------------------------------------------------------------- /src/helpers/applyPrototypes.js: -------------------------------------------------------------------------------- 1 | // Make NW.js and Node globals available in Vue 2 | export default function applyPrototypes (component) { 3 | component.isDesktop = Boolean(window.nw); 4 | if (window.nw) { 5 | component.nw = window.nw; 6 | component.process = window.nw.process; 7 | component.require = window.nw.require; 8 | component.global = global; 9 | } 10 | return component; 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/constants.js: -------------------------------------------------------------------------------- 1 | export const APP_NAME = 'Your App Name Here'; 2 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import { createApp } from 'vue'; 3 | import constantsPlugin from 'vue-options-api-constants-plugin'; 4 | 5 | import { router } from '@/router/index.js'; 6 | 7 | import applyPrototypes from '@/helpers/applyPrototypes.js'; 8 | 9 | import App from '@/App.vue'; 10 | 11 | import '@/assets/main.css'; 12 | 13 | export const app = createApp(App); 14 | applyPrototypes(app.config.globalProperties); 15 | app.use(createPinia()); 16 | app.use(router); 17 | app.use(constantsPlugin); 18 | app.mount('#app'); 19 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | 3 | import FsExample from '@/views/FsExample.vue'; 4 | import HelloWorld from '@/views/HelloWorld.vue'; 5 | import PiniaDemo from '@/views/PiniaDemo.vue'; 6 | import ResourceLinks from '@/views/ResourceLinks.vue'; 7 | 8 | export const router = createRouter({ 9 | history: createWebHashHistory(), 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'welcome', 14 | component: HelloWorld 15 | }, 16 | { 17 | path: '/fs', 18 | name: 'fs', 19 | component: FsExample 20 | }, 21 | { 22 | path: '/pinia', 23 | name: 'pinia', 24 | component: PiniaDemo 25 | }, 26 | { 27 | path: '/resources', 28 | name: 'resources', 29 | component: ResourceLinks 30 | }, 31 | { 32 | path: '/:catchAll(.*)*', 33 | name: '404', 34 | redirect: '/' 35 | } 36 | ] 37 | }); 38 | -------------------------------------------------------------------------------- /src/stores/counter.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const counterStore = defineStore('counter', { 4 | state: function () { 5 | return { 6 | count: 2 7 | }; 8 | }, 9 | actions: { 10 | resetState: function () { 11 | this.count = 2; 12 | }, 13 | incrementCount: function () { 14 | this.count = this.count + 1; 15 | } 16 | }, 17 | getters: { 18 | doubledCount: function (state) { 19 | return state.count * 2; 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/views/FsExample.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 62 | 63 | 72 | -------------------------------------------------------------------------------- /src/views/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 84 | 85 | 116 | -------------------------------------------------------------------------------- /src/views/PiniaDemo.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 38 | -------------------------------------------------------------------------------- /src/views/ResourceLinks.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 66 | -------------------------------------------------------------------------------- /tests/unit/App.test.js: -------------------------------------------------------------------------------- 1 | import App from '@/App.vue'; 2 | 3 | import testHelpers from '@@/unit/testHelpers.js'; 4 | 5 | describe('App.vue', () => { 6 | const setupWrapper = function () { 7 | return testHelpers.mount(App); 8 | }; 9 | 10 | describe('Desktop', () => { 11 | test('Render default contents', () => { 12 | const wrapper = setupWrapper(); 13 | 14 | expect(wrapper) 15 | .toMatchSnapshot(); 16 | }); 17 | }); 18 | 19 | describe('Web', () => { 20 | beforeEach(() => { 21 | window.webSetup(); 22 | }); 23 | 24 | test('Render default contents', () => { 25 | const wrapper = setupWrapper(); 26 | 27 | expect(wrapper) 28 | .toMatchSnapshot(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/App.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`App.vue > Desktop > Render default contents 1`] = ` 4 |
5 | 31 |
32 |
33 | 34 |
35 | `; 36 | 37 | exports[`App.vue > Web > Render default contents 1`] = ` 38 |
39 | 64 |
65 |
66 |
67 |
68 | 69 | Vue logo 73 |
74 |

75 | Your App Name Here 76 |

77 |

78 | Vue Boilerplate 79 |

80 |

81 | This boilerplate uses 82 | 86 | NW.js 87 | 88 | + 89 | 93 | Vite 94 | 95 | + 96 | 100 | Vue 3 101 | 102 | + 103 | 107 | Pinia 108 | 109 | + 110 | 114 | Vue-Router 115 | 116 | + the 117 | 121 | superior Options API 122 | 123 | . 124 |

125 |

126 | Notice that the 127 | 128 | same app conditionally renders 129 | 130 | different content in a normal browser than 131 | 132 | when ran as a desktop app 133 | 134 | . 135 |

136 |
137 |
138 | `; 139 | -------------------------------------------------------------------------------- /tests/unit/components/ExternalLink.test.js: -------------------------------------------------------------------------------- 1 | import ExternalLink from '@/components/ExternalLink.vue'; 2 | 3 | import testHelpers from '@@/unit/testHelpers.js'; 4 | 5 | describe('ExternalLink.vue', () => { 6 | // Props 7 | const name = 'Test'; 8 | const url = 'example.com'; 9 | 10 | // Data 11 | const fullUrl = 'https://' + url; 12 | 13 | beforeEach(() => { 14 | global.vueSnapshots.formatting.attributesPerLine = 2; 15 | global.vueSnapshots.formatting.tagsWithWhitespacePreserved = ['a']; 16 | }); 17 | 18 | const setupWrapper = async () => { 19 | const requiredProps = { name, url }; 20 | const options = { 21 | props: requiredProps 22 | }; 23 | const wrapper = await testHelpers.shallowMount(ExternalLink, options); 24 | return wrapper; 25 | }; 26 | 27 | describe('Desktop', () => { 28 | test('Render default contents', async () => { 29 | const wrapper = await setupWrapper(); 30 | 31 | expect(wrapper) 32 | .toMatchInlineSnapshot(` 33 | Test 34 | `); 35 | }); 36 | 37 | test('Click event', async () => { 38 | const wrapper = await setupWrapper(); 39 | 40 | await wrapper.find('[data-test="link"]').trigger('click'); 41 | 42 | expect(window.nw.Shell.openExternal) 43 | .toHaveBeenCalledWith(fullUrl); 44 | }); 45 | }); 46 | 47 | describe('Web', () => { 48 | beforeEach(() => { 49 | window.webSetup(); 50 | }); 51 | 52 | test('Render default contents', async () => { 53 | const wrapper = await setupWrapper(); 54 | 55 | expect(wrapper) 56 | .toMatchInlineSnapshot(` 57 | Test 58 | `); 59 | }); 60 | 61 | test('Click event', async () => { 62 | const wrapper = await setupWrapper(); 63 | 64 | await wrapper.find('[data-test="link"]').trigger('click'); 65 | 66 | expect(window.open) 67 | .toHaveBeenCalledWith(fullUrl, '_blank'); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/unit/main.test.js: -------------------------------------------------------------------------------- 1 | import { app } from '@/main.js'; 2 | 3 | describe('main.js', () => { 4 | test('Creates Vue app', () => { 5 | expect(app?._component?.name) 6 | .toEqual('App'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/unit/setup.js: -------------------------------------------------------------------------------- 1 | global.document = global.window.document; 2 | document.body.innerHTML = '
'; 3 | const { getComputedStyle } = window; 4 | 5 | // Prevents console log message to install Vue DevTools 6 | // Vue.config.devtools = false; 7 | 8 | // Monkeypatch JSDOM missing transition styles + vue-test-utils not properly stubbing transitions 9 | // in globally included libs 10 | // https://github.com/vuejs/vue-test-utils/issues/839#issuecomment-410474714 11 | window.getComputedStyle = function getComputedStyleStub (el) { 12 | return { 13 | ...getComputedStyle(el), 14 | transitionDelay: '', 15 | transitionDuration: '', 16 | animationDelay: '', 17 | animationDuration: '' 18 | }; 19 | }; 20 | 21 | window.webSetup = function () { 22 | delete window.nw; 23 | window.open = vi.fn(); 24 | }; 25 | 26 | global.beforeEach(() => { 27 | global.vueSnapshots = { 28 | formatting: { 29 | tagsWithWhitespacePreserved: ['pre'] 30 | } 31 | }; 32 | const showDevTools = vi.fn(); 33 | window.nw = { 34 | process: { 35 | cwd: process.cwd, 36 | env: { 37 | NODE_ENV: 'development' 38 | }, 39 | versions: { 40 | 'nw-flavor': 'sdk' 41 | } 42 | }, 43 | require: vi.fn((module) => { 44 | if (module === 'fs') { 45 | return { 46 | readdirSync: vi.fn(() => { 47 | return ['example-file-1.ext', 'example-file-2.ext']; 48 | }) 49 | }; 50 | } 51 | }), 52 | Shell: { 53 | openExternal: vi.fn() 54 | }, 55 | Window: { 56 | get: vi.fn(() => { 57 | return { 58 | showDevTools 59 | }; 60 | }) 61 | } 62 | }; 63 | window.open = vi.fn(); 64 | }); 65 | 66 | global.afterEach(() => { 67 | if (window.nw) { 68 | window.nw.Window.get().showDevTools.mockClear(); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /tests/unit/testHelpers.js: -------------------------------------------------------------------------------- 1 | import { createTestingPinia } from '@pinia/testing'; 2 | import { mount, shallowMount } from '@vue/test-utils'; 3 | import constantsPlugin from 'vue-options-api-constants-plugin'; 4 | 5 | import { router } from '@/router/index.js'; 6 | 7 | import applyPrototypes from '@/helpers/applyPrototypes.js'; 8 | 9 | const pinia = createTestingPinia({ stubActions: false }); 10 | 11 | export default { 12 | setupOptions: function (options) { 13 | options = options || {}; 14 | options.global = options.global || {}; 15 | 16 | options.global.config = options.global.config || {}; 17 | options.global.config.globalProperties = options.global.config.globalProperties || {}; 18 | applyPrototypes(options.global.config.globalProperties); 19 | 20 | options.global.plugins = [ 21 | constantsPlugin, 22 | pinia, 23 | router 24 | ]; 25 | 26 | return options; 27 | }, 28 | shallowMount: function (component, options) { 29 | options = this.setupOptions(options); 30 | const wrapper = shallowMount(component, options); 31 | return wrapper; 32 | }, 33 | mount: function (component, options) { 34 | options = this.setupOptions(options); 35 | const wrapper = mount(component, options); 36 | return wrapper; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /tests/unit/views/FsExample.test.js: -------------------------------------------------------------------------------- 1 | import FsExample from '@/views/FsExample.vue'; 2 | 3 | import testHelpers from '@@/unit/testHelpers.js'; 4 | 5 | describe('FsExample.vue', () => { 6 | const setupWrapper = async () => { 7 | const wrapper = await testHelpers.shallowMount(FsExample); 8 | return wrapper; 9 | }; 10 | 11 | describe('Desktop', () => { 12 | test('Render default contents', async () => { 13 | const wrapper = await setupWrapper(); 14 | 15 | expect(wrapper) 16 | .toMatchSnapshot(); 17 | }); 18 | 19 | test('Click button', async () => { 20 | const wrapper = await setupWrapper(); 21 | 22 | const button = wrapper.find('[data-test="fsExampleButton"]'); 23 | await button.trigger('click'); 24 | 25 | expect(window.nw.require) 26 | .toHaveBeenCalledWith('fs'); 27 | 28 | expect(wrapper) 29 | .toMatchSnapshot(); 30 | }); 31 | 32 | test('Error state', async () => { 33 | window.nw.require.mockImplementation((module) => { 34 | if (module === 'fs') { 35 | return new Error(); 36 | } 37 | }); 38 | 39 | const wrapper = await setupWrapper(); 40 | 41 | const button = wrapper.find('[data-test="fsExampleButton"]'); 42 | await button.trigger('click'); 43 | 44 | expect(window.nw.require) 45 | .toHaveBeenCalledWith('fs'); 46 | 47 | expect(wrapper) 48 | .toMatchSnapshot(); 49 | }); 50 | }); 51 | 52 | describe('Web', () => { 53 | beforeEach(() => { 54 | window.webSetup(); 55 | }); 56 | 57 | test('Render default contents', async () => { 58 | const wrapper = await setupWrapper(); 59 | 60 | expect(wrapper) 61 | .toMatchSnapshot(); 62 | }); 63 | 64 | test('Button function inaccessible', async () => { 65 | const wrapper = await setupWrapper(); 66 | 67 | await wrapper.vm.getCurrentDirectory(); 68 | 69 | expect(wrapper.vm.error) 70 | .toEqual(false); 71 | 72 | expect(wrapper.vm.contents) 73 | .toEqual([]); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/unit/views/HelloWorld.test.js: -------------------------------------------------------------------------------- 1 | import HelloWorld from '@/views/HelloWorld.vue'; 2 | 3 | import testHelpers from '@@/unit/testHelpers.js'; 4 | 5 | describe('HelloWorld.vue', () => { 6 | const setupWrapper = async () => { 7 | const wrapper = await testHelpers.shallowMount(HelloWorld); 8 | return wrapper; 9 | }; 10 | 11 | describe('Desktop', () => { 12 | test('Render default contents', async () => { 13 | const wrapper = await setupWrapper(); 14 | 15 | expect(wrapper) 16 | .toMatchSnapshot(); 17 | }); 18 | 19 | test('App name and message displays correctly', async () => { 20 | const wrapper = await setupWrapper(); 21 | 22 | expect(wrapper.find('[data-test="appName"]').text()) 23 | .toEqual('Your App Name Here'); 24 | 25 | expect(wrapper.find('[data-test="message"]').text()) 26 | .toEqual('NW.js & Vue Boilerplate'); 27 | }); 28 | 29 | test('Open dev tools', async () => { 30 | const wrapper = await setupWrapper(); 31 | 32 | const button = wrapper.find('[data-test="devToolsButton"]'); 33 | await button.trigger('click'); 34 | 35 | expect(window.nw.Window.get().showDevTools) 36 | .toHaveBeenCalled(); 37 | }); 38 | 39 | test('App opens in default browser', async () => { 40 | const wrapper = await setupWrapper(); 41 | 42 | const button = wrapper.find('[data-test="openAppInBrowserButton"]'); 43 | await button.trigger('click'); 44 | 45 | expect(window.nw.Shell.openExternal) 46 | .toHaveBeenCalledWith('http://localhost:3000/#/'); 47 | }); 48 | }); 49 | 50 | describe('Web', () => { 51 | beforeEach(() => { 52 | window.webSetup(); 53 | }); 54 | 55 | test('Render default contents', async () => { 56 | const wrapper = await setupWrapper(); 57 | 58 | expect(wrapper) 59 | .toMatchSnapshot(); 60 | }); 61 | 62 | test('Message displays correctly', async () => { 63 | const wrapper = await setupWrapper(); 64 | 65 | expect(wrapper.find('[data-test="message"]').text()) 66 | .toEqual('Vue Boilerplate'); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/unit/views/PiniaDemo.test.js: -------------------------------------------------------------------------------- 1 | import PiniaDemo from '@/views/PiniaDemo.vue'; 2 | 3 | import testHelpers from '@@/unit/testHelpers.js'; 4 | 5 | describe('PiniaDemo.vue', () => { 6 | const setupWrapper = async () => { 7 | const wrapper = await testHelpers.mount(PiniaDemo); 8 | wrapper.vm.counterStore.resetState(); 9 | return wrapper; 10 | }; 11 | 12 | describe('Desktop', () => { 13 | test('Render default contents', async () => { 14 | const wrapper = await setupWrapper(); 15 | 16 | expect(wrapper) 17 | .toMatchSnapshot(); 18 | }); 19 | 20 | test('Click increment', async () => { 21 | const wrapper = await setupWrapper(); 22 | 23 | const button = wrapper.find('[data-test="incrementButton"]'); 24 | await button.trigger('click'); 25 | 26 | expect(wrapper) 27 | .toMatchSnapshot(); 28 | }); 29 | }); 30 | 31 | describe('Web', () => { 32 | beforeEach(() => { 33 | window.webSetup(); 34 | }); 35 | 36 | test('Render default contents', async () => { 37 | const wrapper = await setupWrapper(); 38 | 39 | expect(wrapper) 40 | .toMatchSnapshot(); 41 | }); 42 | 43 | test('Click increment', async () => { 44 | const wrapper = await setupWrapper(); 45 | 46 | const button = wrapper.find('[data-test="incrementButton"]'); 47 | await button.trigger('click'); 48 | 49 | expect(wrapper) 50 | .toMatchSnapshot(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/unit/views/__snapshots__/FsExample.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`FsExample.vue > Desktop > Click button 1`] = ` 4 |
5 |

6 | 📁 File System Example: 7 |

8 | 9 | 12 |

13 | 14 | The contents of the current working directory: 15 | 16 |

17 | 25 |
26 | `; 27 | 28 | exports[`FsExample.vue > Desktop > Error state 1`] = ` 29 |
30 |

31 | 📁 File System Example: 32 |

33 |
34 | There was an error attempting to read from the file system. 35 |
36 | 39 | 40 |
41 | `; 42 | 43 | exports[`FsExample.vue > Desktop > Render default contents 1`] = ` 44 |
45 |

46 | 📁 File System Example: 47 |

48 | 49 | 52 | 53 |
54 | `; 55 | 56 | exports[`FsExample.vue > Web > Render default contents 1`] = ``; 57 | -------------------------------------------------------------------------------- /tests/unit/views/__snapshots__/HelloWorld.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`HelloWorld.vue > Desktop > Render default contents 1`] = ` 4 |
5 |
6 | NW.js logo 10 | Vue logo 14 |
15 |

16 | Your App Name Here 17 |

18 |

19 | NW.js & Vue Boilerplate 20 |

21 |

22 | This boilerplate uses 23 | 27 | + 28 | 32 | + 33 | 37 | + 38 | 42 | + 43 | 47 | + the 48 | 52 | . 53 |

54 |
58 |

59 | 62 |

63 |

64 | 67 |

68 |
69 |
70 | `; 71 | 72 | exports[`HelloWorld.vue > Web > Render default contents 1`] = ` 73 |
74 |
75 | 76 | Vue logo 80 |
81 |

82 | Your App Name Here 83 |

84 |

85 | Vue Boilerplate 86 |

87 |

88 | This boilerplate uses 89 | 93 | + 94 | 98 | + 99 | 103 | + 104 | 108 | + 109 | 113 | + the 114 | 118 | . 119 |

120 |

121 | Notice that the 122 | 123 | same app conditionally renders 124 | 125 | different content in a normal browser than 126 | 127 | when ran as a desktop app 128 | 129 | . 130 |

131 |
132 | `; 133 | -------------------------------------------------------------------------------- /tests/unit/views/__snapshots__/PiniaDemo.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`PiniaDemo.vue > Desktop > Click increment 1`] = ` 4 |
8 |

9 | 🍍 Pinia Example: 10 |

11 | 30 |
31 | `; 32 | 33 | exports[`PiniaDemo.vue > Desktop > Render default contents 1`] = ` 34 |
38 |

39 | 🍍 Pinia Example: 40 |

41 | 60 |
61 | `; 62 | 63 | exports[`PiniaDemo.vue > Web > Click increment 1`] = ` 64 |
68 |

69 | 🍍 Pinia Example: 70 |

71 | 90 |
91 | `; 92 | 93 | exports[`PiniaDemo.vue > Web > Render default contents 1`] = ` 94 |
98 |

99 | 🍍 Pinia Example: 100 |

101 | 120 |
121 | `; 122 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import vue from '@vitejs/plugin-vue'; 4 | import { defineConfig } from 'vite'; 5 | import vueDevTools from 'vite-plugin-vue-devtools'; 6 | import { configDefaults } from 'vitest/dist/config.js'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | build: { 11 | // Make sure this matches the Chromium version built into NW.js 12 | target: 'chrome137', 13 | outDir: 'dist-vue' 14 | }, 15 | plugins: [ 16 | vue(), 17 | vueDevTools() 18 | ], 19 | resolve: { 20 | alias: { 21 | '@': fileURLToPath(new URL('./src', import.meta.url)), 22 | '@@': fileURLToPath(new URL('./tests', import.meta.url)) 23 | } 24 | }, 25 | test: { 26 | globals: true, 27 | environment: 'happy-dom', 28 | coverage: { 29 | exclude: [ 30 | ...(configDefaults?.coverage?.exclude || []), 31 | '**/dist-vue/**', 32 | './server.cjs' 33 | ], 34 | reportsDirectory: './tests/unit/coverage' 35 | }, 36 | root: '.', 37 | setupFiles: [ 38 | './tests/unit/setup.js' 39 | ], 40 | snapshotSerializers: [ 41 | './node_modules/vue3-snapshot-serializer/index.js' 42 | ] 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /waitOnConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": { 3 | "accept": "text/html" 4 | } 5 | } 6 | --------------------------------------------------------------------------------