├── .eslintrc.js ├── .github ├── actions │ ├── get-build-number │ │ ├── action.yml │ │ ├── index.js │ │ └── package.json │ ├── get-version │ │ ├── action.yml │ │ ├── index.js │ │ └── package.json │ └── update-version │ │ ├── action.yml │ │ ├── index.js │ │ └── package.json └── workflows │ ├── build.yml │ ├── github-page.yml │ └── validate.yml ├── .gitignore ├── .npmignore ├── README.md ├── docs ├── .vitepress │ ├── config.js │ └── theme │ │ ├── index.css │ │ └── index.js ├── index.md └── zh │ └── index.md ├── electron-vue-next ├── .editorconfig ├── .eslintrc.js ├── .github │ ├── actions │ │ ├── get-build-number │ │ │ ├── action.yml │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── get-version │ │ │ ├── action.yml │ │ │ ├── index.js │ │ │ └── package.json │ │ └── update-version-changelog │ │ │ ├── action.yml │ │ │ ├── index.js │ │ │ └── package.json │ └── workflows │ │ ├── build.yml │ │ └── validate.yml ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── CHANGELOG.md ├── build │ └── icons │ │ ├── 256x256.png │ │ ├── icon.icns │ │ └── icon.ico ├── package.json ├── scripts │ ├── build.base.config.js │ ├── build.config.js │ ├── build.js │ ├── build.lite.config.js │ ├── dev.install.js │ ├── dev.js │ ├── jsconfig.json │ ├── plugins │ │ ├── rollup.assets.plugin.js │ │ ├── rollup.devtool.plugin.js │ │ ├── rollup.esbuild.plugin.js │ │ ├── rollup.preload.plugin.js │ │ ├── rollup.renderer.plugin.js │ │ ├── rollup.static.plugin.js │ │ ├── rollup.typescript.plugin.js │ │ ├── rollup.worker.plugin.js │ │ └── util.js │ ├── rollup.config.js │ ├── util.js │ └── vite.config.js ├── src │ ├── main │ │ ├── dialog.ts │ │ ├── index.dev.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── main.d.ts │ │ ├── services │ │ │ ├── BaseService.ts │ │ │ ├── FooService.ts │ │ │ ├── Service.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── workers │ │ │ └── index.ts │ ├── preload │ │ ├── another.ts │ │ ├── index.ts │ │ └── tsconfig.json │ ├── renderer │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ ├── About.vue │ │ │ ├── Home.vue │ │ │ ├── HomeNavigator.vue │ │ │ └── SumEquation.vue │ │ ├── composables │ │ │ ├── count.ts │ │ │ ├── electron.ts │ │ │ ├── index.ts │ │ │ ├── sample.ts │ │ │ └── service.ts │ │ ├── index.css │ │ ├── index.html │ │ ├── index.ts │ │ ├── renderer.d.ts │ │ ├── router.ts │ │ ├── side.html │ │ ├── store │ │ │ ├── index.ts │ │ │ └── modules │ │ │ │ ├── bar.ts │ │ │ │ └── foo.ts │ │ └── tsconfig.json │ └── shared │ │ ├── shared.d.ts │ │ ├── sharedLib.ts │ │ └── tsconfig.json └── static │ └── logo.png ├── index.js ├── package-lock.json ├── package.json └── scripts ├── dev.docs.js └── jsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('eslint').Linter.Config} 3 | */ 4 | module.exports = { 5 | env: { 6 | es2021: true, 7 | browser: true, 8 | node: true 9 | }, 10 | extends: [ 11 | 'plugin:vue/essential', 12 | 'standard' 13 | ], 14 | globals: { 15 | __static: true, 16 | __windowUrls: true, 17 | __preloads: true, 18 | __workers: true, 19 | NodeJS: true 20 | }, 21 | parserOptions: { 22 | ecmaVersion: 12, 23 | parser: '@typescript-eslint/parser', 24 | sourceType: 'module' 25 | }, 26 | plugins: [ 27 | 'vue', 28 | '@typescript-eslint' 29 | ], 30 | rules: { 31 | 'space-before-function-paren': 0, 32 | 'vue/no-multiple-template-root': 0, 33 | 'import/no-absolute-path': 0 34 | }, 35 | ignorePatterns: [ 36 | 'node_modules/**', 37 | 'dist/**' 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/actions/get-build-number/action.yml: -------------------------------------------------------------------------------- 1 | name: TEST 2 | description: Prepare Pull Request for the current change 3 | outputs: 4 | build_number: 5 | description: The release body 6 | runs: 7 | using: 'node12' 8 | main: index.js 9 | 10 | -------------------------------------------------------------------------------- /.github/actions/get-build-number/index.js: -------------------------------------------------------------------------------- 1 | async function main(output) { 2 | output('build_number', process.env.GITHUB_RUN_NUMBER); 3 | } 4 | 5 | function setOutput(name, value) { 6 | process.stdout.write(Buffer.from(`::set-output name=${name}::${value}`)) 7 | } 8 | 9 | main(setOutput); 10 | -------------------------------------------------------------------------------- /.github/actions/get-build-number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.2.6" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/actions/get-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Get Package Version 2 | description: Get NPM Package Version 3 | outputs: 4 | version: 5 | description: The version 6 | runs: 7 | using: 'node12' 8 | main: index.js 9 | 10 | -------------------------------------------------------------------------------- /.github/actions/get-version/index.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | 3 | async function main(output) { 4 | output('version', JSON.parse(readFileSync('./package.json')).version) 5 | } 6 | 7 | function setOutput(name, value) { 8 | process.stdout.write(Buffer.from(`::set-output name=${name}::${value}`)) 9 | } 10 | 11 | main(setOutput); 12 | -------------------------------------------------------------------------------- /.github/actions/get-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.2.6" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/actions/update-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Update Version and Write Changelog 2 | description: Update the version and write changelog 3 | inputs: 4 | version: 5 | description: The version to write 6 | default: '' 7 | required: false 8 | changelog: 9 | description: The changelog to write 10 | default: '' 11 | required: false 12 | runs: 13 | using: 'node12' 14 | main: index.js 15 | 16 | -------------------------------------------------------------------------------- /.github/actions/update-version/index.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync, readFileSync } = require("fs"); 2 | 3 | function main(input) { 4 | const version = input('version') 5 | 6 | if (version) { 7 | const package = JSON.parse(readFileSync('./package.json')) 8 | package.version = version 9 | writeFileSync('./package.json', JSON.stringify(package, null, 2)) 10 | } 11 | } 12 | 13 | function getInput(name) { 14 | return process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 15 | } 16 | 17 | main(getInput); 18 | -------------------------------------------------------------------------------- /.github/actions/update-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.2.6" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'electron-vue-next/build/**' 9 | - 'electron-vue-next/scripts/*' 10 | - 'electron-vue-next/src/**' 11 | - 'electron-vue-next/static/**' 12 | - 'electron-vue-next/package.json' 13 | - 'electron-vue-next/package-lock.json' 14 | - 'scripts/*' 15 | - 'index.js' 16 | 17 | jobs: 18 | build: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js 15 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: 15 29 | - name: Use npm 7 30 | run: npm install -g npm@7 31 | - name: Install 32 | run: | 33 | npm ci --no-optional 34 | - name: Get Build Number 35 | id: vars 36 | uses: ./.github/actions/get-build-number 37 | - name: Build 38 | run: | 39 | npm run build:production 40 | env: 41 | BUILD_NUMBER: ${{ steps.vars.outputs.build_number }} 42 | FULL_RELEASE: ${{ startsWith(github.event.commits[0].message, 'chore(release)') }} 43 | 44 | repare-release: 45 | runs-on: ubuntu-latest 46 | needs: build 47 | if: ${{ !startsWith(github.event.commits[0].message, 'chore(release)') }} 48 | steps: 49 | - uses: actions/checkout@v2 50 | - name: Use Node.js 15 51 | uses: actions/setup-node@v2 52 | with: 53 | node-version: 15 54 | - name: Use npm 7 55 | run: npm install -g npm@7 56 | - name: Install 57 | run: | 58 | npm ci --no-optional 59 | env: 60 | CI: true 61 | - name: Get Package Version 62 | uses: ./.github/actions/get-version 63 | id: package 64 | - name: Bump Version and Generate Changelog 65 | id: version 66 | uses: ci010/conventional-changelog-action@master 67 | with: 68 | github-token: ${{ secrets.github_token }} 69 | version: ${{ steps.package.outputs.version }} 70 | tag-prefix: 'v' 71 | - name: Update package.json and CHANGELOG.md 72 | uses: ./.github/actions/update-version 73 | with: 74 | version: ${{ steps.version.outputs.version }} 75 | - name: Create Pull Request 76 | if: ${{ steps.version.outputs.skipped == 'false' }} 77 | uses: peter-evans/create-pull-request@v3 78 | with: 79 | token: ${{ secrets.GITHUB_TOKEN }} 80 | commit-message: >- 81 | ${{ format('chore(release): {0}', steps.version.outputs.version) }} 82 | title: ${{ format('Prepare Release {0}', steps.version.outputs.version) }} 83 | body: ${{ steps.version.outputs.clean_changelog }} 84 | branch: prepare-release 85 | 86 | release: 87 | if: startsWith(github.event.commits[0].message, 'chore(release)') 88 | runs-on: ubuntu-latest 89 | needs: build 90 | steps: 91 | - uses: actions/checkout@v2 92 | - name: Use Node.js 15 93 | uses: actions/setup-node@v2 94 | with: 95 | node-version: 15 96 | - name: Use npm 7 97 | run: npm install -g npm@7 98 | - name: Install 99 | run: | 100 | npm ci --no-optional 101 | - name: Publish NPM 102 | run: | 103 | git reset --hard 104 | npm publish 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 108 | - name: Get Package Version 109 | uses: ./.github/actions/get-version 110 | - name: Create Release 111 | id: create_release 112 | uses: actions/create-release@master 113 | env: 114 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 115 | with: 116 | tag_name: ${{ format('v{0}', steps.package.outputs.version) }} 117 | release_name: ${{ format('v{0}', steps.package.outputs.version) }} 118 | draft: false 119 | prerelease: false 120 | body: ${{ steps.prepare_release.outputs.clean_changelog }} 121 | - name: Delete PR head branch 122 | uses: dawidd6/action-delete-branch@master 123 | with: 124 | github_token: ${{ secrets.GITHUB_TOKEN }} 125 | branch: prepare-release 126 | be_kind: true # don't fail on errors (optional) -------------------------------------------------------------------------------- /.github/workflows/github-page.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Github Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'docs/**' 9 | jobs: 10 | page-deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 15 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 15 20 | - name: Use npm 7 21 | run: npm install -g npm@7 22 | - name: Install 23 | run: | 24 | npm ci 25 | env: 26 | CI: true 27 | - name: Build Document 28 | run: | 29 | npm run build:docs 30 | - name: Deploy to GitHub Pages 31 | if: success() 32 | uses: crazy-max/ghaction-github-pages@v2 33 | with: 34 | target_branch: gh-pages 35 | build_dir: docs/.vitepress/dist 36 | jekyll: false 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 15 12 | uses: actions/setup-node@v2 13 | with: 14 | node-version: 15 15 | - name: Use npm 7 16 | run: npm install -g npm@7 17 | - name: Install 18 | run: | 19 | npm ci 20 | - name: Lint 21 | run: | 22 | npm run lint 23 | - name: Build 24 | run: | 25 | npm run build 26 | npm run build:docs 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.local 4 | thumbs.db 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.local 4 | thumbs.db 5 | package-lock.json 6 | /vetur.config.js 7 | docs 8 | /scripts 9 | /.gitignore 10 | /.github 11 | /electron-vue-next/dist 12 | /electron-vue-next/build 13 | !/electron-vue-next/build/icons 14 | electron-vue-next/extensions -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This repository contains the starter template for using vue-next with the latest electron. 4 | 5 | *I started to learn electron & vue by the great project [electron-vue](https://github.com/SimulatedGREG/electron-vue). This project is also inspired from it.* 6 | 7 | You can see the document [here](https://ci010.github.io/electron-vue-next/index.html). 8 | 9 | 同样,我们也有[中文文档](https://ci010.github.io/electron-vue-next/zh)。 10 | 11 | ## Features 12 | 13 | - Electron 11 14 | - Follow the [security](https://www.electronjs.org/docs/tutorial/security) guide of electron, make renderer process a browser only environment 15 | - Using [electron-builder](https://github.com/electron-userland/electron-builder) to build 16 | - Empower [vue-next](https://github.com/vuejs/vue-next) and its eco-system 17 | - Using [vite](https://github.com/vitejs/vite) which means develop renderer process can be blazingly fast! 18 | - Using [vuex 4.0](https://github.com/vuejs/vuex/tree/4.0) with strong type state, getters, and commit 19 | - Using [vue-router-next](https://github.com/vuejs/vue-router-next) 20 | - Using [eslint](https://www.npmjs.com/package/eslint) with Javascript Standard by default 21 | - Built-in TypeScript Support 22 | - Using [esbuild](https://github.com/evanw/esbuild) in [rollup](https://github.com/rollup/rollup) (align with vite) to build main process typescript code 23 | - Github Action with Github Release is out-of-box 24 | - Auto bump version in package.json and generate CHANGELOG.md if you follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) 25 | - Detail how this work described in [Release Process](#release-process) section 26 | - Integrate VSCode well 27 | - Support debug .ts/.vue files in main/renderer process by vscode debugger 28 | - Detail see [Debug](#debug-in-vscode) section 29 | - Multiple Windows Support 30 | - Can add a new window for App easily. See [Add a New Window](#new-window) section 31 | - [vue-devtool](https://github.com/vuejs/vue-devtools) support 32 | - Run npm run postinstall to install extensions 33 | - Support vue-router-next and vuex 4 with new UI 34 | 35 | 36 | ## Getting Started 37 | 38 | Run `npm init electron-vue-next ` 39 | 40 | Once you have your project, and in the project folder: 41 | 42 | ```shell 43 | # Install dependencies with linter 44 | npm install 45 | 46 | # Will start vite server, rollup devserver, and electron to dev! 47 | npm run dev 48 | 49 | # OPTIONAL. Will compile the main and renderer process to javascript and display output size 50 | npm run build 51 | 52 | # OPTIONAL. Will compile all and output an unpacked electron app. You can directly 53 | npm run build:dir 54 | 55 | # Will compile all and build all products and ready to release 56 | npm run build:production 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('vitepress').DefaultTheme.Config} 3 | */ 4 | const themeConfig = { 5 | search: { 6 | searchMaxSuggestions: 10, 7 | }, 8 | sidebar: 'auto', 9 | repo: 'ci010/electron-vue-next', 10 | docsDir: 'docs', 11 | repoLabel: 'Github', 12 | lastUpdated: true, 13 | prevLink: true, 14 | nextLink: true, 15 | locales: { 16 | '/': { 17 | docsDir: 'docs', 18 | lang: 'en-US', 19 | title: 'electron-vue-next', 20 | description: 'vue hooks', 21 | label: 'English', 22 | selectText: 'Languages', 23 | }, 24 | '/zh/': { 25 | docsDir: 'docs', 26 | lang: 'zh-CN', 27 | title: 'electron-vue-next', 28 | description: 'vue hooks', 29 | label: '中文', 30 | selectText: '语言', 31 | }, 32 | }, 33 | } 34 | 35 | /** 36 | * @type {import('vitepress').UserConfig} 37 | */ 38 | const config = { 39 | lang: 'en-US', 40 | themeConfig, 41 | base: process.env.NODE_ENV === 'development' ? undefined : '/electron-vue-next', 42 | title: 'Electron Vue Next', 43 | } 44 | 45 | module.exports = config; 46 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.css: -------------------------------------------------------------------------------- 1 | .theme.no-sidebar aside { 2 | display: block; 3 | } -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import index from 'vitepress/dist/client/theme-default/index' 2 | import './index.css' 3 | 4 | export default index -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # electron-vue-next 2 | 3 | This repository contains the starter template for using vue-next with the latest electron. 4 | 5 | *I started to learn electron & vue by the great project [electron-vue](https://github.com/SimulatedGREG/electron-vue). This project is also inspired from it.* 6 | 7 | *Holpfully, you generally learn how to use rollup & its plugin API from this project* :) 8 | 9 | ## Features 10 | 11 | - Electron 11 12 | - Follow the [security](https://www.electronjs.org/docs/tutorial/security) guide of electron, make renderer process a browser only environment 13 | - Using [electron-builder](https://github.com/electron-userland/electron-builder) to build 14 | - Empower [vue-next](https://github.com/vuejs/vue-next) and its eco-system 15 | - Using [vite](https://github.com/vitejs/vite) which means develop renderer process can be blazingly fast! 16 | - Using [vuex 4.0](https://github.com/vuejs/vuex/tree/4.0) with strong type state, getters, and commit 17 | - Using [vue-router-next](https://github.com/vuejs/vue-router-next) 18 | - Using [eslint](https://www.npmjs.com/package/eslint) with Javascript Standard by default 19 | - Built-in TypeScript Support 20 | - Using [esbuild](https://github.com/evanw/esbuild) in [rollup](https://github.com/rollup/rollup) (align with vite) to build main process typescript code 21 | - Built-in a typescript rollup plugin to typecheck 22 | - NodeJS `worker_threads` workflow out-of-box 23 | - Don't need to worry the worker threads bundle/build when you use it. 24 | - Preload scripts build workflow out-of-box 25 | - No need to worry about the bundle/build of electron preload script. 26 | - Built-in support for preload reload in DEV mode. Changing preload scripts won't restart the whole electron app 27 | - Github Action with Github Release is out-of-box 28 | - Auto bump version in package.json and generate CHANGELOG.md if you follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) 29 | - Detail how this work described in [Release Process](#release-process) section 30 | - Integrate VSCode well 31 | - Support debug .ts/.vue files in main/renderer process by vscode debugger 32 | - Detail see [Debug](#debugging) section 33 | - Multiple Windows Support 34 | - Can add a new window for App easily. See [Add a New Window](#new-window) section 35 | - [vue-devtool](https://github.com/vuejs/vue-devtools) support 36 | - Run npm run postinstall to install extensions 37 | - Support vue-router-next and vuex 4 with new UI 38 | 39 | 40 | ## Quick Start 41 | 42 | You should use `npm init` to create the project from template: `npm init electron-vue-next`. 43 | 44 | Once you're done, you can run following commands: 45 | 46 | ```shell 47 | # Install dependencies 48 | npm install 49 | 50 | # Will start vite server, rollup devserver, and electron to dev! 51 | npm run dev 52 | 53 | # OPTIONAL. Will compile the main and renderer process to javascript and display output size 54 | npm run build 55 | 56 | # OPTIONAL. Will compile all and output an unpacked electron app. You can directly 57 | npm run build:dir 58 | 59 | # Will compile all and build all products and ready to release 60 | npm run build:production 61 | 62 | ``` 63 | 64 | ### Config Your Project and Build 65 | 66 | Once you install your project, you should change the package base info in [package.json](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/package.json), 67 | and also the build information in [build.base.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/build.base.config.js). 68 | 69 | ## Project Structure 70 | 71 | ### File Tree 72 | 73 | Your workspace should looks like 74 | 75 | ``` 76 | your-project 77 | ├─ scripts all dev scripts, build script directory 78 | ├─ extensions temp folder of the vue-devtools extension 79 | ├─ build build resource and output directory 80 | │ └─ icons/ build icon directory 81 | ├─ dist compiled output directory 82 | ├─ src 83 | │ ├─ main 84 | │ │ ├─ services/ services to access the network or files 85 | │ │ ├─ workers/ multi-thread scripts using nodejs worker_threads 86 | │ │ ├─ dialog.ts the ipc handler to support dialog API from renderer process 87 | │ │ ├─ global.ts typescript global definition 88 | │ │ ├─ index.dev.ts the development rollup entry 89 | │ │ ├─ index.ts real electron start-up entry file 90 | │ │ └─ logger.ts a simple logger implementation 91 | │ ├─ preload 92 | │ │ ├─ index.ts the preload entry 93 | │ │ └─ another.ts another preload entry 94 | │ ├─ renderer 95 | │ │ ├─ assets/ assets directoy 96 | │ │ ├─ components/ all vue components 97 | │ │ ├─ hooks/ hooks or composition API 98 | │ │ ├─ router.ts vue-router initializer 99 | │ │ ├─ store.ts vuex store initializer 100 | │ │ ├─ App.vue entry vue file imported by index.ts 101 | │ │ ├─ index.css entry css file for vite 102 | │ │ ├─ index.html entry html file for vite 103 | │ │ └─ index.ts entry script file for vite 104 | │ └─ shared shared folder can be access from both main and renderer side 105 | │ ├─ store/ vuex store definition 106 | │ └─ sharedLib.ts an example file that can be access from both side 107 | ├─ static/ static resource directory 108 | ├─ .eslintrc.js 109 | ├─ .gitignore 110 | ├─ package.json 111 | └─ README.md 112 | ``` 113 | 114 | #### assets, static resources, build resources... what's the difference? 115 | 116 | The assets is only used by the renderer process (in-browser display), like picture or font. They are **bundled by vite/rollup**. You can directly `import` them in `.vue/.ts` files under renderer directory. The default assets are in [renderer/renderer/assets](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/src/renderer/assets) 117 | 118 | The static resources are the static files which main process wants to access (like read file content) in **runtime vie file system**. They might be the tray icon file, browser window icon file. The static folder is at [static](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/static). 119 | 120 | The build resources are used by `electron-builder` to build the installer. They can be your program icon of installer, or installer script. Default build icons are under [build/icons](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/build/icons). 121 | 122 | *Notice that your program icon can show up in multiple place! Don't mixup them!* 123 | - *In build icons, of course you want your program has correct icon.* 124 | - *In static directory, sometime you want your program has **tray** which require icon in static directory.* 125 | - *In assets, sometime you want to display your program icon inside a page. You need to place them in the assets!* 126 | 127 | ### Main and Renderer Processes 128 | 129 | Quote from electron official document about [main and renderer processes](https://www.electronjs.org/docs/tutorial/quick-start#main-and-renderer-processes). The main process is about 130 | 131 | > - The Main process creates web pages by creating BrowserWindow instances. Each BrowserWindow instance runs the web page in its Renderer process. When a BrowserWindow instance is destroyed, the corresponding Renderer process gets terminated as well. 132 | > - The Main process manages all web pages and their corresponding Renderer processes. 133 | 134 | And the renderer process is about 135 | 136 | > - The Renderer process manages only the corresponding web page. A crash in one Renderer process does not affect other Renderer processes. 137 | > - The Renderer process communicates with the Main process via IPC to perform GUI operations in a web page. Calling native GUI-related APIs from the Renderer process directly is restricted due to security concerns and potential resource leakage. 138 | 139 | Commonly, the main process is about your core business logic, and renderer side act as a data consumer to render the UI. Though, this is not absolutly right. Someone thinks the main process should not do any job other than communicating with system API (by electron API), since once the main process is blocked, the whole app will be blocked (not responsed). So if you have some really CPU heavy job, you definitly should not put them in the main process (Most of IO job in nodejs are async, that's fine). Maybe you can use the nodejs [worker_thread](https://nodejs.org/api/worker_threads.html) module to put them into another thread, so it won't make the whole app not responsable. 140 | 141 | So this design is depend on your core business type. If it's a IO heavy job, it's fine to put them in main process. If it's a CPU heavy job, you need to consider not to put them much in main process. 142 | 143 | Following the [security](https://www.electronjs.org/docs/tutorial/security) guideline of electron, in this boilerplate, the renderer process [**does not** have access to nodejs module by default](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content). The electron provide the `preload` options in `webPreferences`. This boilerplate provides one simple way to solve this problem, wrapping your core logic into `Service`. 144 | 145 | The `Service` is a type of class defined under the `src/main/services`. All the public method can be access by the renderer process. 146 | It's the bridge between the main and renderer. You can look at [Service](#service) for the detail. 147 | 148 | ***Notice that this is really a simple/trivial solution which derives from my personal electron project (much complex case). It only shows a possibility. Real life software might need more modifications on it!*** 149 | 150 | ### NPM Scripts 151 | 152 | #### `npm run dev` 153 | 154 | Start the vite dev server hosting the renderer webpage with hot reloading. 155 | Start the rollup server hosting the main process script. It will auto reload the electron app if you modify the source files. 156 | 157 | #### `npm run build` 158 | 159 | Compile both `main` and `renderer` process code to production, located at `dist` 160 | 161 | #### `npm run build:production` 162 | 163 | It will compile both processes, and then run `electron-builder` to build your app into executable installer or zip. The build config is defined in [scripts/build.base.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/build.base.config.js). 164 | 165 | #### `npm run build:dir` 166 | 167 | It will compile both processes, and it will run `electron-builder` to build only the directoy version of the production electron app, which for example, for windows x64, it's located at `build/win-unpacked`. 168 | 169 | This will much faster than `npm run build:production`. So you can use it to quick testing the production app. 170 | 171 | #### `npm run lint` 172 | 173 | Run eslint to report eslint error. 174 | 175 | #### `npm run lint:fix` 176 | 177 | Run eslint to fix and report eslint error. 178 | 179 | ## Development 180 | 181 | Due to the project is following the [security](https://www.electronjs.org/docs/tutorial/security) guideline. It does not allow the renderer to access node by default. The [Service](#service) is a simple solution to isolate renderer logic and the core logic with full nodejs module access. See [this](#option-using-node-modules-in-renderer-process) section if you want to directly use node modules in renderer process. 182 | 183 | ### Service 184 | 185 | A Service lives in a class in `src/main/services`. It should contain some of your core logic with file or network access in main process. It exposes these logic to renderer process. You call the hook `useService('NameOfService')` to use it in renderer side. 186 | 187 | The concept of service is totally optional. This is a design for security. ***If you think this is redundent and not fit with your program design, you can just remove it.*** 188 | 189 | #### Create a new Service 190 | 191 | Add a file to the `/src/main/services` named `BarService.ts` 192 | 193 | ```ts 194 | export default class BarService extends Service { 195 | async doSomeCoreLogic() { 196 | // perform some file system or network work here 197 | } 198 | } 199 | ``` 200 | 201 | And you need to add it to the `interface Services` in `src/main/services/index.ts`. 202 | 203 | ```ts 204 | import { BarService } from './BarService' 205 | 206 | export interface Services { 207 | // ... other existed services 208 | BarService: BarService 209 | } 210 | ``` 211 | 212 | Then, add it to the `initializeServices` in `src/main/index.ts` 213 | 214 | ```ts 215 | async function initializeServices(logger: Logger) { 216 | initialize({ 217 | // ...other services 218 | BarService: new BarService(logger) 219 | }) 220 | } 221 | ``` 222 | 223 | And this is ready to be used in renderer process by `useService('BarService')`. See [Using Service in Renderer](#using-service-in-renderer). 224 | 225 | #### Using Other Service in a Service 226 | 227 | If you need to use other `Service`, like `FooService`. You need to `@Inject` decorator to inject during runtime. 228 | 229 | ```ts 230 | export default class BarService extends Service { 231 | @Inject('FooService') 232 | private fooService: FooService 233 | 234 | async doSomeCoreLogic() { 235 | const result = await this.fooService.foo() 236 | // perform some file system or network operations here 237 | } 238 | } 239 | ``` 240 | 241 | #### Using Service in Renderer 242 | 243 | You can directly access all the async methods in a service class by `useService('nameOfService')` 244 | 245 | Here is an example in [About.vue](), using the `BaseService`. 246 | 247 | ```vue 248 | 256 | 257 | 280 | ``` 281 | 282 | #### Remove Service Infra 283 | 284 | If you don't like Service design, you just easily remove it by 285 | 286 | 1. Remove the whole `src/main/services` directory 287 | 2. Remove the import line `import { initialize } from './services'` and initialization line `initialize(logger)` in `src/main/index.ts` 288 | 289 | 290 | ### Static Resource 291 | 292 | Place all your static resources under the `static` folder. 293 | 294 | To use them in the main process, you just need to import them by `/@static/`. 295 | 296 | For example, we have a `logo.png` in static folder, and we want to get it path in runtime: 297 | 298 | ```ts 299 | import logoPath from '/@static/logo.png' // this is the absolute path of the logo 300 | ``` 301 | 302 | The plugin manage this is the `scripts/rollup.static.plugin.js`. 303 | 304 | ### Preload Script 305 | 306 | No matter you use the Service design or not, you will need to care about the preload script of the `BrowserWindow`. 307 | 308 | *If you don't know what is the preload script. You can read it in [electron document about BrowserWindow](https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions), and also the [security guideline](https://www.electronjs.org/docs/tutorial/security).* 309 | 310 | In this template, we have already setup the build script for preload. You can see all the preloads under `/src/preload`. 311 | 312 | You must put the preload script under that folder. If you want to use it in main process. 313 | You can just import them by `import preloadPath from '/@preload/'` 314 | 315 | For example, if you add a new preload script named `/src/preload/my-preload.ts`, 316 | you can refer it while creating the `BrowserWindow`: 317 | 318 | ```ts 319 | import myPreloadPath from '/@preload/my-preload' 320 | 321 | new BrowserWindow({ 322 | webPreferences: { 323 | preload: myPreloadPath, 324 | } 325 | }) 326 | ``` 327 | 328 | The rollup config of preload is located at the `rollup.config.js`. 329 | 330 | The preload script in `dist` will be built like `dist/.preload.js`. 331 | 332 | The plugin manage this process is located at `scripts/rollup.preload.plugin.js`. 333 | 334 | ### Hooks or Composable in Renderer Process 335 | 336 | One great feature of vue 3 is the [composition-api](https://v3.vuejs.org/api/composition-api.html). You can write up some basic piece of logic and compose them up during the setup functions. Currently, these `hooks` are placed in `/src/renderer/hooks` by default. 337 | 338 | Take the example from vue composition api site, you have such code in `/src/renderer/hooks/mouse.ts` 339 | 340 | ```ts 341 | import { ref, onMounted, onUnmounted } from 'vue' 342 | 343 | export function useMousePosition() { 344 | const x = ref(0) 345 | const y = ref(0) 346 | 347 | function update(e) { 348 | x.value = e.pageX 349 | y.value = e.pageY 350 | } 351 | 352 | onMounted(() => { 353 | window.addEventListener('mousemove', update) 354 | }) 355 | 356 | onUnmounted(() => { 357 | window.removeEventListener('mousemove', update) 358 | }) 359 | 360 | return { x, y } 361 | } 362 | ``` 363 | 364 | You'd better to export this `mouse.ts` to `/src/renderer/hooks/index.ts` 365 | 366 | ```ts 367 | // other exports... 368 | 369 | export * from './mouse.ts' 370 | ``` 371 | 372 | Then in the `vue` file you can import all hooks by the alias path 373 | 374 | ```vue 375 | 378 | 390 | ``` 391 | 392 | ### Electron API in Renderer Process 393 | 394 | The boilplate exposes several electron APIs by default. You can access them by `useShell`, `useClipboard`, `useIpc` and `useDialog`. 395 | *These are provided by `static/preload.js` script. If you remove the preload during the creation of this BrowserWindow, this won't work.* 396 | 397 | ```ts 398 | import { defineComponent } from 'vue' 399 | import { useShell } from '/@/hooks' 400 | 401 | export default defineComponent({ 402 | setup() { 403 | const shell = useShell() // this is equivalence to the import { shell } from 'electron' normally 404 | // the shell object type definition works normally 405 | } 406 | }) 407 | ``` 408 | 409 | The only exception is the `useDialog`. You can only use `async` functions in it as the API call goes through IPC and it must be `async`. 410 | 411 | ### Dependencies 412 | 413 | If you adding a new dependency, make sure if it's using any **nodejs** module, add it as `external` in the `package.json`. Otherwise, the vite will complain about "I cannot handle it!". 414 | 415 | ```json 416 | { 417 | // ...other package.json content 418 | "dependencies": { 419 | // ...other dependencies 420 | "a-nodejs-package": "" 421 | }, 422 | "external": [ 423 | // ...other existed excluded packages 424 | "a-nodejs-package" // your new package 425 | ], 426 | // ...rest of package.json 427 | } 428 | ``` 429 | 430 | The raw javascript dependencies are okay for vite. 431 | 432 | #### Native Dependencies 433 | 434 | If you want to use the native dependencies, which need to compile when install, usually, you need [node-gyp](https://github.com/nodejs/node-gyp) to build, the `electron-builder` will rebuild it upon your electron for you. Normally you don't need to worry much. Notice that if you are in Windows, you might want to install [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) to install the compile toolchain. 435 | 436 | #### Dependencies Contains Compiled Binary 437 | 438 | If you want to use the dependencies containing the compiled binary, not only you should adding it to vite `exclude`, you should also take care about the electron-builder config. See the [Build](#exclude-files) section for detail. The development process won't affect much by it. 439 | 440 | ### New Window 441 | 442 | 1. Add a new html file under the `src/renderer` 443 | 2. Reference some typescript/javascript file in you new added html file 444 | 3. Add a code block in your `src/main/index.ts` to control the creation of this window 445 | 446 | For example, you just added a `side.html` under the `src/renderer`. You need to add such controller code in `index.ts`: 447 | 448 | ```ts 449 | import preload from '/@preload/index' 450 | 451 | // This function should be called once app is ready 452 | function createANewWindow() { 453 | // this part is the same as before, modify it as your wish 454 | const win = new BrowserWindow({ 455 | height: 600, 456 | width: 300, 457 | webPreferences: { 458 | preload, 459 | contextIsolation: true, 460 | nodeIntegration: false 461 | } 462 | }) 463 | 464 | // __windowUrls.side is pointing to the real url of `side.html` 465 | win.loadURL(__windowUrls.side) 466 | } 467 | 468 | ``` 469 | 470 | The `scripts/vite.config.js` will automatically scan all html files under the `src/renderer`. So, you do not need to touch the any vite/rollup config files. 471 | But, if you want more customization, you can refer the [official vite document](https://vitejs.dev/guide/build.html#multi-page-app) about the multi-page app! 472 | 473 | ### Worker Threads 474 | 475 | If you want to use [worker_threads](https://nodejs.org/api/worker_threads.html) in main process, you need separately load the `Worker` script. The template already setup the build/bundle process of the `Worker` script. Normally, you do not need to modify this build process. 476 | 477 | You just need to import the worker script by ending a `?worker` query in import. 478 | 479 | If you have a new worker file named `src/main/workers/sha256.ts`, 480 | you can access it in main process like: 481 | 482 | ```ts 483 | import createSha256Worker from './workers/sha256?worker' 484 | import { Worker } from 'worker_threads' 485 | 486 | const worker: Worker = createSha256Worker(/* options */) 487 | ``` 488 | 489 | The worker thread files are built together with the normal main process code. They are under the same config in `rollup.config.js`. 490 | 491 | In `dist`, The worker script will be compiled as `dist/.worker.js`. 492 | 493 | ### Debugging 494 | 495 | This is really simple. In vscode debug section, you will see three profiles: 496 | 497 | 1. Electron: Main (attach) 498 | 2. Electron: Renderer (attach) 499 | 3. Electron: Main & Renderer (attach) 500 | 501 | 502 | ```json 503 | { 504 | "version": "0.2.0", 505 | "configurations": [ 506 | { 507 | "name": "Electron: Main (attach)", 508 | "type": "node", 509 | "request": "attach", 510 | "cwd": "${workspaceFolder}", 511 | "outFiles": [ 512 | "${workspaceFolder}/dist/**/*.js" 513 | ], 514 | "smartStep": true, 515 | "sourceMaps": true, 516 | "protocol": "inspector", 517 | "port": 5858, 518 | "timeout": 20000 519 | }, 520 | { 521 | "name": "Electron: Renderer (attach)", 522 | "type": "chrome", 523 | "request": "attach", 524 | "port": 9222, 525 | "webRoot": "${workspaceFolder}", 526 | "timeout": 15000 527 | }, 528 | ], 529 | "compounds": [ 530 | { 531 | "name": "Electron: Main & Renderer (attach)", 532 | "configurations": ["Electron: Main (attach)", "Electron: Renderer (attach)"] 533 | } 534 | ] 535 | } 536 | ``` 537 | 538 | The name should be clear. The first one attach to main and the second one attach to renderer (required vscode chrome debug extension). 539 | The third one is run the 1 and 2 at the same time. 540 | 541 | You should first run `npm run dev` and start debugging by the vscode debug. 542 | 543 | ### Option: Using Node Modules in Renderer Process 544 | 545 | By default, the renderer process environment is just a raw front-end environment. You cannot use any nodejs module here. (Use service alternative) 546 | 547 | If you just want to use node modules in electron renderer/browser side anyway, you can just enable the `nodeIntegration` in BrowserWindow creation. 548 | 549 | For example, you can enable the main window node integration like this: 550 | 551 | ```ts 552 | const mainWindow = new BrowserWindow({ 553 | height: 600, 554 | width: 800, 555 | webPreferences: { 556 | preload: join(__static, 'preload.js'), 557 | nodeIntegration: true // adding this to enable the node integration 558 | } 559 | }) 560 | ``` 561 | 562 | ## Build 563 | 564 | The project build is based on [electron-builder](https://github.com/electron-userland/electron-builder). The config file is majorly in [scripts/build.base.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/build.base.config.js). And you can refer the electron-builder [document](https://www.electron.build/). 565 | 566 | ### Compile 567 | 568 | The project will compile typescript/vue source code by rollup into javascript production code. The rollup config for main process is in [rollup.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/rollup.config.js). It will output the production code to `dist/index.js`. 569 | 570 | Notice that by default, this project's rollup config won't bundle the nodejs dependencies used in main process. As the rollup is based on esm, it has a hard time to resolve some circular dependencies problems, which can happen frequently in some nodejs package (e.g. electron-updater, Webpack can handle these kind of problem though). Once you put them into the `external` in `package.json`, they will be packed in the `node_modules` in output asar. Just let you know that this is not like webpack, bundling them into your `index.js`. 571 | 572 | The config to compile renderer process is in [vite.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/vite.config.js). It will compile the production code into `dist/renderer/*`. 573 | 574 | #### Speed Up Compile 575 | 576 | If you feel the compile time is too long, this is majorly caused by typecheck for main process code. 577 | 578 | You can set the wait to typescript plugin to false in `rollup.config.js` to skip type check to dev. 579 | 580 | ```ts 581 | pluginTypescript({ 582 | tsconfig: [join(__dirname, '../src/main/tsconfig.json'), join(__dirname, '../src/preload/tsconfig.json')], 583 | wait: false // add this to not wait the type error result 584 | }), 585 | ``` 586 | 587 | You will still see the typecheck result, but the build won't be prevent if the typecheck failed. 588 | 589 | If you still think the compile is slow, you can just remove this plugin from `rollup.config.js` 590 | 591 | ### Exclude Files 592 | 593 | Normally, once you correctly config the `dependencies` in [Development](#development) section, you should not worry to much about the build. But some dependencies contains compiled binary. You might want to exclude them out of the unrelated OS builds. 594 | 595 | For example, [7zip-min](https://github.com/onikienko/7zip-min): 596 | 597 | Since it using the `7zip-bin` which carry binary for multiple platform, we need to correctly include them in config. 598 | Modify the electron-builder build script `build.base.config.js` 599 | 600 | ```js 601 | asarUnpack: [ 602 | "node_modules/7zip-bin/**/*" 603 | ], 604 | ``` 605 | 606 | Add them to `asarUnpack` to ensure the electron builder correctly pack & unpack them. 607 | 608 | To optimize for multi-platform, you should also exclude them from `files` of each platform config `build.config.js` 609 | 610 | ```js 611 | mac: { 612 | // ... other mac configs 613 | files: [ 614 | "node_modules/7zip-bin/**/*", 615 | "!node_modules/7zip-bin/linux/**", 616 | "!node_modules/7zip-bin/win/**" 617 | ] 618 | }, 619 | win: { 620 | // ... other win configs 621 | files: [ 622 | "node_modules/7zip-bin/**/*", 623 | "!node_modules/7zip-bin/linux/**", 624 | "!node_modules/7zip-bin/mac/**" 625 | ] 626 | }, 627 | linux: { 628 | // ... other linux configs 629 | files: [ 630 | "node_modules/7zip-bin/**/*", 631 | "!node_modules/7zip-bin/win/**", 632 | "!node_modules/7zip-bin/mac/**" 633 | ] 634 | }, 635 | ``` 636 | 637 | ## Release 638 | 639 | The out-of-box github action will validate each your PR by eslint and run `npm run build`. It will not trigger electron-builder to build production assets. 640 | 641 | For each push in master branch, it will build production assets for win/mac/linux platform and upload it as github action assets. It will also create a **pull request** to asking you to bump version and update the changelog. 642 | 643 | It using the conventional-commit. If you want to auto-generate the changelog, you should follow the [conventional commit guideline](https://www.conventionalcommits.org/en/v1.0.0). 644 | 645 | If the **bump version PR** is approved and merged to master, it will auto build and release to github release. 646 | 647 | **If you want to disable this github action release process, just remove the [.github/workflows/build.yml](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/.github/workflows/build.yml) file.** 648 | 649 | ### AutoUpdate Support 650 | 651 | This boilerplate include the [electron-updater](https://github.com/electron-userland/electron-builder/tree/master/packages/electron-updater) as dependencies by default. You can follow the [electron-builder](https://github.com/electron-userland/electron-builder) guideline to implement the autoUpdate process. 652 | -------------------------------------------------------------------------------- /docs/zh/index.md: -------------------------------------------------------------------------------- 1 | # electron-vue-next 2 | 3 | 此仓库包含了一个用于快速上手 vue-next 和 electron 的模板~ 4 | 5 | *我通过使用 [electron-vue](https://github.com/SimulatedGREG/electron-vue),学习了如何使用 electron 和 vue。所以这个项目很大程度上受到了它的启发。* 6 | 7 | *希望你在使用这个模板的时候能够逐渐熟悉 rollup 和它的 API* :) 8 | ## 特性清单 9 | 10 | - Electron 11 11 | - 遵从 [ 安全性,原生能力和你的责任 ](https://www.electronjs.org/docs/tutorial/security) 这篇文章的指导,将 renderer 进程配置为纯“浏览器环境”(没有 node 环境) 12 | - 使用 [electron-builder](https://github.com/electron-userland/electron-builder) 来构建项目 13 | - 跟随 [vue-next](https://github.com/vuejs/vue-next) 的新生态 14 | - 使用 [vite](https://github.com/vitejs/vite) 来构建 renderer 进程,热重载速度非常之快 15 | - 使用 [vuex 4.0](https://github.com/vuejs/vuex/tree/4.0),并自带类型推断代码,尽可能利用 typescript 的类型系统 16 | - 使用了新的 [vue-router-next](https://github.com/vuejs/vue-router-next) 17 | - 内置 [eslint](https://www.npmjs.com/package/eslint),默认使用 Javascript Standard 18 | - 内置 TypeScript 19 | - 使用 [esbuild](https://github.com/evanw/esbuild) 和 [rollup](https://github.com/rollup/rollup) 来构建 main 进程的 typescript(和 vite 使用的 esbuild 版本相同) 20 | - 开箱即用的 NodeJS 多线程支持 21 | - 模板中已经把 worker script 的打包过程配置好了,使用时无需再调整构建配置 22 | - 开箱即用的 preload 的支持 23 | - 模板中已经把 preload 的打包过程配置好了,使用时无需再调整构建配置 24 | - 在开发模式下支持 preload 自动重载。preload script 的变化不会导致整个 Electron App 重启。 25 | - 开箱即用的 Github Action 发布流程 26 | - 自动升级版本号并且生成更变日志,只要你的 git commits 遵从 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) 27 | - 具体细节你可以在 [发布](#发布) 这个章节查找 28 | - 和 VSCode 集成 29 | - 自带 VSCode 的 Debug 配置。可以在 VSCode 中 debug typescript 和 vue 文件,main 和 renderer 的都可以。 30 | - 具体实现细节可以看 [Debug](#在-vscode-中-debug) 这个章节 31 | - 支持多窗口 32 | - 可以简单地让 App 增加一个新的窗口,详情参见 [如何添加一个新的窗口](#添加一个新的窗口) 33 | - Vue Devtools 开箱即用 34 | - 通过 npm run postinstall 来确保 devtools 的安装 35 | - 支持新的 vue-router-next 和 vuex 4 36 | 37 | ## 上手指南 38 | 39 | 通过 npm init 来创建模板: 40 | 41 | `npm init electron-vue-next` 42 | 43 | 之后在仓库根目录下: 44 | 45 | ```shell 46 | # 安装依赖 47 | npm install 48 | 49 | # 创建 dev 服务器,将启动 electron 和 vite 的热重载服务器 50 | npm run dev 51 | 52 | # 可选. 将 main 和 renderer 进程编译成 JavaScript,并显示输出大小 53 | npm run build 54 | 55 | # 可选. 将所有编译输出打包到一个 electron 应用程序中,以文件夹形式存在 56 | npm run build:dir 57 | 58 | # 将所有输出打包到实际的 electron 安装包中 59 | npm run build:production 60 | 61 | ``` 62 | 63 | ### 配置你的项目信息和构建脚本 64 | 65 | 在你安装完项目依赖之后,你应该首先去 [package.json](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/package.json) 中更改项目基本信息,如项目名,作者信息,git 仓库地址等。 66 | 同时你需要更新构建信息 [build.base.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/build.base.config.js), 67 | 68 | ## 项目结构 69 | 70 | ### 文件目录结构 71 | 72 | 整个项目的文件结构大概如此: 73 | 74 | ``` 75 | your-project 76 | ├─ scripts 所有的脚本文件夹,比如 build 的脚本就在这放着 77 | ├─ extensions vue-devtools 拓展临时存放的文件夹 78 | ├─ build build 使用的资源文件,同时也是 build 的输出文件夹 79 | │ └─ icons/ build 使用的图标文件 80 | ├─ dist 编译后的js会在这 81 | ├─ src 82 | │ ├─ main 83 | │ │ ├─ services/ 包含一些 App 与网络、磁盘文件交互的的业务逻辑 84 | │ │ ├─ workers/ 使用 nodejs worker_threads 的多线程脚本 85 | │ │ ├─ dialog.ts 对 electron dialog API 的简单封装,让 renderer 可以使用 dialog 86 | │ │ ├─ global.ts typescript 的一些全局定义 87 | │ │ ├─ index.dev.ts rollup 开发环境的入口文件 88 | │ │ ├─ index.ts 共享的入口文件,基本逻辑都从这开始 89 | │ │ └─ logger.ts 一个简单的 Logger 90 | │ ├─ preload 91 | │ │ ├─ index.ts preload 入口 92 | │ │ └─ another.ts 另一个 preload 入口 93 | │ ├─ renderer 94 | │ │ ├─ assets/ assets 文件夹 95 | │ │ ├─ components/ 所有 vue components 96 | │ │ ├─ hooks/ 钩子函数或组合式 API 97 | │ │ ├─ router.ts vue-router 初始代码 98 | │ │ ├─ store.ts vuex 初始代码 99 | │ │ ├─ App.vue Vue 文件的入口文件,被 index.ts 导入 100 | │ │ ├─ index.css vite 会编译的 css 的入口文件 101 | │ │ ├─ index.html vite 会编译的 html 的入口文件 102 | │ │ └─ index.ts vite 会编译的 typescript 的入口文件 103 | │ └─ shared 在 main 和 renderer 之间共享的代码文件夹,其中代码两边都可以 import 到 104 | │ ├─ store/ vuex store 的定义 105 | │ └─ sharedLib.ts 一个简单的 main/renderer 共享模块的例子 106 | ├─ static/ 静态资源文件夹 107 | ├─ .eslintrc.js 108 | ├─ .gitignore 109 | ├─ package.json 110 | └─ README.md 111 | ``` 112 | 113 | #### assets, 静态资源 (static), 构建资源... 有啥区别? 114 | 115 | assets 文件只在 Renderer 进程中使用,他们会被 vite,也就是 rollup 系统打包到最终的构建文件中,你可以直接在 vue/ts 文件中 import 他们,基本上不用自己关心。assets 默认位置在 [renderer/renderer/assets](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/src/renderer/assets) 116 | 117 | 静态资源,指的是一些需要被 main 进程在运行中使用的文件,比如你的系统托盘小图标 (Tray) 就需要放在 static 文件夹中,在运行时通过文件系统 (fs) 获取。或如你需要在 Windows 下运行一段 powershell,这些 powershell 文件通常就需要放在 static 文件夹下,并且在构建配置文件中明确标出 asarUnpack。默认静态文件夹在 [static](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/static)。 118 | 119 | 而构建资源是指那些被 `electron-builder` 使用的资源,他们会用来构建安装包等。例如程序的图标,安装程序的自定义脚本等。默认的程序图标放在 [build/icons](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/build/icons) 里。 120 | 121 | *请注意你的程序图标可能需要在各种地方使用!不要混淆他们!* 122 | - *对于安装包图标, 他们应该在 `build/icons`,这会影响例如安装包图标,或者在 File Explorer 中显示的图标* 123 | - *对于静态资源中的图标,一般是用作于设置 **(系统托盘) tray** 或者当前窗口在任务栏的图标* 124 | - *对于在 assets 中的图标,这些一般使用在页面内显示 logo* 125 | 126 | 127 | ### 主进程和渲染进程的概念 128 | 129 | 从 Electron 官方文档 [main and renderer processes](https://www.electronjs.org/docs/tutorial/quick-start#main-and-renderer-processes) 引用的解释。主进程 (main process) 是 130 | 131 | > - 主进程通过创建 BrowserWindow 实例来创建 网页。 每一个 BrowserWindow 实例在其渲染过程中运行网页, 当一个 BrowserWindow 实例被销毁时,对应的渲染过程也会被终止。 132 | > - 主进程 管理 所有网页及其对应的渲染进程。 133 | 134 | 而渲染进程 (renderer process) 则是 135 | 136 | > - 渲染进程只能管理相应的网页, 一个渲染进程的崩溃不会影响其他渲染进程。 137 | > - 渲染进程通过 IPC 与主进程通信在网在页上执行 GUI 操作。 出于安全和可能的资源泄漏考虑,直接从渲染器进程中调用与本地 GUI 有关的 API 受到限制。 138 | 139 | 一般来讲,主进程包含了你的核心业务逻辑,而渲染进程则负责显示。当然这不绝对,有些人认为主进程就应该只负责一些和系统交互的操作,不应该有重 CPU 的操作,因为如果主进程 CPU 负荷过高会将整个 App 卡住(幸好 Nodejs 大部分 IO API 都是 async,并不会卡住整个 app)。因此如果你有一些非常吃 CPU 的工作,应该考虑用 nodejs 的 [worker_thread](https://nodejs.org/api/worker_threads.html) 把他们放到别的线程中。 140 | 141 | 所以这方面的设计和你的 app 的业务高度相关,如果你的业务只是有频繁的 IO 操作,把这些逻辑放在 main 也没什么问题。如果你的业务需要占用 CPU 很长时间,你则需要考虑把他们放在 main 进程之外的地方了。 142 | 143 | 根据 electron 的 [security](https://www.electronjs.org/docs/tutorial/security) 教程。在这个模板中,renderer 进程 [默认情况下 **并没有** 权限去访问 nodejs 的模块](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content). Electron 在 `webPreferences` 里提供了 `preload` 选项来处理这种问题。在这个模板中,我们则提供了 `Service` 来处理这个问题。 144 | 145 | `Service` 是一系列定义在 `src/main/services` 文件夹下的 class。`Service` 所有的 public 方法,经过我们的封装,都可以简单地在 renderer 进程访问。 146 | 它也可以看作一个 main 和 renderer 进程之间的桥梁。你可以参考 [Service](#服务-service) 章节来一探究竟。 147 | 148 | ***请注意 Serivce 是一个非常朴素简单的解决方案。它是从一个更为复杂的个人项目中抽取成的一个相对简单的实现方案。它只是展示一种可能性,实际上你可能需要根据你的实际情况调整 Service 的设计!*** 149 | 150 | 151 | ### NPM 脚本 152 | 153 | #### `npm run dev` 154 | 155 | 开启 vite 开发环境,vite 将提供 renderer (浏览器端)的热重载。 156 | 同时开启一个 rollup 开发环境,检测 main 端的代码变化,如果 main 的代码有变动,它会自动重启你的整个 electron 程序。 157 | 158 | #### `npm run build` 159 | 160 | 将 `main` 和 `renderer` 的代码编译到 production 环境, 输出的代码在 `dist` 161 | 162 | #### `npm run build:production` 163 | 164 | 编译所有代码,并且使用 `electron-builder` 来你的 app build 成可执行的 exe 文件或者 zip 等。这个的配置文件在 [scripts/build.base.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/build.base.config.js)。 165 | 166 | #### `npm run build:dir` 167 | 168 | 编译所有代码, 并且使用 `electron-builder` 编译你的 app 到 production 环境,但它只输出文件夹形式的 build (不打包成安装程序),比如对于 windows x64,他会把你的程序编译到 `build/win-unpacked`,并不输出 installer。 169 | 170 | 自然,这个会比 `npm run build:production` 快。你可以使用它来快速测试 production 的软件运行状况。 171 | 172 | #### `npm run lint` 173 | 174 | 使用 eslint 来检查代码风格。 175 | 176 | #### `npm run lint:fix` 177 | 178 | 使用 eslint 来检查代码风格并尽可能的修复。 179 | 180 | ## 开发 181 | 182 | 本项目默认遵从 [security](https://www.electronjs.org/docs/tutorial/security)。在默认情况下,Renderer (浏览器) 不能访问 NodeJS 的模块,这意味着你不能在浏览器中直接访问 fs 来读写文件。你需要通过使用 [Service](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/src/main/services/Service.ts) 来访问 NodeJS 资源。在 vue 中使用 `useService('NameOfService')` 来获得 service 提供的方法。 183 | 184 | ### 服务 (Service) 185 | 186 | 所有 Service 的实现都放在 `src/main/services`。Service 应该包含一些 App 与网络、磁盘文件交互的的核心业务逻辑,这些逻辑将运行在主进程。Service 会自动暴露接口到渲染进程,在渲染进程中,你可以通过一个 `useService('NameOfService')` 来直接使用 Service。 187 | 188 | Serivce 本身是完全可选的。之所以有这种设计是因为 Electron 提倡的安全性 ***如果你认为这个设计是多余的,或者是过度设计,你完全可以移除它。*** 189 | 190 | #### 创建一个新 Service 191 | 192 | 在 `/src/main/services` 里添加一个文件叫 `BarService.ts` 193 | 194 | ```ts 195 | export default class BarService extends Service { 196 | async doSomeCoreLogic() { 197 | // 在这里做一些异步的核心业务逻辑 198 | } 199 | } 200 | ``` 201 | 202 | 之后你需要把这个 `BarService` 加到 `Services` 的接口中,在 `src/main/services/index.ts` 里: 203 | 204 | ```ts 205 | import { BarService } from './BarService' 206 | 207 | export interface Services { 208 | // ... 一些其他的 Services 209 | BarService: BarService 210 | } 211 | ``` 212 | 213 | 之后,你需要把它添加到 `src/main/index.ts` 的 `initializeServices` 里: 214 | 215 | ```ts 216 | async function initializeServices(logger: Logger) { 217 | initialize({ 218 | // ... 其他 services 的初始化 219 | BarService: new BarService(logger) 220 | }) 221 | } 222 | ``` 223 | 224 | 现在这个 `BarService` 已经可以被渲染进程调用了,只需要通过 `useService('BarService')` 就可以,详情请见 [Using Service in Renderer](#在渲染进程浏览器中使用某个-service). 225 | 226 | #### Services 之间的交互 227 | 228 | 如果你在一个 `Service` 中想使用其他 `Service`,比如 `FooService`。你需要使用 `@Inject` 装饰器。 229 | 230 | ```ts 231 | export default class BarService extends Service { 232 | @Inject('FooService') 233 | private fooService: FooService 234 | 235 | async doSomeCoreLogic() { 236 | const result = await this.fooService.foo() 237 | } 238 | } 239 | ``` 240 | 241 | #### 在渲染进程(浏览器)中使用某个 Service 242 | 243 | 在 renderer 进程中,你可以通过 `useService('nameOfService')` 直接访问所有 service 的所有异步(async)方法。 244 | 245 | 这是 [About.vue]() 里的一个例子,它在使用 `BaseService`. 246 | 247 | ```vue 248 | 256 | 257 | 280 | ``` 281 | 282 | #### 移除 Service 架构 283 | 284 | 如果你不喜欢 Service 的设计,你可以简单地把他们移除掉: 285 | 1. 删掉整个 `src/main/services` 文件夹 286 | 2. 删掉 `src/main/index.ts` 里面的 `import { initialize } from './services'` 和 `initialize(logger)` 287 | 288 | 当然你可以在 `npm init` 之初就选择不要 service。 289 | 290 | ### 静态资源 291 | 292 | 你需要把所有的的静态资源放置在 `static` 文件夹下. 293 | 294 | 如果你想在 main 进程中使用它们,你只需要通过 `/@static/` 这种方式来导入. 295 | 296 | 例如,我们有一个 `logo.png` 文件放在里面,我们在代码中只需要像如下方式导入: 297 | 298 | ```ts 299 | import logoPath from '/@static/logo.png' // 这个就是 logo 的绝对路径 300 | ``` 301 | 302 | 管理以上行为的插件在 `scripts/rollup.static.plugin.js` 中。 303 | 304 | ### Preload 脚本 305 | 306 | 无论你用不用 Service 的设计,你可能都需要考虑 preload,preload 让你能够在 renderer 进程中插入一些安全的可以访问 node 模块的代码。 307 | 308 | *如果你不知道啥是 preload,你可以阅读 [electron 关于 BrowserWindow 的官方文档](https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions)和[官方安全指南](https://www.electronjs.org/docs/tutorial/security).* 309 | 310 | 在这个模板中,我们已经配置好了 preload 的构建流程,所有 preload 都被放在 `/src/preload` 文件夹下。 311 | 312 | 你必须吧 preload 脚本放置在此文件夹下。如果你想在创建窗口的时候使用它,你需要通过 `import preloadPath from '/@preload/'` 这种方式导入它。 313 | 314 | 例如,如果你新加了一个 preload 文件,叫作 `/src/preload/my-preload.ts`, 315 | 你可以通过以下方式在创建 `BrowserWindow` 时引用它: 316 | 317 | ```ts 318 | import myPreloadPath from '/@preload/my-preload' 319 | 320 | new BrowserWindow({ 321 | webPreferences: { 322 | preload: myPreloadPath, 323 | } 324 | }) 325 | ``` 326 | 327 | preload 的 rollup 配置同样放置在 `rollup.config.js` 中。 328 | 329 | 在 `dist` 文件夹下,每个 preload script 会被编译成 `dist/.preload.js` 的形式。 330 | 331 | 管理以上行为的插件放置于 `scripts/rollup.preload.plugin.js`。 332 | 333 | ### 在渲染进程中使用 Hooks (Composable) 334 | 335 | Vue 3 的一大特性就是 [组合式 API](https://v3.cn.vuejs.org/api/composition-api.html)。你可以通过组合模式,将各种简单逻辑在 `setup` 函数中拼装出复杂的业务逻辑。这些组合函数都默认放在 `/src/renderer/hooks` 中。 336 | 337 | 下面就是官方文档中的例子,你在 `/src/renderer/hooks/mouse.ts` 里有以下代码: 338 | 339 | ```ts 340 | import { ref, onMounted, onUnmounted } from 'vue' 341 | 342 | export function useMousePosition() { 343 | const x = ref(0) 344 | const y = ref(0) 345 | 346 | function update(e) { 347 | x.value = e.pageX 348 | y.value = e.pageY 349 | } 350 | 351 | onMounted(() => { 352 | window.addEventListener('mousemove', update) 353 | }) 354 | 355 | onUnmounted(() => { 356 | window.removeEventListener('mousemove', update) 357 | }) 358 | 359 | return { x, y } 360 | } 361 | ``` 362 | 363 | 你可以把 `mouse.ts` 在 `/src/renderer/hooks/index.ts` 中导出: 364 | 365 | ```ts 366 | // 其他导出... 367 | 368 | export * from './mouse.ts' 369 | ``` 370 | 371 | 然后你在 `vue` 文件中,就可以这样来导入: 372 | 373 | ```vue 374 | 377 | 389 | ``` 390 | 391 | ### 在渲染进程中使用 Electron API 392 | 393 | 此项目在默认情况下已经封装了一些 electron API 供 renderer 进程使用,例如 `useShell`, `useClipboard`, `useIpc` 和 `useDialog`。 394 | 395 | *这些 API 是通过加载 `static/preload.js` 来实现的。如果你把 preload 在创建 BrowserWindow 的时候给移除了,在 renderer 进程这些就用不了了。* 396 | 397 | ```ts 398 | import { defineComponent } from 'vue' 399 | import { useShell } from '/@/hooks' 400 | 401 | export default defineComponent({ 402 | setup() { 403 | const shell = useShell() // 这个等价于 import { shell } from 'electron' 404 | } 405 | }) 406 | ``` 407 | 408 | 一般的 use 风格的 electron API 都等价于直接从 electron import,但是 `useDialog` 是唯一例外,你只能在其中使用 `async` 的 API。 409 | 410 | ### 管理依赖 411 | 412 | 如果你想添加新的 npm 包作为依赖使用,你需要注意这个依赖是不是一个基于 nodejs 的模块。如果它是一个 nodejs 的包,你需要把这个包名放进 `package.json` 的 `external` 列表中。这个列表是用于告诉 vite 不要优化某些依赖,如果你不在这里剔除他们,vite就会抱怨说“我优化不了这些!”之类的话。 413 | 414 | 415 | ```json 416 | { 417 | // ...other package.json content 418 | "dependencies": { 419 | // ...other dependencies 420 | "a-nodejs-package": "" 421 | }, 422 | "external": [ 423 | // ...other existed excluded packages 424 | "a-nodejs-package" // your new package 425 | ], 426 | // ...rest of package.json 427 | } 428 | ``` 429 | 430 | 当然如果这个依赖是纯 JS 实现,你就不需要把它加到这里面了。 431 | 432 | #### 原生 (Native) 依赖 433 | 434 | 如果你需要使用一些原生依赖(需要在安装时从源码重新构建成二进制的依赖),通常你需要 [node-gyp](https://github.com/nodejs/node-gyp),但是 `electron-builder` 会自动帮你重新构建 electron 版本的二进制文件。所以你一般不需要太在意这个。请注意,如果你在用 Windows,你可能需要安装 [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) 来帮助你安装构建依赖的环境。 435 | 436 | #### 自带二进制的依赖 437 | 438 | 有一些 package 中含有已经编译好的二进制文件,对于这种 package 我们不但需要把它放进 `external` 中,还需要对 electron-builder 的配置稍加改动。具体细节请参见 [在构建中剔除某些具体文件](#在构建中剔除某些具体文件) 章节。当然这对于正常开发流程并没有什么影响。 439 | 440 | ### 添加一个新的窗口 441 | 442 | 1. 在 `src/renderer` 下添加一个新的 html 文件 443 | 2. 在新添加的 html 文件中引用你新写的 ts/js 文件 444 | 3. 在主进程 `main/index.ts` 中加入一段创建此窗口的代码 445 | 446 | 例如你在 `src/renderer` 下面新增加了 `side.html` ,你需要在 `index.ts` 中加入类似以下代码: 447 | 448 | ```ts 449 | import preload from '/@preload/index' 450 | 451 | // 这个方法应该在启动的时候被调用 452 | function createANewWindow() { 453 | // 这部分和之前都一样,根据自己需求改 454 | const win = new BrowserWindow({ 455 | height: 600, 456 | width: 300, 457 | webPreferences: { 458 | preload, 459 | contextIsolation: true, 460 | nodeIntegration: false 461 | } 462 | }) 463 | 464 | // __windowUrls.side 就是指向你新添加的 html 的 url 465 | win.loadURL(__windowUrls.side) 466 | } 467 | 468 | ``` 469 | 470 | 在 `scripts/vite.config.js` 中会自动扫描 `src/renderer` 下的所有 html 文件,所以一般来说你不需要改 vite 的配置文件。 471 | 当然你可以参照 [vite 的官方文档](https://vitejs.dev/guide/build.html#multi-page-app)来更加自定义多页面的功能。 472 | 473 | ### 多线程 (Thread Worker) 474 | 475 | 如果你想在 main 进程中使用 nodejs 的 [worker_threads](https://nodejs.org/api/worker_threads.html),Worker 脚本需要被独立加载。 476 | 我们在这里已经集成了 Worker 脚本的打包构建流程。通常来讲,你不需要修改构建配置就可以添加新的 Worker。 477 | 478 | 如果你想把一个文件导入为 `worker_thread` 你只需要在导入路径后面加上 `?worker`。 479 | 480 | 如果你添加了一个叫 `src/main/workers/sha256.ts` 的 Worker 脚本,你可以这样来创建一个使用此脚本的 Worker: 481 | 482 | ```ts 483 | import createSha256Worker from './workers/sha256?worker' 484 | import { Worker } from 'worker_threads' 485 | 486 | const worker: Worker = createSha256Worker(/* options */) 487 | ``` 488 | 489 | Worker 的脚本会和普通的 main 进程代码一起在 rollup 中编译,在 `rollup.config.js` 中他们共享相同的 rollup 配置。 490 | 491 | 在 `dist` 文件夹下, Worker 的脚本会被编译成 `dist/.worker.js`。 492 | 493 | ### 在 VSCode 中 Debug 494 | 495 | 本项目内置配置好的 vscode debug 配置。你会在 .vscode/launch.json 中看到以下三个配置 496 | 497 | 1. Electron: Main (attach) 498 | 2. Electron: Renderer (attach) 499 | 3. Electron: Main & Renderer (attach) 500 | 501 | 502 | ```json 503 | { 504 | "version": "0.2.0", 505 | "configurations": [ 506 | { 507 | "name": "Electron: Main (attach)", 508 | "type": "node", 509 | "request": "attach", 510 | "cwd": "${workspaceFolder}", 511 | "outFiles": [ 512 | "${workspaceFolder}/dist/**/*.js" 513 | ], 514 | "smartStep": true, 515 | "sourceMaps": true, 516 | "protocol": "inspector", 517 | "port": 5858, 518 | "timeout": 20000 519 | }, 520 | { 521 | "name": "Electron: Renderer (attach)", 522 | "type": "chrome", 523 | "request": "attach", 524 | "port": 9222, 525 | "webRoot": "${workspaceFolder}", 526 | "timeout": 15000 527 | }, 528 | ], 529 | "compounds": [ 530 | { 531 | "name": "Electron: Main & Renderer (attach)", 532 | "configurations": ["Electron: Main (attach)", "Electron: Renderer (attach)"] 533 | } 534 | ] 535 | } 536 | ``` 537 | 538 | 如果你看得懂的话就比较清晰了. 第一个是 attach 到 Electron 的 main 进程上。第二个是 attach 到 Renderer 进程上(需要 vscode 安装 Chrome Debugger 插件)。第三个则是这俩的合体,两个都 attach 上。 539 | 540 | 注意,这些配置都是 attach 模式,你需要先通过 `npm run dev` 启动 Electron 后使用。 541 | 542 | ### 可选项: 在渲染进程中使用 Node 模块 543 | 544 | 默认情况下,渲染进程就是个普通的前端浏览器环境。你不能在里面访问 nodejs 模块。 545 | 546 | 如果你是在想在里面用 node 模块,或者你对 service 的设计感到厌倦,你可以直接在你创建 BrowserWindow 之初开启 `nodeIntegration`。这个属性开启会让你的 renderer 进程也能访问 node。 547 | 548 | 比如你可以有如下代码让我们的主窗口能访问 node: 549 | 550 | ```ts 551 | const mainWindow = new BrowserWindow({ 552 | height: 600, 553 | width: 800, 554 | webPreferences: { 555 | preload: join(__static, 'preload.js'), 556 | nodeIntegration: true // 这让这个浏览器可以访问 node 557 | } 558 | }) 559 | ``` 560 | 561 | ## 构建 562 | 563 | 此项目的构建是直接使用 [electron-builder](https://github.com/electron-userland/electron-builder) 来达成的。它的配置主要放在 [scripts/build.base.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/build.base.config.js) 文件中。当然你可以参考 electron-builder 的[官方文档](https://www.electron.build/)来使用。 564 | 565 | ### 编译流程 566 | 567 | 首先,我们会将 typescript/vue 的源码通过 rollup 以 production 模式编译成 JavaScript。rollup 对主进程的编译配置在 [rollup.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/rollup.config.js) 中,它会把编译出来的结果输出到 `dist/index.js`。 568 | 569 | 注意,因为 rollup 是基于 esm 的,对循环依赖的处理没法像 webpack 那样理想,所以在尝试打包不少 nodejs 的 package 时会遇到循环依赖的问题。而你自己对这些 package 没有掌控 (webpack 一般能处理这种循环依赖的问题,并不会直接失败掉),所以此项目默认带的 rollup 构建脚本是不会打包 main 中使用的 nodejs 依赖的,你只要在 `package.json` 中把他们标注成 `external`,这些依赖就会以 `node_modules` 的形式存在于我们的构建输出的 asar 中。所以当你发现 `index.js` 中没有打包 nodejs 的依赖代码也别感到奇怪就是了。 570 | 571 | 而渲染进程的编译配置放在 [vite.config.js](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/scripts/vite.config.js) 里,它会将结果输出到 `dist/renderer/*` 里。 572 | 573 | #### 加速编译 574 | 575 | 如果你觉得编译时间实在是太长了,一般来说这种情况都是因为 main 进程的代码要类型检查很久。 576 | 577 | 你可以在 rollup.config.js 里设置 typescript 插件的参数来跳过类型检查: 578 | 579 | ```ts 580 | pluginTypescript({ 581 | tsconfig: [join(__dirname, '../src/main/tsconfig.json'), join(__dirname, '../src/preload/tsconfig.json')], 582 | wait: false // 设置这个会让 rollup build 跳过类型检查的结果 583 | }), 584 | ``` 585 | 586 | 关掉 wait 你还是能看到类型检查的结果,但是这个结果的失败不会再影响你的 rollup build 了。 587 | 588 | 如果你关了还是觉得慢,你可以把这个插件移除出 rollup.config.js 589 | 590 | ### 在构建中剔除某些具体文件 591 | 592 | 通常来讲,如果你的 `dependencies` 和 `external` 配置正确,你不需要太担心构建的问题。但是有一些依赖包含了已经编译好的二进制。你可能希望正确打包这些预编译的二进制文件。 593 | 594 | 例如, [7zip-min](https://github.com/onikienko/7zip-min): 595 | 596 | 因为它引用了 `7zip-bin`,而 `7zip-bin` 自带了针对多平台的二进制文件,我们需要妥善处理这些已经 build 好的二进制文件。我们自然不希望在某一个平台的构件中看到另一个平台的二进制文件。 597 | 更改 electron-builder 的构建配置: `build.base.config.js` 598 | 599 | ```js 600 | asarUnpack: [ 601 | "node_modules/7zip-bin/**/*" 602 | ], 603 | ``` 604 | 605 | 将他们添加到 `asarUnpack` 中来保证 electron-builder 在安装后会正确解压这些二进制文件。 606 | 607 | 你还需要在 `build.config.js` 中为每个平台配置 `files`,这样就不会让某个平台的出现在它不该出现的地方了: 608 | 609 | ```js 610 | mac: { 611 | // ... 其他 mac 配置 612 | files: [ 613 | "node_modules/7zip-bin/**/*", 614 | "!node_modules/7zip-bin/linux/**", 615 | "!node_modules/7zip-bin/win/**" 616 | ] 617 | }, 618 | win: { 619 | // ... 其他 win 配置 620 | files: [ 621 | "node_modules/7zip-bin/**/*", 622 | "!node_modules/7zip-bin/linux/**", 623 | "!node_modules/7zip-bin/mac/**" 624 | ] 625 | }, 626 | linux: { 627 | // ... 其他 linux 配置 628 | files: [ 629 | "node_modules/7zip-bin/**/*", 630 | "!node_modules/7zip-bin/win/**", 631 | "!node_modules/7zip-bin/mac/**" 632 | ] 633 | }, 634 | ``` 635 | 636 | ## 发布 637 | 638 | 自带的 github action 会在你每个 PR 提交的时跑 eslint 和 `npm run build`。并不会做完整的 build (因为 build production 比较花时间,当然你可以自己打开) 639 | 640 | 当有新的 push 进了 master branch,github action 会自动在 windows/mac/linux 上 build 生产环境的代码。如果构建都成功了,除了会把构建的输出上传到 github assets 之外,它还会创建一个 PR,其中给你提升了 package.json 的版本号,并且会写新的 changelog 到 changelog.md 中。 641 | 642 | 如果你想要它自动生成 changelog,你得遵循 [conventional commit guideline](https://www.conventionalcommits.org/en/v1.0.0)。 643 | 644 | 实际应用中你只需要检查这个 PR,如果没啥问题点击通过,它就会再 build 一遍,并且将结果发布到 github release 上。 645 | 646 | **如果你不需要这种自动流程,你可以将以下文件移除 [.github/workflows/build.yml](https://github.com/ci010/electron-vue-next/tree/master/electron-vue-next/.github/workflows/build.yml)** 647 | 648 | ### 自动更新的支持 649 | 650 | 这个模板默认自带了 [electron-updater](https://github.com/electron-userland/electron-builder/tree/master/packages/electron-updater)。你可以参照 [electron-builder](https://github.com/electron-userland/electron-builder) 的流程来实现自动更新。 651 | -------------------------------------------------------------------------------- /electron-vue-next/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # https://github.com/jokeyrhyme/standard-editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # defaults 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /electron-vue-next/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('eslint').Linter.Config} 3 | */ 4 | module.exports = { 5 | env: { 6 | es2021: true, 7 | browser: true, 8 | node: true 9 | }, 10 | extends: [ 11 | 'plugin:vue/essential', 12 | 'standard' 13 | ], 14 | globals: { 15 | __static: true, 16 | __windowUrls: true, 17 | __preloads: true, 18 | __workers: true, 19 | NodeJS: true 20 | }, 21 | parserOptions: { 22 | ecmaVersion: 12, 23 | parser: '@typescript-eslint/parser', 24 | sourceType: 'module' 25 | }, 26 | plugins: [ 27 | 'vue', 28 | '@typescript-eslint' 29 | ], 30 | rules: { 31 | 'space-before-function-paren': 0, 32 | 'vue/no-multiple-template-root': 0, 33 | 'import/no-absolute-path': 0 34 | }, 35 | ignorePatterns: [ 36 | 'node_modules/**', 37 | 'dist/**' 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/get-build-number/action.yml: -------------------------------------------------------------------------------- 1 | name: TEST 2 | description: Prepare Pull Request for the current change 3 | outputs: 4 | build_number: 5 | description: The release body 6 | runs: 7 | using: 'node12' 8 | main: index.js 9 | 10 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/get-build-number/index.js: -------------------------------------------------------------------------------- 1 | async function main(output) { 2 | output('build_number', process.env.GITHUB_RUN_NUMBER); 3 | } 4 | 5 | function setOutput(name, value) { 6 | process.stdout.write(Buffer.from(`::set-output name=${name}::${value}`)) 7 | } 8 | 9 | main(setOutput); 10 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/get-build-number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.2.6" 5 | } 6 | } -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/get-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Get Package Version 2 | description: Get NPM Package Version 3 | outputs: 4 | version: 5 | description: The version 6 | runs: 7 | using: 'node12' 8 | main: index.js 9 | 10 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/get-version/index.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | 3 | async function main(output) { 4 | output('version', JSON.parse(readFileSync('./package.json')).version) 5 | } 6 | 7 | function setOutput(name, value) { 8 | process.stdout.write(Buffer.from(`::set-output name=${name}::${value}`)) 9 | } 10 | 11 | main(setOutput); 12 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/get-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.2.6" 5 | } 6 | } -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/update-version-changelog/action.yml: -------------------------------------------------------------------------------- 1 | name: Update Version and Write Changelog 2 | description: Update the version and write changelog 3 | inputs: 4 | version: 5 | description: The version to write 6 | default: '' 7 | required: false 8 | changelog: 9 | description: The changelog to write 10 | default: '' 11 | required: false 12 | runs: 13 | using: 'node12' 14 | main: index.js 15 | 16 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/update-version-changelog/index.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync, readFileSync } = require("fs"); 2 | 3 | function main(input) { 4 | const version = input('version') 5 | const changelog = input('changelog') 6 | 7 | if (changelog) { 8 | const content = changelog + readFileSync('./CHANGELOG.md').toString() 9 | writeFileSync('./CHANGELOG.md', Buffer.from(content)) 10 | } 11 | if (version) { 12 | const package = JSON.parse(readFileSync('./package.json')) 13 | package.version = version 14 | writeFileSync('./package.json', JSON.stringify(package, null, 2)) 15 | } 16 | } 17 | 18 | function getInput(name) { 19 | return process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 20 | } 21 | 22 | main(getInput); 23 | -------------------------------------------------------------------------------- /electron-vue-next/.github/actions/update-version-changelog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.2.6" 5 | } 6 | } -------------------------------------------------------------------------------- /electron-vue-next/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'build/**' 9 | - 'scripts/*' 10 | - 'src/**' 11 | - 'static/**' 12 | - 'package.json' 13 | - 'package-lock.json' 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js 12 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | # - name: Get npm cache directory 28 | # id: npm-cache 29 | # run: | 30 | # echo "::set-output name=dir::$(npm config get cache)" 31 | # - uses: actions/cache@v1 32 | # with: 33 | # path: ${{ steps.npm-cache.outputs.dir }} 34 | # key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 35 | # restore-keys: | 36 | # ${{ runner.os }}-node- 37 | - name: Install 38 | run: | 39 | npm ci --no-optional 40 | - name: Get Build Number 41 | id: vars 42 | uses: ./.github/actions/get-build-number 43 | - name: Build 44 | run: | 45 | npm run build:production 46 | env: 47 | BUILD_NUMBER: ${{ steps.vars.outputs.build_number }} 48 | FULL_RELEASE: ${{ startsWith(github.event.commits[0].message, 'chore(release)') }} 49 | # If you build nsis web, you should uncomment below 50 | # - name: Upload Web Build 51 | # uses: actions/upload-artifact@v2 52 | # with: 53 | # name: build 54 | # path: build/nsis-web/*.* 55 | - name: Upload Build 56 | uses: actions/upload-artifact@v2 57 | with: 58 | name: build 59 | path: build/*.* 60 | - name: Upload Windows Asar 61 | if: ${{ runner.os == 'Windows' && startsWith(github.event.commits[0].message, 'chore(release)') }} 62 | uses: actions/upload-artifact@v2 63 | with: 64 | name: build 65 | path: build/win-unpacked/resources/app.asar 66 | 67 | repare-release: 68 | runs-on: ubuntu-latest 69 | needs: build 70 | if: ${{ !startsWith(github.event.commits[0].message, 'chore(release)') }} 71 | strategy: 72 | matrix: 73 | node-version: [12.x] 74 | steps: 75 | - uses: actions/checkout@v2 76 | - name: Fetch All 77 | run: git fetch --prune --unshallow 78 | - name: Use Node.js ${{ matrix.node-version }} 79 | uses: actions/setup-node@v1 80 | with: 81 | node-version: ${{ matrix.node-version }} 82 | - name: Install 83 | run: | 84 | npm ci --no-optional 85 | env: 86 | CI: true 87 | - name: Get Package Version 88 | uses: ./.github/actions/get-version 89 | id: package 90 | - name: Bump Version and Generate Changelog 91 | id: version 92 | uses: ci010/conventional-changelog-action@master 93 | with: 94 | github-token: ${{ secrets.github_token }} 95 | version: ${{ steps.package.outputs.version }} 96 | tag-prefix: 'v' 97 | - name: Update package.json and CHANGELOG.md 98 | uses: ./.github/actions/update-version-changelog 99 | with: 100 | version: ${{ steps.version.outputs.version }} 101 | changelog: ${{ steps.version.outputs.changelog }} 102 | - name: Create Pull Request 103 | if: ${{ steps.version.outputs.skipped == 'false' }} 104 | uses: peter-evans/create-pull-request@v3 105 | with: 106 | token: ${{ secrets.GITHUB_TOKEN }} 107 | commit-message: >- 108 | ${{ format('chore(release): {0}', steps.version.outputs.version) }} 109 | title: ${{ format('Prepare Release {0}', steps.version.outputs.version) }} 110 | body: ${{ steps.version.outputs.clean_changelog }} 111 | branch: prepare-release 112 | 113 | release: 114 | if: startsWith(github.event.commits[0].message, 'chore(release)') 115 | runs-on: ubuntu-latest 116 | needs: build 117 | steps: 118 | - uses: actions/checkout@v2 119 | - name: Use Node.js 12 120 | uses: actions/setup-node@v1 121 | with: 122 | node-version: 12 123 | - name: Fetch All 124 | run: git fetch --prune --unshallow 125 | # - uses: actions/cache@v1 126 | # with: 127 | # path: ~/.npm 128 | # key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 129 | # restore-keys: | 130 | # ${{ runner.os }}-node- 131 | - name: Install 132 | run: | 133 | npm ci --no-optional 134 | - name: Download Build 135 | uses: actions/download-artifact@v2 136 | with: 137 | name: build 138 | path: build 139 | - name: Get Package Version 140 | uses: ./.github/actions/get-version 141 | id: package 142 | - name: Prepare Release 143 | id: prepare_release 144 | uses: ci010/conventional-changelog-action@master 145 | with: 146 | github-token: ${{ secrets.github_token }} 147 | tag-prefix: 'v' 148 | - name: Draft Release 149 | id: create_release 150 | uses: voxelum/create-release@master 151 | env: 152 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 153 | with: 154 | tag_name: ${{ format('v{0}', steps.package.outputs.version) }} 155 | release_name: ${{ format('v{0}', steps.package.outputs.version) }} 156 | draft: false 157 | prerelease: false 158 | body: ${{ steps.prepare_release.outputs.clean_changelog }} 159 | asset_dir_path: ./build 160 | -------------------------------------------------------------------------------- /electron-vue-next/.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - name: Install 16 | run: | 17 | npm ci 18 | - name: Lint 19 | run: | 20 | npm run lint 21 | - name: Build 22 | run: | 23 | npm run build 24 | 25 | -------------------------------------------------------------------------------- /electron-vue-next/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | build/* 5 | !build/icons 6 | *.local 7 | thumbs.db 8 | /extensions -------------------------------------------------------------------------------- /electron-vue-next/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "eamodio.gitlens", 7 | "dbaeumer.vscode-eslint", 8 | "yzhang.markdown-all-in-one", 9 | "johnsoncodehk.volar", 10 | "msjsdiag.debugger-for-chrome" 11 | ], 12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 13 | "unwantedRecommendations": [ 14 | "octref.vetur" 15 | ] 16 | } -------------------------------------------------------------------------------- /electron-vue-next/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Electron: Main (attach)", 9 | "type": "node", 10 | "request": "attach", 11 | "cwd": "${workspaceFolder}", 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/**/*.js" 14 | ], 15 | "smartStep": true, 16 | "sourceMaps": true, 17 | "protocol": "inspector", 18 | "port": 5858, 19 | "timeout": 20000 20 | }, 21 | { 22 | "name": "Electron: Renderer (attach)", 23 | "type": "chrome", 24 | "request": "attach", 25 | "port": 9222, 26 | "webRoot": "${workspaceFolder}", 27 | "timeout": 15000 28 | }, 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Electron: Main & Renderer (attach)", 33 | "configurations": ["Electron: Main (attach)", "Electron: Renderer (attach)"] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /electron-vue-next/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "eslint.options": { 4 | "extensions": [ 5 | ".js", 6 | ".jsx" 7 | ] 8 | }, 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "typescriptreact", 14 | "vue" 15 | ], 16 | "vetur.format.defaultFormatter.js": "vscode-typescript", 17 | "vetur.validation.templateProps": true, 18 | "vetur.experimental.templateInterpolationService": true, 19 | "vetur.format.defaultFormatter.ts": "vscode-typescript" 20 | } -------------------------------------------------------------------------------- /electron-vue-next/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ci010/electron-vue-next/4f8772e38a99a4bc5268e06d6766a0e5a2f2edb7/electron-vue-next/CHANGELOG.md -------------------------------------------------------------------------------- /electron-vue-next/build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ci010/electron-vue-next/4f8772e38a99a4bc5268e06d6766a0e5a2f2edb7/electron-vue-next/build/icons/256x256.png -------------------------------------------------------------------------------- /electron-vue-next/build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ci010/electron-vue-next/4f8772e38a99a4bc5268e06d6766a0e5a2f2edb7/electron-vue-next/build/icons/icon.icns -------------------------------------------------------------------------------- /electron-vue-next/build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ci010/electron-vue-next/4f8772e38a99a4bc5268e06d6766a0e5a2f2edb7/electron-vue-next/build/icons/icon.ico -------------------------------------------------------------------------------- /electron-vue-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-vue-next", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ci010/electron-vue-next.git" 9 | }, 10 | "scripts": { 11 | "dev:docs": "node scripts/dev.docs.js", 12 | "dev": "node scripts/dev.js", 13 | "build": "node scripts/build.js", 14 | "build:dir": "cross-env BUILD_TARGET=dir node scripts/build.js", 15 | "build:lite": "cross-env BUILD_TARGET=lite node scripts/build.js", 16 | "build:production": "cross-env BUILD_TARGET=production node scripts/build.js", 17 | "lint": "eslint --ext .ts,.vue,.js src scripts", 18 | "lint:fix": "npm run lint -- --fix", 19 | "postinstall": "node ./scripts/dev.install.js" 20 | }, 21 | "author": { 22 | "email": "example@email.com", 23 | "name": "example" 24 | }, 25 | "license": "MIT", 26 | "dependencies": { 27 | "electron-updater": "^4.3.5", 28 | "vue": "^3.0.6", 29 | "vue-router": "^4.0.10", 30 | "vuex": "^4.0.2" 31 | }, 32 | "external": [ 33 | "electron-updater", 34 | "fs-extra" 35 | ], 36 | "devDependencies": { 37 | "@rollup/plugin-alias": "^3.1.2", 38 | "@rollup/plugin-commonjs": "^19.0.0", 39 | "@rollup/plugin-json": "^4.1.0", 40 | "@rollup/plugin-node-resolve": "^13.0.0", 41 | "@rollup/plugin-typescript": "^8.2.1", 42 | "@vitejs/plugin-vue": "^1.2.3", 43 | "@vue/compiler-sfc": "^3.0.8", 44 | "builtin-modules": "^3.1.0", 45 | "chalk": "^4.1.0", 46 | "cross-env": "^7.0.2", 47 | "electron": "^13.1.2", 48 | "electron-builder": "^22.11.7", 49 | "esbuild": "^0.12.8", 50 | "extract-zip": "^1.7.0", 51 | "fs-extra": "^9.0.1", 52 | "got": "^9.6.0", 53 | "magic-string": "^0.25.7", 54 | "rollup": "^2.38.5", 55 | "tslib": "^1.14.1", 56 | "typescript": "^4.1.2", 57 | "vite": "^2.3.7" 58 | }, 59 | "optionalDependencies": { 60 | "@types/node": "^14.14.7", 61 | "@typescript-eslint/eslint-plugin": "^4.7.0", 62 | "@typescript-eslint/parser": "^4.7.0", 63 | "eslint": "^7.13.0", 64 | "eslint-config-standard": "^14.1.1", 65 | "eslint-plugin-import": "^2.22.1", 66 | "eslint-plugin-node": "^11.1.0", 67 | "eslint-plugin-promise": "^4.2.1", 68 | "eslint-plugin-standard": "^4.0.2", 69 | "eslint-plugin-vue": "^7.1.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/build.base.config.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @type {import('electron-builder').Configuration} 4 | */ 5 | const config = { 6 | productName: '', 7 | appId: '', 8 | electronVersion: process.env.ELECTRON_VERSION, // only used for development debugging 9 | directories: { 10 | output: 'build', 11 | buildResources: 'build', 12 | app: 'dist' 13 | }, 14 | // assign publish for auto-updater 15 | // set this to your own repo! 16 | // publish: [{ 17 | // provider: 'github', 18 | // owner: '', 19 | // repo: '' 20 | // }], 21 | files: [ 22 | // don't include node_modules as all js modules are bundled into production js by rollup 23 | // unless you want to prevent some module to bundle up 24 | // list them below 25 | ] 26 | } 27 | 28 | module.exports = config 29 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/build.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./build.base.config') 2 | 3 | /** 4 | * @type {import('electron-builder').Configuration} 5 | */ 6 | const config = { 7 | ...baseConfig, 8 | nsis: { 9 | // eslint-disable-next-line no-template-curly-in-string 10 | artifactName: '${productName}-Setup-${version}.${ext}', 11 | oneClick: false, 12 | allowToChangeInstallationDirectory: true, 13 | perMachine: true, 14 | differentialPackage: true 15 | }, 16 | dmg: { 17 | contents: [ 18 | { 19 | x: 410, 20 | y: 150, 21 | type: 'link', 22 | path: '/Applications' 23 | }, 24 | { 25 | x: 130, 26 | y: 150, 27 | type: 'file' 28 | } 29 | ] 30 | }, 31 | mac: { 32 | icon: 'build/icons/icon.icns', 33 | target: [ 34 | { 35 | target: 'zip' 36 | }, 37 | { 38 | target: 'dmg' 39 | } 40 | ] 41 | }, 42 | win: { 43 | icon: 'build/icons/icon.ico', 44 | target: [ 45 | // disable build for x32 by default 46 | // 'nsis:ia32', 47 | 'nsis:x64', 48 | // uncomment to generate web installer 49 | // electron-builder can use either web or offline installer to auto update 50 | // { 51 | // target: 'nsis-web', 52 | // arch: [ 53 | // 'x64', 54 | // ] 55 | // }, 56 | { 57 | target: 'zip', 58 | arch: [ 59 | 'x64' 60 | ] 61 | } 62 | ] 63 | }, 64 | linux: { 65 | icon: 'build/icons', 66 | target: [ 67 | { 68 | target: 'deb' 69 | }, 70 | { 71 | target: 'rpm' 72 | }, 73 | { 74 | target: 'AppImage' 75 | }, 76 | { 77 | target: 'snap' 78 | } 79 | ] 80 | }, 81 | snap: { 82 | publish: [ 83 | 'github' 84 | ] 85 | } 86 | } 87 | 88 | module.exports = config 89 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/build.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production' 2 | 3 | const { join } = require('path') 4 | const { build } = require('vite') 5 | const chalk = require('chalk') 6 | const { build: electronBuilder } = require('electron-builder') 7 | const { stat, remove, writeFile } = require('fs-extra') 8 | const { rollup } = require('rollup') 9 | const { loadRollupConfig } = require('./util') 10 | 11 | /** 12 | * Generate the distribution version of package json 13 | */ 14 | async function generatePackageJson() { 15 | const original = require('../package.json') 16 | const result = { 17 | name: original.name, 18 | author: original.author, 19 | version: original.version, 20 | license: original.license, 21 | description: original.description, 22 | main: './index.js', 23 | dependencies: Object.entries(original.dependencies).filter(([name, version]) => original.external.indexOf(name) !== -1).reduce((object, entry) => ({ ...object, [entry[0]]: entry[1] }), {}) 24 | } 25 | await writeFile('dist/package.json', JSON.stringify(result)) 26 | } 27 | 28 | /** 29 | * Print the rollup output 30 | * @param {import('rollup').RollupOutput} output 31 | */ 32 | async function printOutput({ output }) { 33 | for (const chunk of output) { 34 | if (chunk.type === 'chunk') { 35 | const filepath = join('dist', chunk.fileName) 36 | const { size } = await stat(join(__dirname, '..', filepath)) 37 | console.log( 38 | `${chalk.gray('[write]')} ${chalk.cyan(filepath)} ${( 39 | size / 1024 40 | ).toFixed(2)}kb` 41 | ) 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Use rollup to build main process 48 | * @param {import('rollup').RollupOptions} config 49 | */ 50 | async function buildMain(config) { 51 | const input = { 52 | index: join(__dirname, '../src/main/index.ts') 53 | } 54 | 55 | const bundle = await rollup({ 56 | ...config, 57 | input 58 | }) 59 | if (!config.output) { 60 | throw new Error('Unexpected rollup config to build!') 61 | } 62 | 63 | await printOutput(await bundle.write(config.output[0])) 64 | } 65 | 66 | /** 67 | * Use vite to build renderer process 68 | */ 69 | function buildRenderer() { 70 | const config = require('./vite.config') 71 | 72 | console.log(chalk.bold.underline('Build renderer process')) 73 | 74 | return build({ 75 | ...config, 76 | mode: process.env.NODE_ENV 77 | }) 78 | } 79 | 80 | /** 81 | * Use electron builder to build your app to installer, zip, or etc. 82 | * 83 | * @param {import('electron-builder').Configuration} config The electron builder config 84 | * @param {boolean} dir Use dir mode to build 85 | */ 86 | async function buildElectron(config, dir) { 87 | console.log(chalk.bold.underline('Build electron')) 88 | const start = Date.now() 89 | const files = await electronBuilder({ publish: 'never', config, dir }) 90 | 91 | for (const file of files) { 92 | const fstat = await stat(file) 93 | console.log( 94 | `${chalk.gray('[write]')} ${chalk.yellow(file)} ${( 95 | fstat.size / 96 | 1024 / 97 | 1024 98 | ).toFixed(2)}mb` 99 | ) 100 | } 101 | 102 | console.log( 103 | `Build completed in ${((Date.now() - start) / 1000).toFixed(2)}s.` 104 | ) 105 | } 106 | 107 | async function start() { 108 | /** 109 | * Load electron-builder Configuration 110 | */ 111 | function loadElectronBuilderConfig() { 112 | switch (process.env.BUILD_TARGET) { 113 | case 'production': 114 | return require('./build.config') 115 | default: 116 | return require('./build.lite.config') 117 | } 118 | } 119 | 120 | const [mainConfig] = await loadRollupConfig() 121 | 122 | await remove(join(__dirname, '../dist')) 123 | 124 | console.log(chalk.bold.underline('Build main process & preload')) 125 | const startTime = Date.now() 126 | await buildMain(mainConfig) 127 | console.log( 128 | `Build completed in ${((Date.now() - startTime) / 1000).toFixed(2)}s.\n` 129 | ) 130 | await buildRenderer() 131 | 132 | console.log() 133 | if (process.env.BUILD_TARGET) { 134 | const config = loadElectronBuilderConfig() 135 | const dir = process.env.BUILD_TARGET === 'dir' 136 | await generatePackageJson() 137 | await buildElectron(config, dir) 138 | } 139 | } 140 | 141 | start().catch((e) => { 142 | console.error(chalk.red(e.toString())) 143 | process.exit(1) 144 | }) 145 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/build.lite.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./build.base.config') 2 | 3 | /** 4 | * @type {import('electron-builder').Configuration} 5 | */ 6 | const config = { 7 | ...baseConfig, 8 | mac: { 9 | icon: 'build/icons/icon.icns', 10 | target: [ 11 | { 12 | target: 'zip' 13 | } 14 | ], 15 | files: [ 16 | // 'node_modules/7zip-bin/**/*', 17 | // '!node_modules/7zip-bin/linux/**', 18 | // '!node_modules/7zip-bin/win/**' 19 | ] 20 | }, 21 | win: { 22 | icon: 'build/icons/icon.ico', 23 | target: [ 24 | { 25 | target: 'zip', 26 | arch: [ 27 | 'x64', 28 | 'ia32' 29 | ] 30 | } 31 | ], 32 | files: [ 33 | // 'node_modules/7zip-bin/**/*', 34 | // '!node_modules/7zip-bin/linux/**', 35 | // '!node_modules/7zip-bin/mac/**' 36 | ] 37 | }, 38 | linux: { 39 | icon: 'build/icons', 40 | target: [ 41 | { 42 | target: 'tar.gz' 43 | } 44 | ], 45 | files: [ 46 | // 'node_modules/7zip-bin/**/*', 47 | // '!node_modules/7zip-bin/win/**', 48 | // '!node_modules/7zip-bin/mac/**' 49 | ] 50 | } 51 | } 52 | 53 | module.exports = config 54 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/dev.install.js: -------------------------------------------------------------------------------- 1 | const got = require('got') 2 | const { createWriteStream } = require('fs-extra') 3 | const { pipeline } = require('stream') 4 | const extract = require('extract-zip') 5 | const { join } = require('path') 6 | const { unlinkSync } = require('fs') 7 | 8 | async function getLatest() { 9 | const result = await got('https://api.github.com/repos/vuejs/vue-devtools/releases?per_page=10', { 10 | headers: { 11 | accept: 'application/vnd.github.v3+json' 12 | }, 13 | json: true 14 | }) 15 | let founded 16 | for (const release of result.body) { 17 | const xpi = release.assets.find(a => a.name.endsWith('.xpi')) 18 | if (xpi) { 19 | founded = release 20 | break 21 | } 22 | } 23 | if (founded) { 24 | const url = founded.assets.find(a => a.name.endsWith('.xpi')).browser_download_url 25 | const stream = got.stream(url) 26 | const zipDest = join(__dirname, '../extensions.zip') 27 | await new Promise((resolve, reject) => { 28 | pipeline(stream, createWriteStream(zipDest), (e) => { 29 | if (e) reject(e) 30 | else resolve(undefined) 31 | }) 32 | }) 33 | const dir = join(__dirname, '../extensions') 34 | await new Promise((resolve, reject) => { 35 | extract(zipDest, { dir }, (err) => { 36 | if (err) reject(err) 37 | else resolve(undefined) 38 | }) 39 | }) 40 | unlinkSync(zipDest) 41 | } 42 | } 43 | 44 | getLatest() 45 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/dev.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | 3 | process.once('exit', terminate) 4 | .once('SIGINT', terminate) 5 | 6 | const electron = require('electron') 7 | const { spawn } = require('child_process') 8 | const { join, resolve } = require('path') 9 | const { createServer } = require('vite') 10 | const { createServer: createSocketServer } = require('net') 11 | const chalk = require('chalk') 12 | const { watch } = require('rollup') 13 | const { EOL } = require('os') 14 | const { loadRollupConfig } = require('./util') 15 | const { remove } = require('fs-extra') 16 | 17 | let manualRestart = false 18 | 19 | /** 20 | * @type {import('child_process').ChildProcessWithoutNullStreams | null} 21 | */ 22 | let electronProcess = null 23 | 24 | /** 25 | * The current active dev socket connecting to the electron main process. 26 | * Currently, this is only used for preloading the preload script 27 | * @type {import('net').Socket | null} 28 | */ 29 | let devSocket = null 30 | 31 | /** 32 | * The customized devserver communicated with the electron main process. 33 | * Currently, this is only used for preloading the preload script 34 | * @type {import('net').Server | null} 35 | */ 36 | let devServer = null 37 | 38 | /** 39 | * Start electron process and inspect port 5858 with 9222 as debug port. 40 | */ 41 | function startElectron() { 42 | /** @type {any} */ 43 | const electronPath = electron 44 | const spawnProcess = spawn( 45 | electronPath, 46 | ['--inspect=5858', '--remote-debugging-port=9222', join(__dirname, '../dist/index.js')] 47 | ) 48 | 49 | /** 50 | * @param {string | Buffer} data 51 | */ 52 | function electronLog(data) { 53 | const colorize = (line) => { 54 | if (line.startsWith('[INFO]')) { 55 | return chalk.green('[INFO]') + line.substring(6) 56 | } else if (line.startsWith('[WARN]')) { 57 | return chalk.yellow('[WARN]') + line.substring(6) 58 | } else if (line.startsWith('[ERROR]')) { 59 | return chalk.red('[ERROR]') + line.substring(7) 60 | } 61 | return chalk.grey('[CONSOLE] ') + line 62 | } 63 | console.log( 64 | data.toString() 65 | .split(EOL) 66 | .filter(s => s.trim() !== '') 67 | .filter(s => s.indexOf('source: chrome-extension:') === -1) 68 | .map(colorize).join(EOL) 69 | ) 70 | } 71 | 72 | spawnProcess.stdout.on('data', electronLog) 73 | spawnProcess.stderr.on('data', electronLog) 74 | spawnProcess.on('exit', (_, signal) => { 75 | if (!manualRestart) { 76 | // if (!devtoolProcess.killed) { 77 | // devtoolProcess.kill(0); 78 | // } 79 | if (!signal) { // Manual close 80 | process.exit(0) 81 | } 82 | } else { 83 | manualRestart = false 84 | } 85 | }) 86 | 87 | electronProcess = spawnProcess 88 | } 89 | 90 | /** 91 | * Kill and restart electron process 92 | */ 93 | function reloadElectron() { 94 | if (electronProcess) { 95 | manualRestart = true 96 | electronProcess.kill('SIGTERM') 97 | console.log(`${chalk.cyan('[DEV]')} ${chalk.bold.underline.green('Electron app restarted')}`) 98 | } else { 99 | console.log(`${chalk.cyan('[DEV]')} ${chalk.bold.underline.green('Electron app started')}`) 100 | } 101 | startElectron() 102 | } 103 | 104 | function reloadPreload() { 105 | if (devSocket) { 106 | devSocket.write(Buffer.from([0])) 107 | } 108 | } 109 | 110 | /** 111 | * Start vite dev server for renderer process and listen 8080 port 112 | */ 113 | async function startRenderer() { 114 | const config = require('./vite.config') 115 | 116 | config.mode = process.env.NODE_ENV 117 | 118 | const server = await createServer(config) 119 | return server.listen(8080) 120 | } 121 | 122 | /** 123 | * @param {import('rollup').RollupOptions} config 124 | */ 125 | async function loadMainConfig(config) { 126 | const input = { 127 | index: join(__dirname, '../src/main/index.dev.ts') 128 | } 129 | 130 | return { 131 | ...config, 132 | input, 133 | watch: { 134 | buildDelay: 500 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Main method of this script 141 | */ 142 | async function main() { 143 | const [mainConfig] = await loadRollupConfig() 144 | 145 | devServer = createSocketServer((sock) => { 146 | console.log(`${chalk.cyan('[DEV]')} Dev socket connected`) 147 | devSocket = sock 148 | sock.on('error', (e) => { 149 | // @ts-ignore 150 | if (e.code !== 'ECONNRESET') { 151 | console.error(e) 152 | } 153 | }) 154 | }).listen(3031, () => { 155 | console.log(`${chalk.cyan('[DEV]')} Dev server listening on 3031`) 156 | }) 157 | 158 | const preloadPrefix = resolve(__dirname, '../src/preload') 159 | let shouldReloadElectron = true 160 | let shouldReloadPreload = false 161 | const config = await loadMainConfig(mainConfig) 162 | await startRenderer() 163 | 164 | // start watch the main & preload 165 | watch(config) 166 | .on('change', (id) => { 167 | console.log(`${chalk.cyan('[DEV]')} change ${id}`) 168 | if (id.startsWith(preloadPrefix)) { 169 | shouldReloadPreload = true 170 | } else { 171 | shouldReloadElectron = true 172 | } 173 | }) 174 | .on('event', (event) => { 175 | switch (event.code) { 176 | case 'END': 177 | if (shouldReloadElectron || !electronProcess) { 178 | reloadElectron() 179 | shouldReloadElectron = false 180 | } else { 181 | console.log(`${chalk.cyan('[DEV]')} Skip start/reload electron.`) 182 | } 183 | if (shouldReloadPreload) { 184 | reloadPreload() 185 | shouldReloadPreload = false 186 | } else { 187 | console.log(`${chalk.cyan('[DEV]')} Skip start/reload preload.`) 188 | } 189 | break 190 | case 'BUNDLE_END': 191 | console.log(`${chalk.cyan('[DEV]')} Bundle ${event.output} ${event.duration + 'ms'}`) 192 | break 193 | case 'ERROR': 194 | console.error(event) 195 | shouldReloadElectron = false 196 | break 197 | } 198 | }) 199 | } 200 | 201 | remove(join(__dirname, '../dist')).then(() => main()).catch(e => { 202 | console.error(e) 203 | terminate() 204 | process.exit(1) 205 | }) 206 | 207 | function terminate() { 208 | if (electronProcess) { 209 | electronProcess.kill() 210 | electronProcess = null 211 | } 212 | if (devSocket) { 213 | devSocket.destroy() 214 | devSocket = null 215 | } 216 | if (devServer) { 217 | devServer.close() 218 | devServer = null 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "strict": true, 5 | "noImplicitAny": false, 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.assets.plugin.js: -------------------------------------------------------------------------------- 1 | import MagicString from 'magic-string' 2 | import { join } from 'path' 3 | 4 | export const assetsUrlRE = /__ASSETS__([a-z\d]{8})__(?:\$_(.*?)__)?/g 5 | 6 | /** 7 | * Replace internal assets symbol to real asset file path 8 | * @type {() => import('rollup').Plugin} 9 | */ 10 | const createPlugin = () => ({ 11 | name: 'electron:assets', 12 | renderChunk(code, chunk) { 13 | /** 14 | * @type {MagicString | undefined} 15 | */ 16 | let s 17 | for (let result = assetsUrlRE.exec(code); result; result = assetsUrlRE.exec(code)) { 18 | s = s || (s = new MagicString(code)) 19 | const [full, hash] = result 20 | const fileName = this.getFileName(hash) 21 | if (this.meta.watchMode) { 22 | s.overwrite(result.index, result.index + full.length, JSON.stringify(join(__dirname, '../dist', fileName))) 23 | } else { 24 | s.overwrite(result.index, result.index + full.length, JSON.stringify(fileName)) 25 | } 26 | } 27 | if (s) { 28 | return { code: s.toString(), map: s.generateMap({ hires: true }) } 29 | } else { 30 | return null 31 | } 32 | } 33 | }) 34 | 35 | export default createPlugin 36 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.devtool.plugin.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | /** 4 | * Provide vue-devtool extension virtual import. 5 | * @returns {import('rollup').Plugin} 6 | */ 7 | export default function createVueDevtoolsPlugin() { 8 | return { 9 | name: 'electron:devtools', 10 | async resolveId(id) { 11 | if (id === 'vue-devtools') { 12 | return id 13 | } 14 | }, 15 | async load(id) { 16 | if (id === 'vue-devtools') { 17 | const path = join(__dirname, '../extensions') 18 | return `export default ${JSON.stringify(path)}` 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.esbuild.plugin.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { transform } from 'esbuild' 3 | import { extname } from 'path' 4 | 5 | /** 6 | * Wrap esbuild to build typescript 7 | * @type {() => import('rollup').Plugin} 8 | */ 9 | const createPlugin = () => { 10 | return ({ 11 | name: 'main:esbuild', 12 | async resolveId(id, importer) { 13 | if (/\?commonjs/.test(id) || id === 'commonjsHelpers.js' || id.endsWith('js')) { 14 | return 15 | } 16 | if (id.endsWith('.ts')) { 17 | return 18 | } 19 | const tsResult = await this.resolve(`${id}.ts`, importer, { skipSelf: true }) 20 | if (tsResult) { 21 | return tsResult 22 | } 23 | const indexTsResult = await this.resolve(`${id}/index.ts`, importer, { skipSelf: true }) 24 | if (indexTsResult) { 25 | return indexTsResult 26 | } 27 | }, 28 | async transform(code, id) { 29 | if (id.endsWith('js') || id.endsWith('js?commonjs-proxy')) { 30 | return 31 | } 32 | if (!id.endsWith('.ts')) { 33 | return 34 | } 35 | function printMessage(m, code) { 36 | console.error(chalk.yellow(m.text)) 37 | if (m.location) { 38 | const lines = code.split(/\r?\n/g) 39 | const line = Number(m.location.line) 40 | const column = Number(m.location.column) 41 | const offset = 42 | lines 43 | .slice(0, line - 1) 44 | .map((l) => l.length) 45 | .reduce((total, l) => total + l + 1, 0) + column 46 | console.error( 47 | require('@vue/compiler-dom').generateCodeFrame(code, offset, offset + 1) 48 | ) 49 | } 50 | } 51 | try { 52 | const result = await transform(code, { 53 | // @ts-ignore 54 | loader: extname(id).slice(1), 55 | sourcemap: true, 56 | sourcefile: id, 57 | target: 'es2020' 58 | }) 59 | if (result.warnings.length) { 60 | console.error(`[main] warnings while transforming ${id} with esbuild:`) 61 | result.warnings.forEach((m) => printMessage(m, code)) 62 | } 63 | return { 64 | code: result.code, 65 | map: result.map 66 | } 67 | } catch (e) { 68 | console.error( 69 | chalk.red(`[main] error while transforming ${id} with esbuild:`) 70 | ) 71 | if (e.errors) { 72 | e.errors.forEach((m) => printMessage(m, code)) 73 | } else { 74 | console.error(e) 75 | } 76 | return { 77 | code: '', 78 | map: undefined 79 | } 80 | } 81 | }, 82 | buildEnd(error) { 83 | // Stop the service early if there's error 84 | if (error && !this.meta.watchMode) { 85 | console.log('esbuild service stop!') 86 | } 87 | }, 88 | generateBundle() { 89 | if (!this.meta.watchMode) { 90 | console.log('esbuild service stop!') 91 | } 92 | } 93 | }) 94 | } 95 | 96 | export default createPlugin 97 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.preload.plugin.js: -------------------------------------------------------------------------------- 1 | import { basename, extname, join } from 'path' 2 | import { cleanUrl } from './util' 3 | 4 | /** 5 | * Resolve the import of preload and emit it as single chunk of js file in rollup. 6 | * @returns {import('rollup').Plugin} 7 | */ 8 | export default function createPreloadPlugin() { 9 | return { 10 | name: 'electron:preload', 11 | 12 | resolveId(source) { 13 | if (source.startsWith('/@preload')) { 14 | return source.replace('/@preload', join(__dirname, '..', 'src', 'preload')) + '?preload' 15 | } 16 | }, 17 | async load(id) { 18 | if (id.endsWith('?preload')) { 19 | const clean = cleanUrl(id) 20 | const ext = extname(clean) 21 | const hash = this.emitFile({ 22 | type: 'chunk', 23 | id: clean, 24 | fileName: `${basename(cleanUrl(id), ext)}.preload.js` 25 | }) 26 | const path = `__ASSETS__${hash}__` 27 | if (this.meta.watchMode) { 28 | return `export default ${path}` 29 | } else { 30 | return `import { join } from 'path'; export default join(__dirname, ${path})` 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.renderer.plugin.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { basename, join } from 'path' 3 | import { cleanUrl } from './util' 4 | 5 | /** 6 | * Resolve import starts with `/@renderer` and ends with `.html` extension to the real file url. 7 | * @returns {import('rollup').Plugin} 8 | */ 9 | export default function createRendererPlugin() { 10 | return { 11 | name: 'electron:renderer', 12 | 13 | resolveId(source) { 14 | if (source.startsWith('/@renderer') && source.endsWith('.html')) { 15 | const target = source.replace('/@renderer', join(__dirname, '../src/renderer')) 16 | if (existsSync(target)) { 17 | return target + '?renderer' 18 | } 19 | } 20 | }, 21 | async load(id) { 22 | if (id.endsWith('?renderer')) { 23 | const clean = cleanUrl(id) 24 | if (this.meta.watchMode) { 25 | // devmode return dev server url 26 | const url = JSON.stringify(`http://localhost:8080/${basename(clean)}`) 27 | return `export default ${url};` 28 | } else { 29 | return `import { join } from 'path'; import { pathToFileURL } from 'url'; export default pathToFileURL(join(__dirname, 'renderer', ${JSON.stringify(basename(clean))})).toString();` 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.static.plugin.js: -------------------------------------------------------------------------------- 1 | import { existsSync, readFile } from 'fs-extra' 2 | import { basename, join } from 'path' 3 | import { cleanUrl } from './util' 4 | 5 | /** 6 | * Resolve import of static resource to real static resource path 7 | * @returns {import('rollup').Plugin} 8 | */ 9 | export default function createStaticPlugin() { 10 | return { 11 | name: 'electron:static', 12 | 13 | resolveId(source) { 14 | if (source.startsWith('/@static')) { 15 | const target = source.replace('/@static', join(__dirname, '../static')) 16 | if (existsSync(target)) { 17 | return target + '?static' 18 | } 19 | } 20 | }, 21 | async load(id) { 22 | if (id.endsWith('?static')) { 23 | const clean = cleanUrl(id) 24 | if (this.meta.watchMode) { 25 | // dev mode just return the file path 26 | return `export default ${JSON.stringify(clean)}` 27 | } else { 28 | const hash = this.emitFile({ 29 | fileName: join('static', basename(clean)), 30 | type: 'asset', 31 | source: await readFile(clean) 32 | }) 33 | return `import { join } from 'path'; export default join(__dirname, __ASSETS__${hash}__);` 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.typescript.plugin.js: -------------------------------------------------------------------------------- 1 | import { createSemanticDiagnosticsBuilderProgram, createWatchCompilerHost, createWatchProgram, DiagnosticCategory, formatDiagnosticsWithColorAndContext, sys } from 'typescript' 2 | 3 | /** 4 | * @param {number | void} timeout 5 | */ 6 | function createDeferred(timeout) { 7 | let promise 8 | let resolve = () => { } 9 | 10 | if (timeout) { 11 | promise = Promise.race([ 12 | // eslint-disable-next-line promise/param-names 13 | new Promise((r) => setTimeout(r, timeout, true)), 14 | // @ts-ignore 15 | // eslint-disable-next-line promise/param-names 16 | new Promise((r) => (resolve = r)) 17 | ]) 18 | } else { 19 | // @ts-ignore 20 | // eslint-disable-next-line promise/param-names 21 | promise = new Promise((r) => (resolve = r)) 22 | } 23 | 24 | return { promise, resolve } 25 | } 26 | 27 | const FILE_CHANGE_DETECTED = 6032 28 | const FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES = 6193 29 | const FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES = 6194 30 | 31 | /** 32 | * Typescript watch program helper to sync Typescript watch status with Rollup hooks. 33 | */ 34 | export class WatchProgramHelper { 35 | _startDeferred = null; 36 | _finishDeferred = null; 37 | 38 | watch(timeout = 1000) { 39 | // Race watcher start promise against a timeout in case Typescript and Rollup change detection is not in sync. 40 | this._startDeferred = createDeferred(timeout) 41 | this._finishDeferred = createDeferred() 42 | } 43 | 44 | /** 45 | * @param {import('typescript').Diagnostic} diagnostic 46 | */ 47 | handleStatus(diagnostic) { 48 | // Fullfil deferred promises by Typescript diagnostic message codes. 49 | if (diagnostic.category === DiagnosticCategory.Message) { 50 | switch (diagnostic.code) { 51 | case FILE_CHANGE_DETECTED: 52 | this.resolveStart() 53 | break 54 | 55 | case FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES: 56 | case FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES: 57 | this.resolveFinish() 58 | break 59 | 60 | default: 61 | } 62 | } 63 | } 64 | 65 | resolveStart() { 66 | if (this._startDeferred) { 67 | this._startDeferred.resolve(false) 68 | this._startDeferred = null 69 | } 70 | } 71 | 72 | resolveFinish() { 73 | if (this._finishDeferred) { 74 | this._finishDeferred.resolve(false) 75 | this._finishDeferred = null 76 | } 77 | } 78 | 79 | async wait() { 80 | if (this._startDeferred) { 81 | const timeout = await this._startDeferred.promise 82 | 83 | // If there is no file change detected by Typescript skip deferred promises. 84 | if (timeout) { 85 | this._startDeferred = null 86 | this._finishDeferred = null 87 | } 88 | 89 | if (this._finishDeferred) { 90 | await this._finishDeferred.promise 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Create a typecheck only typescript plugin 98 | * @param {{tsconfig?: string[]; tsconfigOverride?: import('typescript').CompilerOptions; wait?: boolean }} options 99 | * @returns {import('rollup').Plugin} 100 | */ 101 | const create = ({ tsconfig, tsconfigOverride, wait } = {}) => { 102 | const configPath = tsconfig 103 | if (!configPath) { 104 | throw new Error("Could not find a valid 'tsconfig.json'.") 105 | } 106 | const createProgram = createSemanticDiagnosticsBuilderProgram 107 | 108 | wait = wait ?? true 109 | 110 | /** 111 | * @type {import('typescript').FormatDiagnosticsHost} 112 | */ 113 | const formatHost = { 114 | getCanonicalFileName: path => path, 115 | getCurrentDirectory: sys.getCurrentDirectory, 116 | getNewLine: () => sys.newLine 117 | } 118 | 119 | /** 120 | * @type {import('typescript').WatchOfConfigFile[]} 121 | */ 122 | let programs 123 | 124 | const watcher = new WatchProgramHelper() 125 | 126 | /** 127 | * @type {import('typescript').Diagnostic[]} 128 | */ 129 | const diagnostics = [] 130 | 131 | /** 132 | * @type {import('rollup').Plugin} 133 | */ 134 | const plugin = { 135 | name: 'typescript:checker', 136 | buildStart() { 137 | if (!programs) { 138 | programs = configPath.map((c) => createWatchProgram(createWatchCompilerHost( 139 | c, 140 | tsconfigOverride || { noEmit: true, noEmitOnError: false }, 141 | sys, 142 | createProgram, 143 | (diagnostic) => { 144 | diagnostics.push(diagnostic) 145 | }, 146 | (diagnostic) => watcher.handleStatus(diagnostic) 147 | ))) 148 | } 149 | }, 150 | async load(id) { 151 | if (!id.endsWith('.ts')) { 152 | return null 153 | } 154 | const promise = watcher.wait() 155 | if (wait) { 156 | await promise 157 | } else { 158 | promise.then(() => { 159 | if (diagnostics.length > 0) { 160 | console.error(formatDiagnosticsWithColorAndContext(diagnostics.splice(0), formatHost)) 161 | } 162 | }) 163 | } 164 | }, 165 | generateBundle() { 166 | if (wait && diagnostics.length > 0) { 167 | const count = diagnostics.length 168 | console.error(formatDiagnosticsWithColorAndContext(diagnostics.splice(0), formatHost)) 169 | this.error(`Fail to compile the project. Found ${count} errors.`) 170 | } 171 | }, 172 | watchChange(id) { 173 | if (!id.endsWith('.ts')) { 174 | return 175 | } 176 | watcher.watch() 177 | }, 178 | buildEnd() { 179 | if (!this.meta.watchMode) { 180 | programs.forEach(p => p.close()) 181 | } 182 | } 183 | } 184 | 185 | return plugin 186 | } 187 | 188 | export default create 189 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/rollup.worker.plugin.js: -------------------------------------------------------------------------------- 1 | import { basename, extname } from 'path' 2 | import { cleanUrl, parseRequest } from './util' 3 | 4 | /** 5 | * Resolve ?worker import to the function creating the worker object 6 | * @returns {import('rollup').Plugin} 7 | */ 8 | export default function createWorkerPlugin() { 9 | return { 10 | name: 'electron:worker', 11 | 12 | resolveId(id, importer) { 13 | const query = parseRequest(id) 14 | if (typeof query.worker === 'string') { 15 | return id + `&importer=${importer}` 16 | } 17 | }, 18 | load(id) { 19 | const { worker, importer } = parseRequest(id) 20 | if (typeof worker === 'string' && typeof importer === 'string') { 21 | // emit as separate chunk 22 | const cleanPath = cleanUrl(id) 23 | const ext = extname(cleanPath) 24 | const hash = this.emitFile({ 25 | type: 'chunk', 26 | id: cleanPath, 27 | fileName: `${basename(cleanPath, ext)}.worker.js`, 28 | importer: importer 29 | }) 30 | const path = `__ASSETS__${hash}__` 31 | if (this.meta.watchMode) { 32 | return ` 33 | import { Worker } from 'worker_threads'; 34 | export default function (options) { return new Worker(${path}, options); }` 35 | } else { 36 | return ` 37 | import { join } from 'path'; 38 | import { Worker } from 'worker_threads'; 39 | export default function (options) { return new Worker(join(__dirname, ${path}), options); }` 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/plugins/util.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'querystring' 2 | import { URL } from 'url' 3 | 4 | export const queryRE = /\?.*$/ 5 | export const hashRE = /#.*$/ 6 | 7 | export const cleanUrl = (url) => 8 | url.replace(hashRE, '').replace(queryRE, '') 9 | 10 | export function parseRequest(id) { 11 | const { search } = new URL(id, 'file:') 12 | if (!search) { 13 | return {} 14 | } 15 | return parse(search.slice(1)) 16 | } 17 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | import pluginAlias from '@rollup/plugin-alias' 2 | import pluginCommonJs from '@rollup/plugin-commonjs' 3 | import pluginJson from '@rollup/plugin-json' 4 | import { nodeResolve } from '@rollup/plugin-node-resolve' 5 | import builtins from 'builtin-modules' 6 | import chalk from 'chalk' 7 | import { join } from 'path' 8 | import { external } from '../package.json' 9 | import pluginEsbuild from './plugins/rollup.esbuild.plugin' 10 | import pluginResolve from './plugins/rollup.assets.plugin' 11 | import pluginPreload from './plugins/rollup.preload.plugin' 12 | import pluginRenedrer from './plugins/rollup.renderer.plugin' 13 | import pluginStatic from './plugins/rollup.static.plugin' 14 | import pluginTypescript from './plugins/rollup.typescript.plugin' 15 | import pluginWorker from './plugins/rollup.worker.plugin' 16 | import pluginVueDevtools from './plugins/rollup.devtool.plugin' 17 | 18 | /** 19 | * @type {import('rollup').RollupOptions[]} 20 | */ 21 | const config = [{ 22 | // this is the rollup config of main process 23 | output: { 24 | dir: join(__dirname, '../dist'), 25 | format: 'cjs', 26 | sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : false 27 | }, 28 | onwarn: (warning) => { 29 | if (warning.plugin === 'typescript:checker') { 30 | console.log(chalk.yellow(warning.message)) 31 | } else { 32 | console.log(warning.plugin) 33 | console.log(chalk.yellow(warning.toString())) 34 | } 35 | }, 36 | external: [...builtins, 'electron', ...external], 37 | plugins: [ 38 | pluginAlias({ 39 | entries: { 40 | '/@main': join(__dirname, '../src/main'), 41 | '/@shared': join(__dirname, '../src/shared') 42 | } 43 | }), 44 | pluginVueDevtools(), 45 | pluginStatic(), 46 | pluginRenedrer(), 47 | pluginPreload(), 48 | pluginWorker(), 49 | pluginTypescript({ 50 | tsconfig: [join(__dirname, '../src/main/tsconfig.json'), join(__dirname, '../src/preload/tsconfig.json')], 51 | wait: false 52 | }), 53 | pluginResolve(), 54 | pluginEsbuild(), 55 | nodeResolve({ 56 | browser: false 57 | }), 58 | pluginCommonJs({ 59 | extensions: ['.js', '.cjs'] 60 | }), 61 | pluginJson({ 62 | preferConst: true, 63 | indent: ' ', 64 | compact: false, 65 | namedExports: true 66 | }) 67 | ] 68 | }] 69 | 70 | export default config 71 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/util.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const loadConfigFile = require('rollup/dist/loadConfigFile') 3 | 4 | /** 5 | * Load rollup config 6 | * @returns {Promise} 7 | */ 8 | async function loadRollupConfig() { 9 | const { options, warnings } = await loadConfigFile(join(__dirname, 'rollup.config.js'), { 10 | }) 11 | 12 | warnings.flush() 13 | 14 | return options 15 | } 16 | 17 | module.exports = { 18 | loadRollupConfig 19 | } 20 | -------------------------------------------------------------------------------- /electron-vue-next/scripts/vite.config.js: -------------------------------------------------------------------------------- 1 | const { join, resolve } = require('path') 2 | const { external } = require('../package.json') 3 | const { default: vue } = require('@vitejs/plugin-vue') 4 | const { readdirSync } = require('fs') 5 | 6 | const entries = readdirSync(join(__dirname, '../src/renderer')).filter(f => f.endsWith('.html')) 7 | .map(f => join(__dirname, '../src/renderer', f)) 8 | 9 | /** 10 | * Vite shared config, assign alias and root dir 11 | * @type {import('vite').UserConfig} 12 | */ 13 | const config = { 14 | root: join(__dirname, '../src/renderer'), 15 | base: '', // has to set to empty string so the html assets path will be relative 16 | build: { 17 | rollupOptions: { 18 | input: entries 19 | }, 20 | outDir: resolve(__dirname, '../dist/renderer'), 21 | assetsInlineLimit: 0 22 | }, 23 | resolve: { 24 | alias: { 25 | '/@shared': join(__dirname, '../src/shared'), 26 | '/@': join(__dirname, '../src/renderer') 27 | } 28 | }, 29 | optimizeDeps: { 30 | exclude: external 31 | }, 32 | // @ts-ignore 33 | plugins: [vue()] 34 | } 35 | 36 | module.exports = config 37 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/dialog.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, dialog } from 'electron' 2 | 3 | ipcMain.handle('dialog:showCertificateTrustDialog', (event, ...args) => { 4 | return dialog.showCertificateTrustDialog(args[0]) 5 | }) 6 | ipcMain.handle('dialog:showErrorBox', (event, ...args) => { 7 | return dialog.showErrorBox(args[0], args[1]) 8 | }) 9 | ipcMain.handle('dialog:showMessageBox', (event, ...args) => { 10 | return dialog.showMessageBox(args[0]) 11 | }) 12 | ipcMain.handle('dialog:showOpenDialog', (event, ...args) => { 13 | return dialog.showOpenDialog(args[0]) 14 | }) 15 | ipcMain.handle('dialog:showSaveDialog', (event, ...args) => { 16 | return dialog.showSaveDialog(args[0]) 17 | }) 18 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/index.dev.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | import { Socket } from 'net' 3 | // @ts-ignore 4 | import extensions from 'vue-devtools' 5 | // eslint-disable-next-line import/first 6 | import './index' 7 | 8 | app.on('browser-window-created', (event, window) => { 9 | if (!window.webContents.isDevToolsOpened()) { 10 | window.webContents.openDevTools() 11 | window.webContents.session.loadExtension(extensions) 12 | .catch((e) => { 13 | console.error('Fail to load vue extension. Please run "npm run postinstall" to install extension, or remove it and try again!') 14 | console.error(e) 15 | }) 16 | } 17 | }) 18 | 19 | const devServer = new Socket({}).connect(3031, '127.0.0.1') 20 | devServer.on('data', () => { 21 | BrowserWindow.getAllWindows().forEach(w => w.reload()) 22 | }) 23 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | import './dialog' 3 | import { Logger } from './logger' 4 | import { initialize } from './services' 5 | import createBaseWorker from './workers/index?worker' 6 | import indexPreload from '/@preload/index' 7 | import anotherPreload from '/@preload/another' 8 | import indexHtmlUrl from '/@renderer/index.html' 9 | import sideHtmlUrl from '/@renderer/side.html' 10 | import logoUrl from '/@static/logo.png' 11 | 12 | async function main() { 13 | const logger = new Logger() 14 | logger.initialize(app.getPath('userData')) 15 | initialize(logger) 16 | app.whenReady().then(() => { 17 | const main = createWindow() 18 | const [x, y] = main.getPosition() 19 | const side = createSecondWindow() 20 | side.setPosition(x + 800 + 5, y) 21 | }) 22 | // thread_worker example 23 | createBaseWorker({ workerData: 'worker world' }).on('message', (message) => { 24 | logger.log(`Message from worker: ${message}`) 25 | }).postMessage('') 26 | } 27 | 28 | function createWindow() { 29 | // Create the browser window. 30 | const mainWindow = new BrowserWindow({ 31 | height: 600, 32 | width: 800, 33 | webPreferences: { 34 | preload: indexPreload, 35 | contextIsolation: true, 36 | nodeIntegration: false 37 | }, 38 | icon: logoUrl 39 | }) 40 | 41 | mainWindow.loadURL(indexHtmlUrl) 42 | return mainWindow 43 | } 44 | 45 | function createSecondWindow() { 46 | const sideWindow = new BrowserWindow({ 47 | height: 600, 48 | width: 300, 49 | webPreferences: { 50 | preload: anotherPreload, 51 | contextIsolation: true, 52 | nodeIntegration: false 53 | } 54 | }) 55 | 56 | sideWindow.loadURL(sideHtmlUrl) 57 | return sideWindow 58 | } 59 | 60 | // ensure app start as single instance 61 | if (!app.requestSingleInstanceLock()) { 62 | app.quit() 63 | } 64 | 65 | app.on('window-all-closed', () => { 66 | if (process.platform !== 'darwin') { 67 | app.quit() 68 | } 69 | }) 70 | 71 | process.nextTick(main) 72 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/logger.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | import { createWriteStream } from 'fs' 3 | import { join, resolve } from 'path' 4 | import { PassThrough, pipeline, Transform } from 'stream' 5 | import { format } from 'util' 6 | 7 | function formatMsg(message: any, options: any[]) { return options.length !== 0 ? format(message, options) : format(message) } 8 | function baseTransform(tag: string) { return new Transform({ transform(c, e, cb) { cb(undefined, `[${tag}] [${new Date().toLocaleString()}] ${c}\n`) } }) } 9 | 10 | export interface LoggerFacade { 11 | log(message: any, ...options: any[]): void; 12 | warn(message: any, ...options: any[]): void; 13 | error(message: any, ...options: any[]): void; 14 | } 15 | 16 | export class Logger { 17 | private loggerEntries = { log: baseTransform('INFO'), warn: baseTransform('WARN'), error: baseTransform('ERROR') }; 18 | 19 | readonly log = (message: any, ...options: any[]) => { this.loggerEntries.log.write(formatMsg(message, options)) } 20 | 21 | readonly warn = (message: any, ...options: any[]) => { this.loggerEntries.warn.write(formatMsg(message, options)) } 22 | 23 | readonly error = (message: any, ...options: any[]) => { this.loggerEntries.error.write(formatMsg(message, options)) } 24 | 25 | private output = new PassThrough(); 26 | 27 | private logDirectory: string = '' 28 | 29 | constructor() { 30 | pipeline(this.loggerEntries.log, this.output, () => { }) 31 | pipeline(this.loggerEntries.warn, this.output, () => { }) 32 | pipeline(this.loggerEntries.error, this.output, () => { }) 33 | 34 | process.on('uncaughtException', (err) => { 35 | this.error('Uncaught Exception') 36 | this.error(err) 37 | }) 38 | process.on('unhandledRejection', (reason) => { 39 | this.error('Uncaught Rejection') 40 | this.error(reason) 41 | }) 42 | if (process.env.NODE_ENV === 'development') { 43 | this.output.on('data', (b) => { console.log(b.toString()) }) 44 | } 45 | app.once('browser-window-created', (event, window) => { 46 | this.captureWindowLog(window) 47 | }) 48 | } 49 | 50 | /** 51 | * Initialize log output directory 52 | * @param directory The directory of the log 53 | */ 54 | async initialize(directory: string) { 55 | this.logDirectory = directory 56 | const mainLog = join(directory, 'main.log') 57 | const stream = createWriteStream(mainLog, { encoding: 'utf-8', flags: 'w+' }) 58 | this.output.pipe(stream) 59 | this.log(`Setup main logger to ${mainLog}`) 60 | } 61 | 62 | /** 63 | * Capture the window log 64 | * @param window The browser window 65 | * @param name The name alias of the window. Use window.webContents.id by default 66 | */ 67 | captureWindowLog(window: BrowserWindow, name?: string) { 68 | name = name ?? window.webContents.id.toString() 69 | if (!this.logDirectory) { 70 | this.warn(`Cannot capture window log for window ${name}. Please initialize the logger to set logger directory!`) 71 | return 72 | } 73 | const loggerPath = resolve(this.logDirectory, `renderer.${name}.log`) 74 | this.log(`Setup renderer logger for window ${name} to ${loggerPath}`) 75 | const stream = createWriteStream(loggerPath, { encoding: 'utf-8', flags: 'w+' }) 76 | const levels = ['INFO', 'WARN', 'ERROR'] 77 | window.webContents.on('console-message', (e, level, message, line, id) => { 78 | stream.write(`[${levels[level]}] [${new Date().toUTCString()}] [${id}]: ${message}\n`) 79 | }) 80 | window.once('close', () => { 81 | window.webContents.removeAllListeners('console-message') 82 | stream.close() 83 | }) 84 | } 85 | 86 | /** 87 | * This will create a logger prepend [${tag}] before each log from it 88 | * @param tag The tag to prepend 89 | */ 90 | createLoggerFor(tag: string): LoggerFacade { 91 | function transform(tag: string) { return new Transform({ transform(c, e, cb) { cb(undefined, `[${tag}] ${c}\n`) } }) } 92 | const log = transform(tag).pipe(this.loggerEntries.log) 93 | const warn = transform(tag).pipe(this.loggerEntries.warn) 94 | const error = transform(tag).pipe(this.loggerEntries.error) 95 | 96 | return { 97 | log(message: any, ...options: any[]) { log.write(formatMsg(message, options)) }, 98 | warn(message: any, ...options: any[]) { warn.write(formatMsg(message, options)) }, 99 | error(message: any, ...options: any[]) { error.write(formatMsg(message, options)) } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/main.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | // declare electron static for static file serving 4 | 5 | declare module '*?worker' { 6 | import { Worker, WorkerOptions } from 'worker_threads' 7 | /** 8 | * The helper to create the worker 9 | */ 10 | export default function (options: WorkerOptions): Worker 11 | } 12 | 13 | declare module '/@renderer/*.html' { 14 | /** 15 | * The url of the page 16 | */ 17 | const url: string 18 | export default url 19 | } 20 | 21 | declare module '/@renderer/*' { 22 | const noop: never 23 | export default noop 24 | } 25 | 26 | declare module '/@static/*' { 27 | /** 28 | * The path of the static file 29 | */ 30 | const path: string 31 | export default path 32 | } 33 | 34 | declare module '/@preload/*' { 35 | /** 36 | * The path of the preload file 37 | */ 38 | const path: string 39 | export default path 40 | } 41 | 42 | declare namespace NodeJS { 43 | interface Global { 44 | __static: string 45 | __windowUrls: Record 46 | __preloads: Record 47 | __workers: Record 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/services/BaseService.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import { platform } from 'os' 3 | import { Service } from './Service' 4 | 5 | export class BaseService extends Service { 6 | async getBasicInformation() { 7 | this.log('getBasicInformation is called!') 8 | const result = { 9 | platform: platform(), 10 | version: app.getVersion(), 11 | root: app.getPath('userData') 12 | } 13 | return result 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/services/FooService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from './BaseService' 2 | import { Inject, Service } from './Service' 3 | import { add } from '/@shared/sharedLib' 4 | 5 | export class FooService extends Service { 6 | @Inject('BaseService') 7 | private baseService!: BaseService 8 | 9 | /** 10 | * Example for inject and shared lib 11 | */ 12 | async foo() { 13 | const result = await this.baseService.getBasicInformation() 14 | const sum = add(1, 2) 15 | this.log(`Call function imported from /shared folder! 1 + 2 = ${sum}`) 16 | return { 17 | ...result, 18 | foo: 'bar' 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/services/Service.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LoggerFacade } from '/@main/logger' 2 | 3 | export const INJECTIONS_SYMBOL = Symbol('__injections__') 4 | 5 | export function Inject(type: string) { 6 | return function (target: any, propertyKey: string) { 7 | if (!Reflect.has(target, INJECTIONS_SYMBOL)) { 8 | Reflect.set(target, INJECTIONS_SYMBOL, []) 9 | } 10 | if (!type) { 11 | throw new Error(`Inject recieved type: ${type}!`) 12 | } else { 13 | Reflect.get(target, INJECTIONS_SYMBOL).push({ type, field: propertyKey }) 14 | } 15 | } 16 | } 17 | 18 | export class Service { 19 | readonly name: string 20 | private logger: LoggerFacade 21 | 22 | constructor(logger: Logger) { 23 | this.name = Object.getPrototypeOf(this).constructor.name 24 | this.logger = logger.createLoggerFor(this.name) 25 | } 26 | 27 | protected log(m: any, ...a: any[]) { 28 | this.logger.log(`[${this.name}] ${m}`, ...a) 29 | } 30 | 31 | protected error(m: any, ...a: any[]) { 32 | this.logger.error(`[${this.name}] ${m}`, ...a) 33 | } 34 | 35 | protected warn(m: any, ...a: any[]) { 36 | this.logger.warn(`[${this.name}] ${m}`, ...a) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import { Logger } from '../logger' 3 | import { BaseService } from './BaseService' 4 | import { FooService } from './FooService' 5 | import { INJECTIONS_SYMBOL } from './Service' 6 | 7 | /** 8 | * All services definition 9 | */ 10 | export interface Services { 11 | FooService: FooService, 12 | BaseService: BaseService 13 | } 14 | 15 | let _services!: Services 16 | 17 | /** 18 | * Initialize the services module to serve client (renderer process) 19 | * 20 | * @param logger The simple app logger 21 | */ 22 | export function initialize(logger: Logger) { 23 | _initialize({ 24 | BaseService: new BaseService(logger), 25 | FooService: new FooService(logger) 26 | }) 27 | } 28 | 29 | /** 30 | * Initialize the services module to serve client (renderer process) 31 | * 32 | * @param services The running services for current app 33 | */ 34 | function _initialize(services: Services) { 35 | if (_services) { 36 | throw new Error('Should not initialize the services multiple time!') 37 | } 38 | _services = services 39 | for (const serv of Object.values(services)) { 40 | const injects = Object.getPrototypeOf(serv)[INJECTIONS_SYMBOL] || [] 41 | for (const i of injects) { 42 | const { type, field } = i 43 | if (type in services) { 44 | const success = Reflect.set(serv, field, (services as any)[type]) 45 | if (!success) { 46 | throw new Error(`Cannot set service ${type} to ${Object.getPrototypeOf(serv)}`) 47 | } 48 | } else { 49 | throw new Error(`Cannot find service named ${type}! Which is required by ${Object.getPrototypeOf(serv).constructor.name}`) 50 | } 51 | } 52 | } 53 | } 54 | 55 | export class ServiceNotFoundError extends Error { 56 | constructor(readonly service: string) { 57 | super(`Cannot find service named ${service}!`) 58 | } 59 | } 60 | export class ServiceMethodNotFoundError extends Error { 61 | constructor(readonly service: string, readonly method: string) { 62 | super(`Cannot find method named ${method} in service [${service}]!`) 63 | } 64 | } 65 | 66 | ipcMain.handle('service:call', (event, name: string, method: string, ...payloads: any[]) => { 67 | if (!_services) { 68 | throw new Error('Cannot call any service until the services are ready!') 69 | } 70 | const service = (_services as any)[name] 71 | if (!service) { 72 | throw new ServiceNotFoundError(name) 73 | } 74 | if (!service[method]) { 75 | throw new ServiceMethodNotFoundError(name, method) 76 | } 77 | return service[method](...payloads) 78 | }) 79 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "downlevelIteration": true, 6 | "resolveJsonModule": true, 7 | "lib": ["ESNext"], 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "baseUrl": ".", 11 | "paths": { 12 | "/@shared/*": [ 13 | "../shared/*" 14 | ], 15 | "/@main/*": [ 16 | "./*" 17 | ] 18 | }, 19 | "esModuleInterop": true, 20 | "experimentalDecorators": true, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true 23 | }, 24 | "include": [ 25 | "../shared/**/*", 26 | "." 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /electron-vue-next/src/main/workers/index.ts: -------------------------------------------------------------------------------- 1 | import { parentPort, workerData } from 'worker_threads' 2 | 3 | const port = parentPort 4 | if (!port) throw new Error('IllegalState') 5 | 6 | port.on('message', () => { 7 | port.postMessage(`hello ${workerData}`) 8 | }) 9 | -------------------------------------------------------------------------------- /electron-vue-next/src/preload/another.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs-extra' 2 | 3 | console.log('hello world 2nd preload!') 4 | 5 | /** 6 | * You can access node module here! 7 | */ 8 | export async function readSomeFile() { 9 | console.log('You can use module module in preload no matter the nodeIntegration!') 10 | return readFile('/abc') 11 | } 12 | -------------------------------------------------------------------------------- /electron-vue-next/src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { shell, clipboard, ipcRenderer, contextBridge, Dialog, IpcRenderer } from 'electron' 2 | 3 | console.log('hello world 1st preload!') 4 | 5 | /** 6 | * Wrapper of ipc renderer. 7 | * 8 | * So the `contextIsolation: true` won't prevent you to use method inherit from EventEmitter, 9 | * lile `ipcRenderer.on` 10 | */ 11 | const _ipcRenderer: IpcRenderer = { 12 | invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), 13 | on: (channel, listener) => { 14 | ipcRenderer.on(channel, listener) 15 | return _ipcRenderer 16 | }, 17 | once: (channel, listener) => { 18 | ipcRenderer.once(channel, listener) 19 | return _ipcRenderer 20 | }, 21 | postMessage: (channel, message, transfers) => ipcRenderer.postMessage(channel, message, transfers), 22 | removeAllListeners: (channel) => { 23 | ipcRenderer.removeAllListeners(channel) 24 | return _ipcRenderer 25 | }, 26 | removeListener: (channel, listener) => { 27 | ipcRenderer.removeListener(channel, listener) 28 | return _ipcRenderer 29 | }, 30 | send: (channel, ...args) => ipcRenderer.send(channel, ...args), 31 | sendSync: (channel, ...args) => ipcRenderer.sendSync(channel, ...args), 32 | sendTo: (id, channel, ...args) => ipcRenderer.sendTo(id, channel, ...args), 33 | sendToHost: (channel, ...args) => ipcRenderer.sendToHost(channel, args), 34 | // event emitter methods 35 | setMaxListeners: (n) => { 36 | ipcRenderer.setMaxListeners(n) 37 | return _ipcRenderer 38 | }, 39 | getMaxListeners: () => ipcRenderer.getMaxListeners(), 40 | listeners: (e) => ipcRenderer.listeners(e), 41 | rawListeners: (e) => ipcRenderer.rawListeners(e), 42 | emit: (e, ...args) => ipcRenderer.emit(e, ...args), 43 | listenerCount: (e) => ipcRenderer.listenerCount(e), 44 | addListener: (e, l) => { 45 | ipcRenderer.addListener(e, l) 46 | return _ipcRenderer 47 | }, 48 | off: (e, l) => { 49 | ipcRenderer.off(e, l) 50 | return _ipcRenderer 51 | }, 52 | 53 | prependListener: (e, l) => { 54 | ipcRenderer.prependListener(e, l) 55 | return _ipcRenderer 56 | }, 57 | prependOnceListener: (e, l) => { 58 | ipcRenderer.prependOnceListener(e, l) 59 | return _ipcRenderer 60 | }, 61 | eventNames: () => ipcRenderer.eventNames() 62 | } 63 | 64 | const api = { 65 | shell, 66 | clipboard, 67 | ipcRenderer: _ipcRenderer, 68 | dialog: { 69 | showCertificateTrustDialog(...options: any[]) { 70 | return ipcRenderer.invoke('dialog:showCertificateTrustDialog', ...options) 71 | }, 72 | showErrorBox(...options: any[]) { 73 | return ipcRenderer.invoke('dialog:showErrorBox', ...options) 74 | }, 75 | showMessageBox(...options: any[]) { 76 | return ipcRenderer.invoke('dialog:showMessageBox', ...options) 77 | }, 78 | showOpenDialog(...options: any[]) { 79 | return ipcRenderer.invoke('dialog:showOpenDialog', ...options) 80 | }, 81 | showSaveDialog(...options: any[]) { 82 | return ipcRenderer.invoke('dialog:showSaveDialog', ...options) 83 | } 84 | } as Pick 85 | } 86 | 87 | try { 88 | contextBridge.exposeInMainWorld('electron', api) 89 | } catch { 90 | (window as any).electron = api 91 | } 92 | -------------------------------------------------------------------------------- /electron-vue-next/src/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "downlevelIteration": true, 6 | "resolveJsonModule": true, 7 | "lib": ["ESNext", "DOM"], 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "baseUrl": ".", 11 | "paths": { 12 | "/@shared/*": [ 13 | "../shared/*" 14 | ], 15 | "/@main/*": [ 16 | "./*" 17 | ] 18 | }, 19 | "esModuleInterop": true, 20 | "experimentalDecorators": true, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true 23 | }, 24 | "include": [ 25 | "../shared/**/*", 26 | "./**/*" 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 21 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ci010/electron-vue-next/4f8772e38a99a4bc5268e06d6766a0e5a2f2edb7/electron-vue-next/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/components/About.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/components/Home.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 74 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/components/HomeNavigator.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/components/SumEquation.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 31 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/composables/count.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useStore } from 'vuex' 3 | 4 | export function useCount() { 5 | const { state, commit } = useStore() 6 | const count = computed(() => state.foo.count as number) 7 | const increment = () => commit('increment') 8 | return { 9 | count, 10 | increment 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/composables/electron.ts: -------------------------------------------------------------------------------- 1 | import type Electron from 'electron' 2 | 3 | const { shell, clipboard, ipcRenderer, dialog } = (window as any).electron as typeof Electron 4 | 5 | export function useShell() { 6 | return shell 7 | } 8 | 9 | export function useClipboard() { 10 | return clipboard 11 | } 12 | 13 | export function useIpc() { 14 | return ipcRenderer 15 | } 16 | 17 | export function useDialog() { 18 | return dialog 19 | } 20 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sample' 2 | export * from './count' 3 | export * from './electron' 4 | export * from './service' 5 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/composables/sample.ts: -------------------------------------------------------------------------------- 1 | import { computed, Ref } from 'vue' 2 | 3 | export function useSum(a: Ref, b: Ref) { 4 | return computed(() => a.value + b.value) 5 | } 6 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/composables/service.ts: -------------------------------------------------------------------------------- 1 | import { useIpc } from './electron' 2 | import type { Services } from '/@main/services' 3 | import { toRaw } from 'vue' 4 | 5 | const { invoke } = useIpc() 6 | 7 | function createProxy(service: string) { 8 | return new Proxy({} as any, { 9 | get(_, functionName) { 10 | return (...payloads: any[]) => { 11 | const rawPayloads = payloads.map(e => toRaw(e)); 12 | return invoke('service:call', service, functionName as string, ...rawPayloads); 13 | } 14 | } 15 | }) 16 | } 17 | const servicesProxy: Services = new Proxy({} as any, { 18 | get(_, serviceName) { return createProxy(serviceName as string) } 19 | }) 20 | 21 | export function useService(name: T): Services[T] { 22 | return servicesProxy[name] 23 | } 24 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | #app { 7 | font-family: Avenir, Helvetica, Arial, sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | text-align: center; 11 | color: #2c3e50; 12 | padding: 0; 13 | } 14 | 15 | .rounded { 16 | border: 1px solid grey; 17 | border-radius: 5px; 18 | padding: 5px; 19 | user-select: none; 20 | cursor: pointer; 21 | } 22 | 23 | .rounded:hover { 24 | background: rgba(0, 0, 0, 0.05); 25 | } 26 | 27 | .container { 28 | display: flex; 29 | padding: 10px; 30 | flex-wrap: wrap; 31 | } 32 | 33 | .flex { 34 | display: flex; 35 | flex-wrap: wrap; 36 | } 37 | 38 | .card { 39 | margin: 10px; 40 | padding: 10px; 41 | flex-grow: 1; 42 | border-radius: 4px; 43 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)!important; 44 | transition-duration: 0.25s; 45 | user-select: none; 46 | } 47 | 48 | .card.clickable:hover { 49 | cursor: pointer; 50 | box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12)!important; 51 | } 52 | .card.clickable:active { 53 | background-color: rgba(0, 0, 0, 0.1); 54 | } -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Main App 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './index.css' 4 | import router from './router' 5 | import store from './store' 6 | 7 | const app = createApp(App) 8 | 9 | app.use(router) 10 | app.use(store) 11 | 12 | app.mount('#app') 13 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/renderer.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | declare module '*.vue' { 4 | import { Component } from 'vue' 5 | const component: Component 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createMemoryHistory } from 'vue-router' 2 | import Home from '/@/components/Home.vue' 3 | import About from '/@/components/About.vue' 4 | 5 | const router = createRouter({ 6 | history: createMemoryHistory(), 7 | routes: [ 8 | { 9 | path: '/', 10 | component: Home 11 | }, 12 | { 13 | path: '/about', 14 | component: About 15 | } 16 | ] 17 | }) 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/side.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Side Window 7 | 8 | 9 |
10 | Second Window!!! 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { createStore, StoreOptions } from 'vuex' 3 | import bar from './modules/bar' 4 | import foo from './modules/foo' 5 | 6 | export interface RootState { 7 | } 8 | 9 | const store: StoreOptions = { 10 | state: {}, 11 | getters: {}, 12 | mutations: {}, 13 | modules: { 14 | foo, 15 | bar 16 | } 17 | } 18 | 19 | export default createStore(store) 20 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/store/modules/bar.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import { RootState } from '..' 3 | 4 | export interface State { 5 | value: number; 6 | name: string; 7 | } 8 | 9 | const mod: Module = { 10 | state: { 11 | value: 0, 12 | name: '' 13 | }, 14 | getters: { 15 | valueAndName: state => state.name + ' ' + state.value 16 | }, 17 | mutations: { 18 | setValueAndName: (state, { name, value }) => { state.name = name; state.value = value } 19 | } 20 | } 21 | 22 | export default mod 23 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/store/modules/foo.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex" 2 | import { RootState } from ".." 3 | 4 | export interface State { 5 | count: number 6 | } 7 | 8 | const mod: Module = { 9 | state: { 10 | count: 0 11 | }, 12 | getters: { 13 | }, 14 | mutations: { 15 | increment: state => state.count++, 16 | decrement: state => state.count-- 17 | } 18 | } 19 | 20 | export default mod 21 | -------------------------------------------------------------------------------- /electron-vue-next/src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "downlevelIteration": true, 6 | "resolveJsonModule": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "baseUrl": ".", 10 | "paths": { 11 | "/@shared/*": [ 12 | "../shared/*" 13 | ], 14 | "/@/*": [ 15 | "./*" 16 | ], 17 | "/@main/*": [ 18 | "../main/*" 19 | ], 20 | }, 21 | "esModuleInterop": true, 22 | "experimentalDecorators": true, 23 | "skipLibCheck": true, 24 | "forceConsistentCasingInFileNames": true 25 | }, 26 | "include": [ 27 | "./**/*", 28 | "./renderer.d.ts", 29 | "../shared/**/*" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } -------------------------------------------------------------------------------- /electron-vue-next/src/shared/shared.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * @see https://github.com/vitejs/vite/blob/03acecd797d8393e38c8a78f920c8e0927762018/importMeta.d.ts 5 | */ 6 | declare interface ImportMetaEnv { 7 | [key: string]: string | boolean | undefined 8 | BASE_URL: string 9 | MODE: string 10 | DEV: boolean 11 | PROD: boolean 12 | } 13 | 14 | /** 15 | * @see https://github.com/vitejs/vite/blob/03acecd797d8393e38c8a78f920c8e0927762018/importMeta.d.ts 16 | */ 17 | declare interface ImportMeta { 18 | readonly hot?: { 19 | readonly data: any 20 | 21 | accept(): void 22 | accept(cb: (mod: any) => void): void 23 | 24 | acceptDeps(dep: string, cb: (mod: any) => void): void 25 | acceptDeps(deps: readonly string[], cb: (mods: any[]) => void): void 26 | 27 | dispose(cb: (data: any) => void): void 28 | decline(): void 29 | invalidate(): void 30 | 31 | on(event: string, cb: (...args: any[]) => void): void 32 | } 33 | 34 | readonly env: ImportMetaEnv 35 | } 36 | -------------------------------------------------------------------------------- /electron-vue-next/src/shared/sharedLib.ts: -------------------------------------------------------------------------------- 1 | export function add(a: number, b: number) { 2 | return a + b 3 | } 4 | -------------------------------------------------------------------------------- /electron-vue-next/src/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "downlevelIteration": true, 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "baseUrl": ".", 11 | "esModuleInterop": true, 12 | "experimentalDecorators": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true 15 | }, 16 | "include": [ 17 | "**/*", 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /electron-vue-next/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ci010/electron-vue-next/4f8772e38a99a4bc5268e06d6766a0e5a2f2edb7/electron-vue-next/static/logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const inquirer = require('inquirer') 4 | const commander = require('commander') 5 | const chalk = require('chalk') 6 | const { version, name } = require('./package.json') 7 | const { join, relative, resolve } = require('path') 8 | const { copy, readFile, writeFile, readJSON, writeJSON } = require('fs-extra') 9 | 10 | const program = new commander.Command(name) 11 | 12 | const options = { 13 | outputDirectory: join(process.cwd(), 'electron-vue-next'), 14 | projectName: 'electron-vue-next', 15 | nodeIntegration: false, 16 | threadWorker: true, 17 | mainService: true, 18 | vscode: true 19 | } 20 | 21 | const provided = { 22 | nodeIntegration: false, 23 | name: false, 24 | service: false, 25 | vscode: false 26 | } 27 | 28 | program 29 | .storeOptionsAsProperties(false) 30 | .version(version) 31 | .option('-ni, --no-interactive', 'disable interactive interface') 32 | .option('-n, --name ', 'name of the project') 33 | .option('-en, --enable-node-integration', 'enable node integration') 34 | .option('-ns, --no-service', 'do not generate Service infra') 35 | .option('-ns, --no-thread-worker', 'do not generate thread worker example') 36 | .option('-nc, --no-vscode', 'do not generate VSCode debug config') 37 | .on('option:enable-node-integration', () => { provided.nodeIntegration = true }) 38 | .on('option:name', () => { provided.name = true }) 39 | .on('option:no-service', () => { provided.service = true }) 40 | .on('option:no-vscode', () => { provided.vscode = true }) 41 | .description('Setup vue-electron-next-app to a directory') 42 | .action(() => { 43 | const dir = program.args[0] 44 | const opts = program.opts() 45 | options.outputDirectory = dir 46 | options.name = opts.name || dir 47 | options.nodeIntegration = opts.enableNodeIntegration 48 | options.mainService = opts.service 49 | options.vscode = opts.vscode 50 | if (!options.noInteractive) { 51 | console.log(` 52 | Answer questions in prompt to config the project generator: 53 | 54 | ${chalk.italic.magenta('If you have question, please refer the document https://ci010.github.io/electron-vue-next/')} 55 | `) 56 | interactive(dir).then(setupProject) 57 | } else { 58 | setupProject() 59 | } 60 | }) 61 | .parse(process.argv) 62 | 63 | async function interactive(name) { 64 | const { projectName, nodeIntegration, mainService, vscode, threadWorker } = await inquirer.prompt([ 65 | { type: 'input', default: name || 'electron-vue-app', message: 'Name of the project:', name: 'projectName', when: !provided.name }, 66 | { type: 'confirm', default: false, message: 'Enable node integration for renderer:', name: 'nodeIntegration', when: !provided.nodeIntegration }, 67 | { 68 | type: 'confirm', 69 | default: true, 70 | message: 'Use Service infrastructure to handle main/renderer communication:', 71 | name: 'mainService', 72 | when: !provided.service 73 | }, 74 | { 75 | type: 'confirm', 76 | default: false, 77 | message: 'Include thread_worker example', 78 | name: 'threadWorker' 79 | }, 80 | { type: 'confirm', default: true, message: 'Generate vscode debug config:', name: 'vscode', when: !provided.vscode } 81 | ]) 82 | options.threadWorker = threadWorker 83 | options.projectName = projectName 84 | options.nodeIntegration = nodeIntegration 85 | options.mainService = mainService 86 | options.vscode = vscode 87 | options.outputDirectory = options.outputDirectory || projectName 88 | } 89 | 90 | async function setupProject() { 91 | const srcDir = join(__dirname, 'electron-vue-next') 92 | const distDir = resolve(options.outputDirectory || 'electron-vue-next') 93 | 94 | if (srcDir === distDir) { 95 | throw new Error('The generated directory cannot be the same as the source directory in node_modules!') 96 | } 97 | 98 | await copy(srcDir, distDir, { 99 | overwrite: true, 100 | filter: (src, dest) => { 101 | const relativePath = relative(srcDir, src) 102 | if (!options.mainService) { 103 | if (relativePath.startsWith(join('src', 'main', 'services'))) { 104 | return false 105 | } 106 | if (relativePath.startsWith(join('src', 'main', 'logger.ts'))) { 107 | return false 108 | } 109 | if (relativePath.startsWith(join('src', 'renderer', 'components', 'About.vue'))) { 110 | return false 111 | } 112 | if (relativePath.startsWith(join('src', 'renderer', 'composables', 'service.ts'))) { 113 | return false 114 | } 115 | } 116 | if (!options.threadWorker) { 117 | if (relativePath.startsWith(join('src', 'main', 'workers'))) { 118 | return false 119 | } 120 | } 121 | if (!options.vscode) { 122 | if (relativePath.startsWith('.vscode')) { 123 | return false 124 | } 125 | } 126 | return true 127 | } 128 | }) 129 | const packageJSON = await readJSON(join(distDir, 'package.json')) 130 | packageJSON.name = options.projectName 131 | if (!options.mainService) { 132 | { 133 | const indexPath = join(distDir, 'src/main/index.ts') 134 | const lines = (await readFile(indexPath)).toString().split('\n') 135 | const filteredLine = new Set([ 136 | 'import { Logger } from \'./logger\'', 137 | 'import { initialize } from \'./services\'', 138 | 'const logger = new Logger()', 139 | 'logger.initialize(app.getPath(\'userData\'))', 140 | 'initialize(logger)' 141 | ]) 142 | const result = lines.filter((line) => !filteredLine.has(line.trim())).join('\n') 143 | await writeFile(indexPath, result) 144 | } 145 | { 146 | const routerPath = join(distDir, 'src/renderer/router.ts') 147 | const routerLines = (await readFile(routerPath)).toString().split('\n') 148 | routerLines.splice(11, 4) 149 | routerLines.splice(2, 1) 150 | await writeFile(routerPath, routerLines.join('\n')) 151 | } 152 | { 153 | const path = join(distDir, 'src/renderer/composables/index.ts') 154 | const lines = (await readFile(path)).toString().split('\n') 155 | lines.splice(11, 4) 156 | await writeFile(path, lines.join('\n')) 157 | } 158 | { 159 | const path = join(distDir, 'src/renderer/components/HomeNavigator.vue') 160 | const lines = (await readFile(path)).toString().split('\n') 161 | lines.splice(3, 1) 162 | await writeFile(path, lines.join('\n')) 163 | } 164 | } 165 | if (options.nodeIntegration) { 166 | const indexPath = join(distDir, 'src/main/index.ts') 167 | const lines = (await readFile(indexPath)).toString().split('\n') 168 | const nodeIntegrationLine = lines.indexOf(' nodeIntegration: false') 169 | lines[nodeIntegrationLine] = ' nodeIntegration: true' 170 | await writeFile(indexPath, lines.join('\n')) 171 | } 172 | if (!options.threadWorker) { 173 | const indexPath = join(distDir, 'src/main/index.ts') 174 | const lines = (await readFile(indexPath)).toString().split('\n') 175 | const filteredLine = new Set([ 176 | 'import createBaseWorker from \'./workers/index?worker\'', 177 | '// thread_worker example', 178 | 'createBaseWorker({ workerData: \'worker world\' }).on(\'message\', (message) => {', 179 | // eslint-disable-next-line no-template-curly-in-string 180 | 'logger.log(`Message from worker: ${message}`)', 181 | '}).postMessage(\'\')' 182 | ]) 183 | const result = lines 184 | .filter(l => !filteredLine.has(l.trim())) 185 | .join('\n') 186 | await writeFile(indexPath, result) 187 | } 188 | await writeJSON(join(distDir, 'package.json'), packageJSON, { spaces: 4 }) 189 | 190 | console.log() 191 | console.log(`Project Generated at: ${resolve(options.outputDirectory)}\n`) 192 | console.log(`Next, you should process following commands: 193 | 194 | ${chalk.cyan('cd')} ${options.outputDirectory} 195 | ${chalk.cyan('npm')} install 196 | Install dependencies of the project 197 | 198 | ${chalk.cyan('npm')} run dev 199 | Start the development environment`) 200 | } 201 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-electron-vue-next", 3 | "version": "0.0.16", 4 | "description": "Create electron-vue-next app scaffold from cli", 5 | "bin": { 6 | "create-electron-vue-next": "index.js" 7 | }, 8 | "scripts": { 9 | "dev:docs": "cross-env NODE_ENV=development node scripts/dev.docs.js", 10 | "dev": "npm run dev --prefix electron-vue-next", 11 | "build": "npm run build --prefix electron-vue-next", 12 | "build:docs": "vitepress build docs", 13 | "build:dir": "cross-env ELECTRON_VERSION=13.1.2 npm run build:dir --prefix electron-vue-next", 14 | "build:lite": "cross-env ELECTRON_VERSION=13.1.2 npm run build:lite --prefix electron-vue-next", 15 | "build:production": "cross-env ELECTRON_VERSION=13.1.2 npm run build:production --prefix electron-vue-next", 16 | "lint": "eslint --ext .ts,.vue,.js --config electron-vue-next/.eslintrc.js electron-vue-next/src electron-vue-next/scripts index.js scripts", 17 | "lint:fix": "npm run lint -- --fix" 18 | }, 19 | "author": { 20 | "email": "cijhn@hotmail.com", 21 | "name": "ci010" 22 | }, 23 | "license": "MIT", 24 | "workspaces": [ 25 | "electron-vue-next" 26 | ], 27 | "dependencies": { 28 | "commander": "^6.2.0", 29 | "fs-extra": "^9.0.1", 30 | "inquirer": "^7.3.3" 31 | }, 32 | "devDependencies": { 33 | "vitepress": "^0.15.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/dev.docs.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const { createServer } = require('vitepress') 3 | 4 | const port = 3000 5 | 6 | async function startVitepress() { 7 | try { 8 | const server = await createServer('docs', { 9 | // optimizeDeps: { 10 | // exclude: [...external, ...Object.keys(dependencies), ...builtins] 11 | // } 12 | }) 13 | server.listen(port, () => { 14 | console.log(`${chalk.green('[vitepress]')} listening at http://localhost:${port}`) 15 | }) 16 | } catch (err) { 17 | console.error(chalk.red('failed to start server. error:\n'), err) 18 | } 19 | } 20 | 21 | startVitepress() 22 | -------------------------------------------------------------------------------- /scripts/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "strict": true, 5 | "noImplicitAny": false, 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } --------------------------------------------------------------------------------