├── config └── app-config.json ├── .browserslistrc ├── babel.config.js ├── postcss.config.js ├── src.preload ├── preload.js ├── preload-for-user-contents.js └── preload-isolated.js ├── index.js ├── src ├── shims-vue.d.ts ├── assets │ └── logo.png ├── views │ ├── About.vue │ └── Home.vue ├── windows │ ├── MainWindow │ │ ├── main.ts │ │ ├── App.vue │ │ └── router.ts │ └── SubWindow │ │ ├── main.ts │ │ ├── App.vue │ │ └── router.ts ├── shims-tsx.d.ts └── components │ └── HelloWorld.vue ├── public ├── favicon.ico ├── sub-window.html └── main-window.html ├── electron-builder.yml ├── src.mainproc ├── ipc │ ├── app.ts │ └── views.Home.ts ├── lib │ ├── conf.ts │ └── SimpleWindowManager.ts ├── main.ts └── windows │ ├── UserContentWindow.ts │ ├── SubWindow.ts │ └── MainWindow.ts ├── tslint.json ├── tests └── unit │ └── example.spec.ts ├── .vscode └── launch.json ├── vue.config.js ├── .gitignore ├── .npmignore ├── tsconfig.json ├── tsconfig.mainproc.json ├── webpack.mainproc.config.js ├── README.md └── package.json /config/app-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src.preload/preload.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | window.ipcRenderer = require('electron').ipcRenderer; 4 | 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This file is electron main process bootstrap. 2 | // 3 | 4 | require('./bin/main'); 5 | 6 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellyln/vue-electron-typescript-quickstart/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellyln/vue-electron-typescript-quickstart/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src.preload/preload-for-user-contents.js: -------------------------------------------------------------------------------- 1 | 2 | // nothing to do. 3 | 4 | // console.log("preload-for-user-contents:" + __filename); 5 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | directories: 2 | output: dist.electron 3 | buildResources: build 4 | appId: your.id 5 | mac: 6 | category: your.app.category.type 7 | asarUnpack: config/** 8 | -------------------------------------------------------------------------------- /src.mainproc/ipc/app.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ipcMain } from 'electron'; 3 | 4 | 5 | 6 | // ipcMain.on('app:awesome-sub-module:message-foo', (event: any, arg: any) => { 7 | // // 8 | // }); 9 | -------------------------------------------------------------------------------- /src/windows/MainWindow/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | router, 9 | render: (h) => h(App), 10 | }).$mount('#app'); 11 | -------------------------------------------------------------------------------- /src/windows/SubWindow/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | router, 9 | render: (h) => h(App), 10 | }).$mount('#app'); 11 | -------------------------------------------------------------------------------- /src.mainproc/lib/conf.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { app } from 'electron'; 5 | 6 | 7 | 8 | export const appConfig = JSON.parse( 9 | fs.readFileSync(path.join(app.getAppPath(), 'config/app-config.json')).toString()); 10 | -------------------------------------------------------------------------------- /src.preload/preload-isolated.js: -------------------------------------------------------------------------------- 1 | 2 | { 3 | const { contextBridge, ipcRenderer } = require('electron'); 4 | 5 | contextBridge.exposeInMainWorld( 6 | 'myAppApi', { 7 | ping: async (value) => { 8 | return ipcRenderer.invoke('views:Home:message-foo', value); 9 | }, 10 | } 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode { } 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue { } 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "quotemark": [true, "single"], 13 | "indent": [true, "spaces", 4], 14 | "interface-name": false, 15 | "ordered-imports": false, 16 | "object-literal-sort-keys": false, 17 | "no-consecutive-blank-lines": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import HelloWorld from '@/components/HelloWorld.vue'; 4 | 5 | describe('HelloWorld.vue', () => { 6 | it('renders props.msg when passed', () => { 7 | const msg = 'new message'; 8 | const wrapper = shallowMount(HelloWorld, { 9 | propsData: { msg }, 10 | }); 11 | expect(wrapper.text()).to.include(msg); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /public/sub-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sub window 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/main-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-hello-world 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src.mainproc/lib/SimpleWindowManager.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BrowserWindow } from 'electron'; 3 | 4 | 5 | 6 | const windowMap = new Map(); 7 | 8 | 9 | export function registerWindow(key: symbol | BrowserWindow, wnd: BrowserWindow) { 10 | windowMap.set(key, wnd); 11 | } 12 | 13 | 14 | export function unregisterWindow(key: symbol | BrowserWindow) { 15 | return windowMap.delete(key); 16 | } 17 | 18 | 19 | export function isRegistered(key: symbol | BrowserWindow) { 20 | return windowMap.has(key); 21 | } 22 | 23 | 24 | export function getWindow(key: symbol | BrowserWindow) { 25 | return windowMap.get(key); 26 | } 27 | -------------------------------------------------------------------------------- /src.mainproc/ipc/views.Home.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ipcMain } from 'electron'; 3 | import { createSubWindow } from '../windows/SubWindow'; 4 | import { createUserContentWindow } from '../windows/UserContentWindow'; 5 | 6 | 7 | 8 | ipcMain.handle('views:Home:message-foo', (event: any, message: string) => { 9 | // tslint:disable-next-line:no-console 10 | console.log(message); 11 | 12 | switch (message) { 13 | case 'ping': 14 | createSubWindow(); 15 | // TODO: BUG: MainWindow and SubWindow's devtools can't open by CSP if UserContentWindow is created. 16 | createUserContentWindow(); 17 | break; 18 | } 19 | return 'pong'; 20 | }); 21 | -------------------------------------------------------------------------------- /.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": "Debug Main Process", 9 | "type": "node", 10 | "request": "launch", 11 | "cwd": "${workspaceRoot}", 12 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 13 | "windows": { 14 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 15 | }, 16 | "args" : ["."] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /src/windows/MainWindow/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/windows/SubWindow/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/windows/MainWindow/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from '../../views/Home.vue'; 4 | 5 | Vue.use(Router); 6 | 7 | const router = new Router({ 8 | mode: 'history', 9 | base: process.env.BASE_URL, 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | component: Home, 15 | }, 16 | { 17 | path: '*', 18 | redirect: '/', 19 | }, 20 | { 21 | path: '/about', 22 | name: 'about', 23 | // route level code-splitting 24 | // this generates a separate chunk (about.[hash].js) for this route 25 | // which is lazy-loaded when the route is visited. 26 | component: () => import(/* webpackChunkName: "about" */ '../../views/About.vue'), 27 | }, 28 | ], 29 | }); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /src/windows/SubWindow/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from '../../views/Home.vue'; 4 | 5 | Vue.use(Router); 6 | 7 | const router = new Router({ 8 | mode: 'history', 9 | base: process.env.BASE_URL, 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | component: Home, 15 | }, 16 | { 17 | path: '*', 18 | redirect: '/about', 19 | }, 20 | { 21 | path: '/about', 22 | name: 'about', 23 | // route level code-splitting 24 | // this generates a separate chunk (about.[hash].js) for this route 25 | // which is lazy-loaded when the route is visited. 26 | component: () => import(/* webpackChunkName: "about" */ '../../views/About.vue'), 27 | }, 28 | ], 29 | }); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 38 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pages: { 3 | mainwindow: { 4 | // entry for the page 5 | entry: 'src/windows/MainWindow/main.ts', 6 | // the source template 7 | template: 'public/main-window.html', 8 | // output as dist/index.html 9 | filename: 'main-window.html', 10 | // when using title option, 11 | // template title tag needs to be <%= htmlWebpackPlugin.options.title %> 12 | //title: 'Index Page', 13 | // chunks to include on this page, by default includes 14 | // extracted common chunks and vendor chunks. 15 | //chunks: ['chunk-vendors', 'chunk-common', 'index'] 16 | }, 17 | // when using the entry-only string format, 18 | // template is inferred to be `public/subpage.html` 19 | // and falls back to `public/index.html` if not found. 20 | // Output filename is inferred to be `subpage.html`. 21 | subwindow: { 22 | entry: 'src/windows/SubWindow/main.ts', 23 | template: 'public/sub-window.html', 24 | filename: 'sub-window.html', 25 | }, 26 | } 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # local env files 4 | .env.local 5 | .env.*.local 6 | 7 | # Log files 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Visual Studio files 41 | /.vs/ 42 | 43 | # VS Code cache files 44 | /.vscode/.browse.VC* 45 | 46 | # Dependency directory 47 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 48 | /node_modules/ 49 | 50 | /bin/ 51 | /dist/ 52 | /dist.electron/ 53 | 54 | # Editor directories and files 55 | .idea 56 | # .vscode 57 | *.suo 58 | *.ntvs* 59 | *.njsproj 60 | *.sln 61 | *.sw* 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # local env files 4 | .env.local 5 | .env.*.local 6 | 7 | # Log files 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Visual Studio files 41 | /.vs/ 42 | 43 | # VS Code cache files 44 | /.vscode/.browse.VC* 45 | 46 | # Dependency directory 47 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 48 | /node_modules/ 49 | 50 | /bin/ 51 | /dist/ 52 | /dist.electron/ 53 | 54 | # Editor directories and files 55 | .idea 56 | # .vscode 57 | *.suo 58 | *.ntvs* 59 | *.njsproj 60 | *.sln 61 | *.sw* 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | 14 | "noEmitOnError": true, 15 | "noImplicitAny": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "strictNullChecks": true, 20 | // "noUnusedParameters": true, 21 | // "noUnusedLocals": true, 22 | // "noFallthroughCasesInSwitch": true, 23 | 24 | "baseUrl": ".", 25 | "types": [ 26 | "webpack-env", 27 | "mocha", 28 | "chai" 29 | ], 30 | "paths": { 31 | "@/*": [ 32 | "src/*" 33 | ] 34 | }, 35 | "lib": [ 36 | "esnext", 37 | "dom", 38 | "dom.iterable", 39 | "scripthost" 40 | ] 41 | }, 42 | "include": [ 43 | "src/**/*.ts", 44 | "src/**/*.tsx", 45 | "src/**/*.vue", 46 | "tests/**/*.ts", 47 | "tests/**/*.tsx" 48 | ], 49 | "exclude": [ 50 | "node_modules" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.mainproc.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "react", 7 | "jsxFactory": "FooBar.dom", 8 | "importHelpers": true, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | 16 | "noEmitOnError": true, 17 | "noImplicitAny": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "strictNullChecks": true, 22 | // "noUnusedParameters": true, 23 | // "noUnusedLocals": true, 24 | // "noFallthroughCasesInSwitch": true, 25 | 26 | "baseUrl": ".", 27 | "types": ["node", "electron"], 28 | "paths": { 29 | }, 30 | "lib": [ 31 | "esnext", 32 | "scripthost" 33 | ], 34 | // "declaration": true, 35 | // "declarationDir": "./declarations", 36 | // "traceResolution": true, 37 | }, 38 | "include": [ 39 | "src.mainproc/**/*" 40 | ], 41 | "exclude": [ 42 | "bin", 43 | "node_modules", 44 | "spec", 45 | "src", 46 | "public", 47 | "tests" 48 | ] 49 | } -------------------------------------------------------------------------------- /webpack.mainproc.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | 5 | 6 | module.exports = function (env) { 7 | return [{ 8 | target: "electron-main", 9 | entry: { 10 | spec: [ 11 | path.resolve(__dirname, 'src.mainproc/main.ts') 12 | ] 13 | }, 14 | output: { 15 | library: 'main', 16 | 17 | libraryTarget: 'commonjs2', 18 | filename: 'main.js', 19 | path: path.resolve(__dirname, 'bin'), 20 | devtoolModuleFilenameTemplate: '../[resource-path]', 21 | // devtoolModuleFilenameTemplate: void 0 22 | }, 23 | node: { 24 | __filename: false, 25 | __dirname: false, 26 | }, 27 | module: { 28 | rules: [{ 29 | test: /\.tsx?$/, 30 | use: [ 31 | 'babel-loader', 32 | 'ts-loader?' + JSON.stringify({ 33 | configFile: 'tsconfig.mainproc.json' 34 | }), 35 | ], 36 | exclude: /node_modules[\/\\].*$/ 37 | }, { 38 | test: /\.jsx?$/, 39 | use: ['babel-loader'], 40 | exclude: /node_modules[\/\\].*$/ 41 | }, { 42 | enforce: 'pre', 43 | test: /\.[tj]sx?$/, 44 | use: { 45 | loader: 'source-map-loader', 46 | options: { 47 | } 48 | }, 49 | exclude: /node_modules[\/\\].*$/ 50 | }] 51 | }, 52 | plugins: [], 53 | resolve: { 54 | extensions: ['.tsx', '.ts', '.jsx', '.js'] 55 | }, 56 | devtool: 'source-map' 57 | }, 58 | 59 | ]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-electron-typescript-quickstart 2 | 3 | A boilerplate of Electron app that uses Vue in TypeScript. 4 | This is based on the output of Vue CLI 3 and is not ejecting from the CLI. 5 | 6 | 7 | ## Usage 8 | ``` 9 | git clone https://github.com/shellyln/vue-electron-typescript-quickstart.git 10 | cd vue-electron-typescript-quickstart 11 | 12 | vi package.json 13 | # and edit package name, author, ... 14 | 15 | rm package-lock.json 16 | npm install 17 | 18 | rm -rf .git/ 19 | git init 20 | git add . 21 | git commit -m "initial commit." 22 | 23 | npm run build 24 | npm run build:mainproc 25 | npm start 26 | ``` 27 | 28 | --- 29 | 30 | ## Project setup 31 | ``` 32 | npm install 33 | ``` 34 | 35 | ### ~~Compiles and hot-reloads for development~~ 36 | ``` 37 | npm run serve 38 | ``` 39 | 40 | ### Compiles and minifies for production (electron renderer process) 41 | ``` 42 | npm run build 43 | ``` 44 | 45 | ### Compiles and minifies for production (electron main process) 46 | ``` 47 | npm run build:mainproc 48 | ``` 49 | 50 | ### Clean project 51 | ``` 52 | npm run clean 53 | ``` 54 | 55 | ### Start electron app for debug 56 | ``` 57 | npm run start 58 | ``` 59 | 60 | ### Build electron distribution executable files (unpacked) 61 | ``` 62 | npm run pack 63 | ``` 64 | 65 | ### Build electron distribution executable files (packing to the installer) 66 | ``` 67 | npm run dist 68 | ``` 69 | 70 | ### Run your tests 71 | ``` 72 | npm run test 73 | ``` 74 | 75 | ### Lints and fixes files 76 | ``` 77 | npm run lint 78 | ``` 79 | 80 | ### Run your unit tests (renderer process) 81 | ``` 82 | npm run test:unit 83 | ``` 84 | 85 | ### Customize configuration 86 | See [Configuration Reference](https://cli.vuejs.org/config/). 87 | 88 | 89 | --- 90 | 91 | 92 | ## **Electron Documentation (security)** 93 | See [this](https://electronjs.org/docs/tutorial/security) guide. 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-electron-typescript-quickstart", 3 | "version": "0.1.4", 4 | "private": false, 5 | "description": "A boilerplate of Electron app that uses Vue in TypeScript.", 6 | "keywords": [ 7 | "Vue.js", 8 | "Electron", 9 | "TypeScript", 10 | "JavaScript", 11 | "boilerplate", 12 | "quickstart", 13 | "template" 14 | ], 15 | "scripts": { 16 | "start": "electron .", 17 | "pack": "electron-builder --dir -c electron-builder.yml", 18 | "dist": "electron-builder -c electron-builder.yml", 19 | "serve": "vue-cli-service serve", 20 | "build": "vue-cli-service build", 21 | "build:dev": "vue-cli-service build --mode development", 22 | "build:mainproc": "webpack-cli --mode=development --config webpack.mainproc.config.js", 23 | "clean": "rm -rf ./dist && rm -rf ./bin && rm -rf ./dist.electron", 24 | "lint": "vue-cli-service lint", 25 | "test": "npm run test:unit", 26 | "test:unit": "vue-cli-service test:unit" 27 | }, 28 | "dependencies": { 29 | "vue": "^2.6.12", 30 | "vue-class-component": "^7.2.6", 31 | "vue-property-decorator": "^9.1.2", 32 | "vue-router": "^3.4.9" 33 | }, 34 | "devDependencies": { 35 | "@types/chai": "^4.2.14", 36 | "@types/electron": "^1.6.10", 37 | "@types/mocha": "^8.2.0", 38 | "@vue/cli-plugin-babel": "^4.5.9", 39 | "@vue/cli-plugin-typescript": "^4.5.9", 40 | "@vue/cli-plugin-unit-mocha": "^4.5.9", 41 | "@vue/cli-service": "^4.5.9", 42 | "@vue/test-utils": "^1.1.2", 43 | "chai": "^4.2.0", 44 | "electron": "^11.1.1", 45 | "electron-builder": "^22.9.1", 46 | "node-sass": "^5.0.0", 47 | "sass-loader": "^10.1.0", 48 | "source-map-loader": "^1.1.3", 49 | "typescript": "^4.1.3", 50 | "vue-template-compiler": "^2.6.12", 51 | "webpack": "4.44.2", 52 | "webpack-cli": "^4.3.0" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "https://github.com/shellyln/vue-electron-typescript-quickstart.git" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src.mainproc/main.ts: -------------------------------------------------------------------------------- 1 | 2 | // Modules to control application life and create native browser window 3 | import * as url from 'url'; 4 | import * as path from 'path'; 5 | import { app, protocol, Menu } from 'electron'; 6 | 7 | 8 | // Window 9 | 10 | import { appConfig } from './lib/conf'; 11 | import { createMainWindow } from './windows/MainWindow'; 12 | 13 | 14 | // IPC events 15 | 16 | import './ipc/app'; 17 | import './ipc/views.Home'; 18 | 19 | 20 | // Read the application config. 21 | // tslint:disable-next-line:no-console 22 | console.log('app config: ' + JSON.stringify(appConfig, null, 2)); 23 | 24 | 25 | if (app.isPackaged) { 26 | // Removing the menu bar from the window. 27 | Menu.setApplicationMenu(null); 28 | } 29 | 30 | 31 | // App lifecycle events. 32 | 33 | // This method will be called when Electron has finished 34 | // initialization and is ready to create browser windows. 35 | // Some APIs can only be used after this event occurs. 36 | // tslint:disable-next-line:only-arrow-functions 37 | app.on('ready', function() { 38 | protocol.interceptFileProtocol('file', (req, callback) => { 39 | let filePath = new url.URL(req.url).pathname; 40 | if (process.platform === 'win32') { 41 | if (filePath.match(/^\/[A-Za-z]:/)) { 42 | filePath = filePath.slice(1); 43 | } 44 | if (filePath.match(/^[A-Za-z]:\/(css|img|js)/)) { 45 | filePath = path.join(app.getAppPath(), 'dist', filePath.slice(3)); 46 | } else if (filePath.match(/^[A-Za-z]:\/[^/\\]+?\.(js|css|png|jpeg|jpg|ico|svg)$/)) { 47 | // case of "vue-cli-service build --mode development" 48 | filePath = path.join(app.getAppPath(), 'dist', filePath.slice(3)); 49 | } 50 | } else { 51 | if (filePath.match(/^\/(css|img|js)/)) { 52 | filePath = path.join(app.getAppPath(), 'dist', filePath.slice(1)); 53 | } else if (filePath.match(/^\/[^/\\]+?\.(js|css|png|jpeg|jpg|ico|svg)$/)) { 54 | // case of "vue-cli-service build --mode development" 55 | filePath = path.join(app.getAppPath(), 'dist', filePath.slice(1)); 56 | } 57 | } 58 | callback(path.normalize(filePath)); 59 | }); 60 | createMainWindow(); 61 | }); 62 | 63 | 64 | // Quit when all windows are closed. 65 | // tslint:disable-next-line:only-arrow-functions 66 | app.on('window-all-closed', function() { 67 | // On macOS it is common for applications and their menu bar 68 | // to stay active until the user quits explicitly with Cmd + Q 69 | if (process.platform !== 'darwin') { 70 | app.quit(); 71 | } 72 | }); 73 | 74 | 75 | // tslint:disable-next-line:only-arrow-functions 76 | app.on('activate', function() { 77 | // On macOS it's common to re-create a window in the app when the 78 | // dock icon is clicked and there are no other windows open. 79 | createMainWindow(); 80 | }); 81 | 82 | 83 | // In this file you can include the rest of your app's specific main process 84 | // code. You can also put them in separate files and require them here. 85 | -------------------------------------------------------------------------------- /src.mainproc/windows/UserContentWindow.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import { app, BrowserWindow, shell } from 'electron'; 4 | import { registerWindow, unregisterWindow, isRegistered, getWindow } from '../lib/SimpleWindowManager'; 5 | 6 | 7 | 8 | const userContentWindowSymbol = Symbol(); 9 | 10 | 11 | // Keep a global reference of the window object, if you don't, the window will 12 | // be closed automatically when the JavaScript object is garbage collected. 13 | 14 | export function createUserContentWindow() { 15 | // UserContentWindow is singleton. 16 | if (isRegistered(userContentWindowSymbol)) { 17 | return getWindow(userContentWindowSymbol); 18 | } 19 | 20 | // Create the browser window. 21 | let userContentWindow: BrowserWindow | null = new BrowserWindow({ 22 | webPreferences: { 23 | nodeIntegration: false, // Electron 11: default is false 24 | nodeIntegrationInWorker: false, // Electron 11: default is false 25 | nodeIntegrationInSubFrames: false, // Electron 11: default is false 26 | enableRemoteModule: false, // Electron 11: default is false 27 | contextIsolation: true, 28 | preload: path.join(app.getAppPath(), 'src.preload/preload-for-user-contents.js'), 29 | }, 30 | width: 800, 31 | height: 600, 32 | }); 33 | registerWindow(userContentWindowSymbol, userContentWindow); 34 | 35 | // and load the html of the app. 36 | userContentWindow.loadURL('https://shellyln.github.io/'); 37 | 38 | userContentWindow.webContents.session.webRequest.onHeadersReceived((details: any, callback: any) => { 39 | const headers = {...details.responseHeaders}; 40 | for (const key of Object.keys(headers)) { 41 | if (key.toLowerCase() === 'content-security-policy') { 42 | delete headers[key]; 43 | } 44 | if (key.toLowerCase() === 'x-content-security-policy') { 45 | delete headers[key]; 46 | } 47 | } 48 | callback({ 49 | responseHeaders: { 50 | ...headers, 51 | 'content-security-policy': [ 52 | `default-src 'self';` + 53 | `script-src chrome: 'self'${app.isPackaged ? '' : ` devtools: 'unsafe-eval'`};` + 54 | `style-src chrome: 'self' 'unsafe-inline'${app.isPackaged ? '' : ` devtools:`};` + 55 | `img-src 'self';` + 56 | `frame-ancestors 'none';`, 57 | ], 58 | }, 59 | }); 60 | }); 61 | 62 | userContentWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { 63 | // const url = webContents.getURL(); 64 | // 65 | // if (permission === 'notifications') { 66 | // // Approves the permissions request 67 | // callback(true); 68 | // } 69 | // if (!url.startsWith('https://my-website.com')) { 70 | // // Denies the permissions request 71 | // return callback(false); 72 | // } 73 | return callback(false); 74 | }); 75 | 76 | // Open the DevTools. 77 | // userContentWindow.webContents.openDevTools() 78 | 79 | // Emitted when the window is closed. 80 | userContentWindow.on('closed', () => { 81 | // Dereference the window object, usually you would store windows 82 | // in an array if your app supports multi windows, this is the time 83 | // when you should delete the corresponding element. 84 | unregisterWindow(userContentWindowSymbol); 85 | userContentWindow = null; 86 | }); 87 | 88 | userContentWindow.webContents.on('new-window', (event: any, url: string) => { 89 | event.preventDefault(); 90 | if (url.match(/^https?:\/\//)) { 91 | shell.openExternal(url); 92 | } 93 | }); 94 | 95 | return userContentWindow; 96 | } 97 | -------------------------------------------------------------------------------- /src.mainproc/windows/SubWindow.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import { app, BrowserWindow, shell } from 'electron'; 4 | import { registerWindow, unregisterWindow } from '../lib/SimpleWindowManager'; 5 | 6 | 7 | 8 | // Keep a global reference of the window object, if you don't, the window will 9 | // be closed automatically when the JavaScript object is garbage collected. 10 | 11 | export function createSubWindow() { 12 | // Create the browser window. 13 | let subWindow: BrowserWindow | null = new BrowserWindow({ 14 | webPreferences: { 15 | nodeIntegration: false, // Electron 11: default is false 16 | nodeIntegrationInWorker: false, // Electron 11: default is false 17 | nodeIntegrationInSubFrames: false, // Electron 11: default is false 18 | enableRemoteModule: false, // Electron 11: default is false 19 | contextIsolation: true, 20 | preload: path.join(app.getAppPath(), 'src.preload/preload-isolated.js'), 21 | }, 22 | width: 800, 23 | height: 600, 24 | }); 25 | registerWindow(subWindow, subWindow); 26 | 27 | // and load the html of the app. 28 | subWindow.loadFile(path.join(app.getAppPath(), 'dist/sub-window.html')); 29 | 30 | // CSP is not work while the location scheme is 'file'. 31 | // And when if navigated to http/https, CSP is to be enabled. 32 | if (app.isPackaged) { 33 | subWindow.webContents.session.webRequest.onHeadersReceived((details: any, callback: any) => { 34 | const headers = {...details.responseHeaders}; 35 | for (const key of Object.keys(headers)) { 36 | if (key.toLowerCase() === 'content-security-policy') { 37 | delete headers[key]; 38 | } 39 | if (key.toLowerCase() === 'x-content-security-policy') { 40 | delete headers[key]; 41 | } 42 | } 43 | callback({ 44 | responseHeaders: { 45 | ...headers, 46 | 'content-security-policy': [ 47 | `default-src 'none';` + 48 | `frame-ancestors 'none';`, 49 | ], 50 | }, 51 | }); 52 | }); 53 | } else { 54 | // NOTE: Remove CSP to use devtools. 55 | // Refused to load the script 'devtools://devtools/bundled/shell.js' 56 | // because it violates the following Content Security Policy directive: "default-src 'none'. 57 | subWindow.webContents.session.webRequest.onHeadersReceived((details: any, callback: any) => { 58 | callback({ 59 | responseHeaders: { 60 | ...details.responseHeaders, 61 | }, 62 | }); 63 | }); 64 | } 65 | 66 | subWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { 67 | // const url = webContents.getURL(); 68 | // 69 | // if (permission === 'notifications') { 70 | // // Approves the permissions request 71 | // callback(true); 72 | // } 73 | // if (!url.startsWith('https://my-website.com')) { 74 | // // Denies the permissions request 75 | // return callback(false); 76 | // } 77 | return callback(false); 78 | }); 79 | 80 | // Open the DevTools. 81 | // subWindow.webContents.openDevTools() 82 | 83 | // Emitted when the window is closed. 84 | subWindow.on('closed', () => { 85 | // Dereference the window object, usually you would store windows 86 | // in an array if your app supports multi windows, this is the time 87 | // when you should delete the corresponding element. 88 | unregisterWindow(subWindow as BrowserWindow); 89 | subWindow = null; 90 | }); 91 | 92 | subWindow.webContents.on('new-window', (event: any, url: string) => { 93 | event.preventDefault(); 94 | if (url.match(/^https?:\/\//)) { 95 | shell.openExternal(url); 96 | } 97 | }); 98 | 99 | return subWindow; 100 | } 101 | -------------------------------------------------------------------------------- /src.mainproc/windows/MainWindow.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import { app, BrowserWindow, shell } from 'electron'; 4 | import { registerWindow, unregisterWindow, isRegistered, getWindow } from '../lib/SimpleWindowManager'; 5 | 6 | 7 | 8 | const mainWindowSymbol = Symbol(); 9 | 10 | 11 | // Keep a global reference of the window object, if you don't, the window will 12 | // be closed automatically when the JavaScript object is garbage collected. 13 | 14 | export function createMainWindow() { 15 | // MainWindow is singleton. 16 | if (isRegistered(mainWindowSymbol)) { 17 | return getWindow(mainWindowSymbol); 18 | } 19 | 20 | // Create the browser window. 21 | let mainWindow: BrowserWindow | null = new BrowserWindow({ 22 | webPreferences: { 23 | nodeIntegration: false, // Electron 11: default is false 24 | nodeIntegrationInWorker: false, // Electron 11: default is false 25 | nodeIntegrationInSubFrames: false, // Electron 11: default is false 26 | enableRemoteModule: false, // Electron 11: default is false 27 | contextIsolation: true, 28 | preload: path.join(app.getAppPath(), 'src.preload/preload-isolated.js'), 29 | }, 30 | width: 800, 31 | height: 600, 32 | }); 33 | registerWindow(mainWindowSymbol, mainWindow); 34 | 35 | // and load the html of the app. 36 | mainWindow.loadFile(path.join(app.getAppPath(), 'dist/main-window.html')); 37 | 38 | // CSP is not work while the location scheme is 'file'. 39 | // And when if navigated to http/https, CSP is to be enabled. 40 | if (app.isPackaged) { 41 | mainWindow.webContents.session.webRequest.onHeadersReceived((details: any, callback: any) => { 42 | const headers = {...details.responseHeaders}; 43 | for (const key of Object.keys(headers)) { 44 | if (key.toLowerCase() === 'content-security-policy') { 45 | delete headers[key]; 46 | } 47 | if (key.toLowerCase() === 'x-content-security-policy') { 48 | delete headers[key]; 49 | } 50 | } 51 | callback({ 52 | responseHeaders: { 53 | ...headers, 54 | 'content-security-policy': [ 55 | `default-src 'none';` + 56 | `frame-ancestors 'none';`, 57 | ], 58 | }, 59 | }); 60 | }); 61 | } else { 62 | // NOTE: Remove CSP to use devtools. 63 | // Refused to load the script 'devtools://devtools/bundled/shell.js' 64 | // because it violates the following Content Security Policy directive: "default-src 'none'. 65 | mainWindow.webContents.session.webRequest.onHeadersReceived((details: any, callback: any) => { 66 | callback({ 67 | responseHeaders: { 68 | ...details.responseHeaders, 69 | }, 70 | }); 71 | }); 72 | } 73 | 74 | mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { 75 | // const url = webContents.getURL(); 76 | // 77 | // if (permission === 'notifications') { 78 | // // Approves the permissions request 79 | // callback(true); 80 | // } 81 | // if (!url.startsWith('https://my-website.com')) { 82 | // // Denies the permissions request 83 | // return callback(false); 84 | // } 85 | return callback(false); 86 | }); 87 | 88 | // Open the DevTools. 89 | // mainWindow.webContents.openDevTools() 90 | 91 | // Emitted when the window is closed. 92 | mainWindow.on('closed', () => { 93 | // Dereference the window object, usually you would store windows 94 | // in an array if your app supports multi windows, this is the time 95 | // when you should delete the corresponding element. 96 | unregisterWindow(mainWindowSymbol); 97 | mainWindow = null; 98 | }); 99 | 100 | mainWindow.webContents.on('new-window', (event: any, url: string) => { 101 | event.preventDefault(); 102 | if (url.match(/^https?:\/\//)) { 103 | shell.openExternal(url); 104 | } 105 | }); 106 | 107 | return mainWindow; 108 | } 109 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 117 | 118 | 126 | 127 | 128 | 129 | 130 | 146 | --------------------------------------------------------------------------------