├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── setup-tools │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── release-next.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .releaserc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build.js ├── docs-site └── docs │ └── creatingplugins.md ├── package.json ├── pnpm-lock.yaml └── src ├── cli-scripts ├── add.ts ├── common.ts ├── copy.ts ├── index.ts ├── open.ts └── update.ts ├── electron-platform-template ├── .gitignore ├── assets │ ├── appIcon.ico │ ├── appIcon.png │ ├── splash.gif │ └── splash.png ├── electron-builder.config.json ├── live-runner.js ├── package.json ├── resources │ └── electron-publisher-custom.js ├── src │ ├── index.ts │ ├── preload.ts │ ├── rt │ │ ├── electron-plugins.js │ │ └── electron-rt.ts │ └── setup.ts └── tsconfig.json └── electron-platform ├── ElectronDeepLinking.ts ├── ElectronSplashScreen.ts ├── definitions.ts ├── index.ts └── util.ts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "electron", 3 | "projectOwner": "capacitor-community", 4 | "repoType": "github", 5 | "badgeTemplate": "-orange?style=flat-square\" />", 6 | "repoHost": "https://github.com", 7 | "files": [ 8 | "README.md" 9 | ], 10 | "imageSize": 75, 11 | "commit": true, 12 | "commitConvention": "none", 13 | "contributors": [ 14 | { 15 | "login": "mlynch", 16 | "name": "Max Lynch", 17 | "avatar_url": "https://avatars3.githubusercontent.com/u/11214?v=4", 18 | "profile": "http://ionicframework.com/", 19 | "contributions": [ 20 | "code", 21 | "doc" 22 | ] 23 | }, 24 | { 25 | "login": "IT-MikeS", 26 | "name": "Mike S", 27 | "avatar_url": "https://avatars0.githubusercontent.com/u/20338451?v=4", 28 | "profile": "https://github.com/IT-MikeS", 29 | "contributions": [ 30 | "code", 31 | "doc" 32 | ] 33 | }, 34 | { 35 | "login": "danielsogl", 36 | "name": "Daniel Sogl", 37 | "avatar_url": "https://avatars2.githubusercontent.com/u/15234844?v=4", 38 | "profile": "https://github.com/danielsogl", 39 | "contributions": [ 40 | "code" 41 | ] 42 | }, 43 | { 44 | "login": "vevedh", 45 | "name": "Hervé de CHAVIGNY", 46 | "avatar_url": "https://avatars1.githubusercontent.com/u/1430389?v=4", 47 | "profile": "https://github.com/vevedh", 48 | "contributions": [ 49 | "code", 50 | "doc" 51 | ] 52 | }, 53 | { 54 | "login": "leMaik", 55 | "name": "Maik Marschner", 56 | "avatar_url": "https://avatars2.githubusercontent.com/u/5544859?v=4", 57 | "profile": "http://twitter.com/leMaikOfficial", 58 | "contributions": [ 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "stewones", 64 | "name": "Stew", 65 | "avatar_url": "https://avatars1.githubusercontent.com/u/719763?v=4", 66 | "profile": "https://github.com/stewones", 67 | "contributions": [ 68 | "code" 69 | ] 70 | }, 71 | { 72 | "login": "coreyjv", 73 | "name": "Corey Vaillancourt", 74 | "avatar_url": "https://avatars3.githubusercontent.com/u/2730750?v=4", 75 | "profile": "https://github.com/coreyjv", 76 | "contributions": [ 77 | "code" 78 | ] 79 | }, 80 | { 81 | "login": "jgoux", 82 | "name": "Julien Goux", 83 | "avatar_url": "https://avatars0.githubusercontent.com/u/1443499?v=4", 84 | "profile": "https://github.com/jgoux", 85 | "contributions": [ 86 | "code" 87 | ] 88 | }, 89 | { 90 | "login": "MathisTLD", 91 | "name": "MathisTLD", 92 | "avatar_url": "https://avatars.githubusercontent.com/u/42317770?v=4", 93 | "profile": "https://github.com/MathisTLD", 94 | "contributions": [ 95 | "code" 96 | ] 97 | }, 98 | { 99 | "login": "challenger71498", 100 | "name": "challenger71498", 101 | "avatar_url": "https://avatars.githubusercontent.com/u/43464986?v=4", 102 | "profile": "https://github.com/challenger71498", 103 | "contributions": [ 104 | "code" 105 | ] 106 | }, 107 | { 108 | "login": "jdgjsag67251", 109 | "name": "jdgjsag67251", 110 | "avatar_url": "https://avatars.githubusercontent.com/u/88368191?v=4", 111 | "profile": "https://github.com/jdgjsag67251", 112 | "contributions": [ 113 | "code" 114 | ] 115 | } 116 | ], 117 | "contributorsPerLine": 7, 118 | "skipCi": true 119 | } 120 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint', 'import'], 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:import/typescript'], 5 | rules: { 6 | 'no-constant-condition': 'off', 7 | '@typescript-eslint/no-this-alias': 'off', 8 | '@typescript-eslint/no-explicit-any': 'off', 9 | '@typescript-eslint/explicit-module-boundary-types': ['error', { allowArgumentsExplicitlyTypedAsAny: true }], 10 | '@typescript-eslint/array-type': 'error', 11 | '@typescript-eslint/consistent-type-assertions': 'error', 12 | '@typescript-eslint/consistent-type-imports': 'error', 13 | '@typescript-eslint/prefer-for-of': 'error', 14 | '@typescript-eslint/prefer-optional-chain': 'error', 15 | 'import/first': 'error', 16 | 'import/order': [ 17 | 'error', 18 | { 19 | alphabetize: { order: 'asc', caseInsensitive: false }, 20 | groups: [['builtin', 'external'], 'parent', ['sibling', 'index']], 21 | 'newlines-between': 'always', 22 | }, 23 | ], 24 | 'import/newline-after-import': 'error', 25 | 'import/no-duplicates': 'error', 26 | 'import/no-mutable-exports': 'error', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/actions/setup-tools/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Tools' 2 | description: 'Setup tools needed in repo' 3 | 4 | inputs: 5 | skip-install-on-cache-hit: 6 | description: 'If a Cache Hit happens, skip pnpm install' 7 | required: true 8 | default: 'false' 9 | 10 | runs: 11 | using: 'composite' 12 | steps: 13 | - name: Install Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | 18 | - name: Install PNPM 19 | uses: pnpm/action-setup@v2 20 | id: pnpm-install 21 | with: 22 | version: 8 23 | run_install: false 24 | 25 | - name: Get pnpm store directory 26 | id: pnpm-cache 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_ENV 30 | 31 | - name: Setup PNPM cache 32 | id: cache-pnpm-store 33 | uses: actions/cache@v3 34 | env: 35 | STORE_PATH: ${{ env.STORE_PATH }} 36 | with: 37 | path: ${{ env.STORE_PATH }} 38 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 39 | restore-keys: | 40 | ${{ runner.os }}-pnpm-store- 41 | 42 | - name: Install dependencies 43 | if: inputs.skip-install-on-cache-hit == 'false' || (inputs.skip-install-on-cache-hit == 'true' && steps.cache-pnpm-store.cache-hit == 'false') 44 | shell: bash 45 | run: pnpm install -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "next" 8 | pull_request: 9 | types: 10 | - 'synchronize' 11 | - 'opened' 12 | workflow_dispatch: 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | setup: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | - name: 'Setup Tools' 26 | uses: ./.github/actions/setup-tools 27 | 28 | lint: 29 | needs: [setup] 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 30 32 | steps: 33 | - uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 0 36 | token: ${{ secrets.GITHUB_TOKEN }} 37 | - name: 'Setup Tools' 38 | uses: ./.github/actions/setup-tools 39 | - name: 'Lint Package' 40 | shell: bash 41 | run: | 42 | pnpm run lint 43 | 44 | build: 45 | needs: [setup, lint] 46 | runs-on: ubuntu-latest 47 | timeout-minutes: 30 48 | steps: 49 | - uses: actions/checkout@v3 50 | with: 51 | fetch-depth: 0 52 | token: ${{ secrets.GITHUB_TOKEN }} 53 | - name: 'Setup Tools' 54 | uses: ./.github/actions/setup-tools 55 | - name: 'Build Package' 56 | shell: bash 57 | run: | 58 | pnpm run build 59 | -------------------------------------------------------------------------------- /.github/workflows/release-next.yml: -------------------------------------------------------------------------------- 1 | name: Release to NPM Next 2 | 3 | on: 4 | push: 5 | branches: 6 | - next 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | - name: 'Setup Tools' 22 | uses: ./.github/actions/setup-tools 23 | - name: Build package 24 | run: pnpm run build 25 | - name: "NPM Identity" 26 | run: | 27 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_PUBLISH_TOKEN }}" >> ~/.npmrc 28 | npm whoami 29 | - name: Release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 33 | run: | 34 | pnpm whoami 35 | pnpm run shipit-next -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to NPM Latest 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | id-token: write 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: 'Setup Tools' 21 | uses: ./.github/actions/setup-tools 22 | - name: Build package 23 | run: pnpm run build 24 | - name: "NPM Identity" 25 | run: | 26 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_PUBLISH_TOKEN }}" >> ~/.npmrc 27 | npm whoami 28 | - name: Release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 32 | run: pnpm run shipit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .cache/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | .DS_Store 7 | .vscode/ 8 | npm-debug.log 9 | template.tar.gz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .github/ 3 | node_modules/ 4 | plugin-examples/ 5 | .husky/ 6 | src/ 7 | .eslintrc.js 8 | .prettierrc.js 9 | .eslintignore 10 | .prettierignore 11 | docs/ 12 | .all-contributorsrc 13 | .gitignore 14 | CODE_OF_CONDUCT.md 15 | CONTRIBUTING.md 16 | rollup.config.plugins.js 17 | /tsconfig.json 18 | /tsconfig.electron.json 19 | /tsconfig.plugins.json 20 | deployChangeLog.sh 21 | CHANGELOG.md 22 | tsconfig.cli-scripts.json 23 | tsconfig.runtime.json 24 | changelog.config.js 25 | template/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/** 3 | package.json -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, // default: 80 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, // default: false 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'es5', 10 | bracketSpacing: true, 11 | bracketSameLine: false, 12 | arrowParens: 'always', 13 | overrides: [ 14 | { 15 | files: ['*.java'], 16 | options: { 17 | printWidth: 140, 18 | tabWidth: 4, 19 | useTabs: false, 20 | trailingComma: 'none', 21 | }, 22 | }, 23 | { 24 | files: '*.md', 25 | options: { 26 | parser: 'mdx', 27 | }, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { "name": "next", "prerelease": true } 5 | ], 6 | "plugins": [ 7 | "@semantic-release/commit-analyzer", 8 | "@semantic-release/release-notes-generator", 9 | "@semantic-release/changelog", 10 | [ 11 | "@semantic-release/npm", 12 | { 13 | "npmPublish": false 14 | } 15 | ], 16 | [ 17 | "@semantic-release/git", 18 | { 19 | "assets": [ 20 | "package.json", 21 | "pnpm-lock.json", 22 | "CHANGELOG.md" 23 | ], 24 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}\n\n\nskip-checks: true" 25 | } 26 | ], 27 | "@semantic-release/github" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [5.0.1](https://github.com/capacitor-community/electron/compare/v5.0.0...v5.0.1) (2023-09-21) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * electron version bump in template ([5b9c478](https://github.com/capacitor-community/electron/commit/5b9c478a568debec93b60933da4bcd62ce50d15d)) 7 | 8 | # [5.0.0](https://github.com/capacitor-community/electron/compare/v4.1.3...v5.0.0) (2023-09-21) 9 | 10 | 11 | ### Features 12 | 13 | * v5 ([a951a58](https://github.com/capacitor-community/electron/commit/a951a58a67be3be2aa50ca4fdd8252163209c9ce)) 14 | 15 | 16 | ### BREAKING CHANGES 17 | 18 | * use new electron and cap versions 19 | 20 | ## [4.1.3](https://github.com/capacitor-community/electron/compare/v4.1.2...v4.1.3) (2023-09-21) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * typo ([045c260](https://github.com/capacitor-community/electron/commit/045c260bc67318d8c9940f94d07cb6c54c796e6e)) 26 | 27 | # [5.0.0-next.4](https://github.com/capacitor-community/electron/compare/v5.0.0-next.3...v5.0.0-next.4) (2023-09-21) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * no old changelog ([d72b1a7](https://github.com/capacitor-community/electron/commit/d72b1a753200373d6c4f44767b9c00c378bdacfb)) 33 | 34 | # [5.0.0-next.3](https://github.com/capacitor-community/electron/compare/v5.0.0-next.2...v5.0.0-next.3) (2023-05-13) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * clean up ts errors ([0755400](https://github.com/capacitor-community/electron/commit/0755400723cf2847ec4b9f824302d623742da12a)) 40 | 41 | # [5.0.0-next.2](https://github.com/capacitor-community/electron/compare/v5.0.0-next.1...v5.0.0-next.2) (2023-05-12) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * release fix ([85e5fad](https://github.com/capacitor-community/electron/commit/85e5fadf6bdf3267475c409588947752c0a6c2d7)) 47 | * release fixes ([4a0bd52](https://github.com/capacitor-community/electron/commit/4a0bd52ccb79210ee513476765923f386067e1fe)) 48 | 49 | # [5.0.0-next.1](https://github.com/capacitor-community/electron/compare/v4.1.2...v5.0.0-next.1) (2023-05-12) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * typo ([045c260](https://github.com/capacitor-community/electron/commit/045c260bc67318d8c9940f94d07cb6c54c796e6e)) 55 | 56 | 57 | ### Features 58 | 59 | * new repo structure ([b0df017](https://github.com/capacitor-community/electron/commit/b0df0172acef4f4432a1d21d7c9038db8b0cca86)) 60 | * updated electron version ([7d57405](https://github.com/capacitor-community/electron/commit/7d57405050969281f648829bec669ef0bb9029c6)) 61 | 62 | 63 | ### BREAKING CHANGES 64 | 65 | * v24 electron 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please see [`CODE_OF_CONDUCT.md`](https://github.com/capacitor-community/welcome/blob/main/CODE_OF_CONDUCT.md) in the Capacitor Community Welcome repository. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Mike Summerfeldt. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 |

4 | 5 |

6 | Bring your Capacitor ⚡ apps to the desktop with Electron! 🖥 7 |

8 |

9 | 10 | 11 | 12 | 13 | 14 |

15 |

16 | 17 | npm 18 | 19 | GitHub Workflow Status 20 |

21 | 22 | ## ⚠ Version 4 or Above Info 23 | - You will need version `5.4.0`+ of capacitor. 24 | - The template and inner workings have changed a lot, a migration guide will be done but for now creating a new project to tinker with before migrating main projects is a good idea. 25 | - Plugins from previous versions `@capacitor-community/electron` will not function in V4 or above, however all web plugins will continue to function as normal. 26 | - V4 and later comes with no plugins out of the box. Instead V4 and later and above follow Capacitor in seperated plugins, see [Plugin Examples Directory](https://github.com/capacitor-community/electron/tree/main/plugin-examples) for examples. 27 | 28 | ## 📖 Documentation: 29 | 30 | [You can find the docs site here.](https://capacitor-community.github.io/electron/) 31 | 32 | ## 🔐 Security 33 | 34 | While this platform strives to be inline with current secure practices, there are things outside of this platforms control. Please take the time to read through the [security checklist](https://www.electronjs.org/docs/tutorial/security#checklist-security-recommendations) the electron team has created to keep your application as safe and secure as you possibly can. 35 | 36 | ## 🛠 Maintainers 37 | 38 | | Maintainer | GitHub | Social | Sponsoring Company | Primary | 39 | | ---------------- | --------------------------------------- | ----------------------------------------- | ------------------ | ------- | 40 | | Mike Summerfeldt | [IT-MikeS](https://github.com/IT-MikeS) | [@IT_MikeS](https://twitter.com/IT_MikeS) | Volunteer | Yes | 41 | 42 | 43 | ## ✨ Contributors 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |

Max Lynch

💻 📖

Mike Summerfeldt

💻 📖

Daniel Sogl

💻

Hervé de CHAVIGNY

💻 📖

Maik Marschner

💻

Stew

💻

Corey Vaillancourt

💻

Julien Goux

💻

MathisTLD

💻

challenger71498

💻

jdgjsag67251

💻
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const esbuild = require('esbuild'); 4 | const { readdirSync } = require('fs'); 5 | const { join } = require('path'); 6 | const tar = require('tar'); 7 | 8 | async function packTemplate() { 9 | const templateSrc = join('./', 'src', 'electron-platform-template'); 10 | const destTemplateFilePath = join('./', 'template.tar.gz'); 11 | const files = []; 12 | readdirSync(templateSrc).forEach((file) => { 13 | files.push(file); 14 | }); 15 | await tar.create({ gzip: true, file: destTemplateFilePath, cwd: templateSrc }, files); 16 | console.log(`Packed ${destTemplateFilePath}!`); 17 | } 18 | 19 | async function buildCliScrpts() { 20 | await esbuild.build({ 21 | entryPoints: ['src/cli-scripts/index.ts'], 22 | bundle: true, 23 | outfile: 'dist/cli-scripts/cap-scripts.js', 24 | platform: 'node', 25 | target: 'node16', 26 | minify: true, 27 | external: ['child_process', 'fs', 'path', 'fs-extra', 'crypto', 'chalk', 'ora'], 28 | }); 29 | } 30 | 31 | async function buildPlatformCore() { 32 | await esbuild.build({ 33 | entryPoints: ['src/electron-platform/index.ts'], 34 | bundle: true, 35 | outfile: 'dist/core/index.js', 36 | platform: 'node', 37 | target: 'node16', 38 | minify: true, 39 | external: ['electron', 'fs', 'path', 'mime-types', 'events'], 40 | }); 41 | } 42 | 43 | (async () => { 44 | try { 45 | await buildPlatformCore(); 46 | await buildCliScrpts(); 47 | await packTemplate(); 48 | console.log('\nPlatform Build Complete.\n'); 49 | } catch (e) { 50 | console.error(e); 51 | process.exit(1); 52 | } 53 | })(); 54 | -------------------------------------------------------------------------------- /docs-site/docs/creatingplugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Create a Capacitor Electron Plugin 6 | 7 | 1. Create or open a Capacitor V3 compatible plugin in your editor of choice. 8 | 2. Create a folder named `electron` in the root of this plugin, with a sub-folder of `src` in it. 9 | 3. Create a ts file in the above `src` folder named `index.ts` and either paste in the following example code of the `@capacitor/dialog` plugin and edit away, or create your own from scratch: 10 | ```typescript 11 | import { dialog } from 'electron' 12 | 13 | import type { 14 | DialogPlugin, 15 | AlertOptions, 16 | PromptOptions, 17 | PromptResult, 18 | ConfirmOptions, 19 | ConfirmResult, 20 | } from '../../src/definitions'; 21 | 22 | export class DialogElectron implements DialogPlugin { 23 | async alert(options: AlertOptions): Promise { 24 | await dialog.showMessageBox({message: options.message + ' --- electron'}); 25 | } 26 | 27 | async prompt(options: PromptOptions): Promise { 28 | const val = window.prompt(options.message, options.inputText || ''); 29 | return { 30 | value: val !== null ? val : '', 31 | cancelled: val === null, 32 | }; 33 | } 34 | 35 | async confirm(options: ConfirmOptions): Promise { 36 | const val = window.confirm(options.message); 37 | return { 38 | value: val, 39 | }; 40 | } 41 | } 42 | ``` 43 | 4. Create a `.gitignore` file in the `electron` folder with the following: 44 | ``` 45 | dist 46 | ``` 47 | 5. Create a `.npmignore` file in the `electron` folder with the following: 48 | ``` 49 | src 50 | ``` 51 | 6. Create a `rollup.config.js` file in the `electron` folder with the following: 52 | ```javascript 53 | export default { 54 | input: 'electron/build/electron/src/index.js', 55 | output: [ 56 | { 57 | file: 'electron/dist/plugin.js', 58 | format: 'cjs', 59 | sourcemap: true, 60 | inlineDynamicImports: true, 61 | }, 62 | ], 63 | external: ['@capacitor/core'], 64 | }; 65 | ``` 66 | 7. Create a `tsconfig.json` file in the `electron` folder with the following: 67 | ```json 68 | { 69 | "compilerOptions": { 70 | "allowSyntheticDefaultImports": true, 71 | "declaration": true, 72 | "experimentalDecorators": true, 73 | "noEmitHelpers": true, 74 | "importHelpers": true, 75 | "lib": ["dom", "es2020"], 76 | "module": "commonjs", 77 | "noImplicitAny": true, 78 | "noUnusedLocals": true, 79 | "noUnusedParameters": true, 80 | "outDir": "build", 81 | "sourceMap": true, 82 | "strict": false, 83 | "target": "ES2017" 84 | }, 85 | "include": ["src/**/*"] 86 | } 87 | ``` 88 | 8. Modify the main `package.json` in the root directory, add a property to the `capacitor` object so it reflects the following (android and ios shown for example only): 89 | ```json 90 | "capacitor": { 91 | "ios": { 92 | "src": "ios" 93 | }, 94 | "android": { 95 | "src": "android" 96 | }, 97 | "electron": { 98 | "src": "electron" 99 | } 100 | }, 101 | ``` 102 | 9. Add your electron implementation to the `/src/index.ts` where your web implementation is registered: 103 | ```typescript 104 | const Dialog = registerPlugin('Dialog', { 105 | web: () => import('./web').then(m => new m.DialogWeb()), 106 | electron: () => (window as any).CapacitorCustomPlatform.plugins.DialogElectron 107 | }); 108 | ``` 109 | 110 | 10. Modify the main `package.json` in the root directory, add an entry into the `files` array of the following: 111 | `electron/` 112 | 11. Modify the main `package.json` in the root directory, add an entry into the `scripts` object of the following: 113 | `"build-electron": "tsc --project electron/tsconfig.json && rollup -c electron/rollup.config.js && rimraf ./electron/build",` 114 | 12. Modify the main `package.json` in the root directory, edit the `build` entry in the `scripts` object to be the following: 115 | `"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js && npm run build-electron",` 116 | 13. Run the `build` npm script to build your plugin. 117 | 14. Release it to NPM then use in your capacitor apps as any other native plugin like android or ios. (dont forget to use `npx cap sync/copy/update/open @capacitor-community/electron`) 118 | 119 | 120 | ### Check out the `plugin-example` folder in the repo for a small demo plugin. 121 | 122 | ## Events 123 | If you want to emit events from your plugin, your class has to extend the [NodeJS event emitter class](https://nodejs.org/api/events.html#events_class_eventemitter). This is used by Capacitor Electron to determine if it should expose the `addListener` & `removeListener` functions to the Electron runtime plugin. 124 | 125 | ```typescript 126 | import { EventEmitter } from 'events'; 127 | 128 | export default class MyPlugin extends EventEmitter { 129 | constructor() { 130 | setInterval(() => { 131 | this.emit('my-event', 'You successfully listened to the 10sec event!'); 132 | }, 10_000); 133 | } 134 | } 135 | ``` 136 | 137 | In your client code you can do the following: 138 | ```typescript 139 | const id = CapacitorCustomPlatform.plugins.MyPlugin.addListener('my-event', console.log); 140 | 141 | // SOME CODE 142 | 143 | CapacitorCustomPlatform.plugins.MyPlugin.removeListener(id); 144 | ``` 145 | 146 | ## Config 147 | Plugins get access to the `capacitor.config.ts` config object as the first argument to the constructor. E.g.: 148 | ```typescript 149 | export default class App { 150 | private config?: Record; 151 | 152 | constructor(config?: Record) { 153 | this.config = config; 154 | } 155 | 156 | getLaunchUrl(): string | undefined { 157 | const url = this.config?.server?.url; 158 | 159 | return url ? { url } : undefined; 160 | } 161 | } 162 | ``` 163 | 164 | **Keep in mind that the config could possibly be `undefined`.** 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@capacitor-community/electron", 3 | "version": "5.0.1", 4 | "description": "Capacitor community support for the Electron platform!", 5 | "main": "dist/core/index.js", 6 | "typings": "dist/core/index.d.ts", 7 | "scripts": { 8 | "lint": "pnpm eslint . --ext ts && pnpm prettier \"./**/*.{css,html,ts,js,java}\" --check", 9 | "fmt": "pnpm eslint . --ext ts --fix && pnpm prettier \"./**/*.{css,html,ts,js,java}\" --write", 10 | "clean": "rimraf ./dist && rimraf ./template.tar.gz", 11 | "build": "pnpm run clean && pnpm run fmt && pnpm run build-platform && pnpm run build-platform-types", 12 | "capacitor:add": "node dist/cli-scripts/cap-scripts.js add", 13 | "capacitor:copy": "node dist/cli-scripts/cap-scripts.js copy", 14 | "capacitor:update": "node dist/cli-scripts/cap-scripts.js update", 15 | "capacitor:sync": "node dist/cli-scripts/cap-scripts.js sync", 16 | "capacitor:open": "node dist/cli-scripts/cap-scripts.js open", 17 | "build-platform": "node ./build.js", 18 | "build-platform-types": "tsc ./src/electron-platform/index.ts --outDir ./dist/core --declaration --emitDeclarationOnly --esModuleInterop", 19 | "shipit": "pnpm semantic-release && npm publish --tag latest --provenance", 20 | "shipit-next": "pnpm semantic-release && npm publish --tag next --provenance", 21 | "commit": "git add . && pnpm git-cz && git push" 22 | }, 23 | "license": "MIT", 24 | "author": "IT-MikeS", 25 | "devDependencies": { 26 | "@commitlint/cli": "^17.6.3", 27 | "@commitlint/config-conventional": "^17.6.3", 28 | "@semantic-release/changelog": "^6.0.3", 29 | "@semantic-release/commit-analyzer": "^9.0.2", 30 | "@semantic-release/git": "^10.0.1", 31 | "@semantic-release/npm": "^10.0.3", 32 | "@semantic-release/release-notes-generator": "^11.0.1", 33 | "@types/events": "^3.0.0", 34 | "@types/fs-extra": "^11.0.1", 35 | "@types/node": "^18.16.8", 36 | "@typescript-eslint/eslint-plugin": "^5.59.5", 37 | "@typescript-eslint/parser": "^5.59.5", 38 | "commitizen": "^4.3.0", 39 | "cz-conventional-changelog": "^3.3.0", 40 | "electron": "^26.2.2", 41 | "esbuild": "^0.17.18", 42 | "eslint": "^8.40.0", 43 | "eslint-config-prettier": "^8.8.0", 44 | "eslint-plugin-import": "^2.27.5", 45 | "is-ci": "^3.0.1", 46 | "prettier": "^2.8.8", 47 | "rimraf": "^5.0.0", 48 | "semantic-release": "^21.0.2", 49 | "tar": "^6.1.14", 50 | "typescript": "^5.0.4" 51 | }, 52 | "dependencies": { 53 | "@capacitor/cli": ">=5.4.0", 54 | "@capacitor/core": ">=5.4.0", 55 | "@ionic/utils-fs": "~3.1.6", 56 | "chalk": "^4.1.2", 57 | "electron-is-dev": "~2.0.0", 58 | "events": "~3.3.0", 59 | "fs-extra": "~11.1.1", 60 | "keyv": "^4.5.2", 61 | "mime-types": "~2.1.35", 62 | "ora": "^5.4.1" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "https://github.com/capacitor-community/electron" 67 | }, 68 | "bugs": { 69 | "url": "https://github.com/capacitor-community/electron/issues" 70 | }, 71 | "commitlint": { 72 | "extends": [ 73 | "@commitlint/config-conventional" 74 | ] 75 | }, 76 | "config": { 77 | "commitizen": { 78 | "path": "cz-conventional-changelog" 79 | } 80 | }, 81 | "publishConfig": { 82 | "registry": "https://registry.npmjs.org/" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/cli-scripts/add.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import { copySync } from 'fs-extra'; 3 | import { join } from 'path'; 4 | import { extract } from 'tar'; 5 | 6 | import type { TaskInfoProvider } from './common'; 7 | import { readJSON, runExec, writePrettyJSON } from './common'; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | export async function doAdd(taskInfoMessageProvider: TaskInfoProvider): Promise { 11 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 12 | const usersProjectDir = process.env.CAPACITOR_ROOT_DIR!; 13 | const platformNodeModuleTemplateTar = join( 14 | usersProjectDir, 15 | 'node_modules', 16 | '@capacitor-community', 17 | 'electron', 18 | 'template.tar.gz' 19 | ); 20 | const destDir = join(usersProjectDir, 'electron'); 21 | let usersProjectCapConfigFile: string | undefined = undefined; 22 | let configFileName: string | undefined = undefined; 23 | 24 | const configFileOptions = { 25 | ts: join(usersProjectDir, 'capacitor.config.ts'), 26 | js: join(usersProjectDir, 'capacitor.config.js'), 27 | json: join(usersProjectDir, 'capacitor.config.json'), 28 | }; 29 | if (existsSync(configFileOptions.ts)) { 30 | usersProjectCapConfigFile = configFileOptions.ts; 31 | configFileName = 'capacitor.config.ts'; 32 | } else if (existsSync(configFileOptions.js)) { 33 | usersProjectCapConfigFile = configFileOptions.js; 34 | configFileName = 'capacitor.config.js'; 35 | } else { 36 | usersProjectCapConfigFile = configFileOptions.json; 37 | configFileName = 'capacitor.config.json'; 38 | } 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 41 | const configData = JSON.parse(process.env.CAPACITOR_CONFIG!); 42 | 43 | if (!existsSync(destDir)) { 44 | mkdirSync(destDir); 45 | taskInfoMessageProvider(`extracting template`); 46 | await extract({ file: platformNodeModuleTemplateTar, cwd: destDir }); 47 | taskInfoMessageProvider(`copying capacitor config file`); 48 | copySync(usersProjectCapConfigFile, join(destDir, configFileName)); 49 | 50 | const appName: string = configData.appName; 51 | const platformPackageJson = readJSON(join(destDir, 'package.json')); 52 | const rootPackageJson = readJSON(join(usersProjectDir, 'package.json')); 53 | platformPackageJson.name = appName; 54 | if (rootPackageJson.repository) { 55 | platformPackageJson.repository = rootPackageJson.repository; 56 | } 57 | taskInfoMessageProvider(`setting up electron project`); 58 | writePrettyJSON(join(destDir, 'package.json'), platformPackageJson); 59 | 60 | taskInfoMessageProvider(`installing npm modules`); 61 | await runExec(`cd ${destDir} && npm i`); 62 | } else { 63 | throw new Error('Electron platform already exists.'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/cli-scripts/common.ts: -------------------------------------------------------------------------------- 1 | import type { CapacitorConfig } from '@capacitor/cli'; 2 | import chalk from 'chalk'; 3 | import { exec } from 'child_process'; 4 | import { createHash } from 'crypto'; 5 | import { readFileSync, existsSync, writeFileSync } from 'fs'; 6 | import type { Ora } from 'ora'; 7 | import ora from 'ora'; 8 | import { dirname, join, parse, resolve } from 'path'; 9 | 10 | const enum PluginType { 11 | Core, 12 | Cordova, 13 | Incompatible, 14 | } 15 | interface PluginManifest { 16 | electron: { 17 | src: string; 18 | }; 19 | ios: { 20 | src: string; 21 | doctor?: any[]; 22 | }; 23 | android: { 24 | src: string; 25 | }; 26 | } 27 | export interface Plugin { 28 | id: string; 29 | name: string; 30 | version: string; 31 | rootPath: string; 32 | manifest?: PluginManifest; 33 | repository?: any; 34 | xml?: any; 35 | ios?: { 36 | name: string; 37 | type: PluginType; 38 | path: string; 39 | }; 40 | android?: { 41 | type: PluginType; 42 | path: string; 43 | }; 44 | } 45 | 46 | type DeepReadonly = { readonly [P in keyof T]: DeepReadonly }; 47 | 48 | export type ExternalConfig = DeepReadonly; 49 | interface Config { 50 | readonly app: AppConfig; 51 | } 52 | interface AppConfig { 53 | readonly rootDir: string; 54 | readonly appId: string; 55 | readonly appName: string; 56 | readonly webDir: string; 57 | readonly webDirAbs: string; 58 | readonly package: PackageJson; 59 | readonly extConfigType: 'json' | 'js' | 'ts'; 60 | readonly extConfigName: string; 61 | readonly extConfigFilePath: string; 62 | readonly extConfig: ExternalConfig; 63 | /** 64 | * Whether to use a bundled web runtime instead of relying on a bundler/module 65 | * loader. If you're not using something like rollup or webpack or dynamic ES 66 | * module imports, set this to "true" and import "capacitor.js" manually. 67 | */ 68 | readonly bundledWebRuntime: boolean; 69 | } 70 | interface PackageJson { 71 | readonly name: string; 72 | readonly version: string; 73 | readonly dependencies?: { readonly [key: string]: string | undefined }; 74 | readonly devDependencies?: { readonly [key: string]: string | undefined }; 75 | } 76 | 77 | export async function getPlugins(packageJsonPath: string): Promise<(Plugin | null)[]> { 78 | const packageJson: PackageJson = (await readJSON(packageJsonPath)) as PackageJson; 79 | //console.log(packageJson); 80 | const possiblePlugins = getDependencies(packageJson); 81 | //console.log(possiblePlugins); 82 | const resolvedPlugins = await Promise.all(possiblePlugins.map(async (p) => resolvePlugin(p))); 83 | 84 | return resolvedPlugins.filter((p) => !!p); 85 | } 86 | 87 | export function getDependencies(packageJson: PackageJson): string[] { 88 | return [...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]; 89 | } 90 | 91 | export async function resolvePlugin(name: string): Promise { 92 | try { 93 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 94 | const usersProjectDir = process.env.CAPACITOR_ROOT_DIR!; 95 | const packagePath = resolveNode(usersProjectDir, name, 'package.json'); 96 | if (!packagePath) { 97 | console.error( 98 | `\nUnable to find ${chalk.bold(`node_modules/${name}`)}.\n` + `Are you sure ${chalk.bold(name)} is installed?` 99 | ); 100 | return null; 101 | } 102 | 103 | const rootPath = dirname(packagePath); 104 | const meta = await readJSON(packagePath); 105 | if (!meta) { 106 | return null; 107 | } 108 | if (meta.capacitor) { 109 | return { 110 | id: name, 111 | name: fixName(name), 112 | version: meta.version, 113 | rootPath, 114 | repository: meta.repository, 115 | manifest: meta.capacitor, 116 | }; 117 | } 118 | } catch (e) { 119 | // ignore 120 | } 121 | return null; 122 | } 123 | 124 | export function resolveNode(root: string, ...pathSegments: string[]): string | null { 125 | try { 126 | const t = require.resolve(pathSegments.join('/'), { paths: [root] }); 127 | //console.log(t); 128 | return t; 129 | } catch (e) { 130 | return null; 131 | } 132 | } 133 | 134 | export function errorLog(message: string): void { 135 | console.log(chalk.red(`Error: ${message}`)); 136 | } 137 | 138 | export function getCwd(): string | undefined { 139 | const _cwd = process.env.INIT_CWD; 140 | return _cwd; 141 | } 142 | 143 | export function readJSON(pathToUse: string): { [key: string]: any } { 144 | const data = readFileSync(pathToUse, 'utf8'); 145 | return JSON.parse(data); 146 | } 147 | 148 | export function runExec(command: string): Promise { 149 | return new Promise((resolve, reject) => { 150 | exec(command, (error, stdout, stderr) => { 151 | if (error) { 152 | reject(stdout + stderr); 153 | } else { 154 | resolve(stdout); 155 | } 156 | }); 157 | }); 158 | } 159 | 160 | export function fixName(name: string): string { 161 | name = name 162 | .replace(/\//g, '_') 163 | .replace(/-/g, '_') 164 | .replace(/@/g, '') 165 | .replace(/_\w/g, (m) => m[1].toUpperCase()); 166 | 167 | return name.charAt(0).toUpperCase() + name.slice(1); 168 | } 169 | 170 | export function hashJsFileName(filename: string, slt: number): string { 171 | const hash = createHash('md5').update(`${Date.now()}-${slt}-${filename}`).digest('hex'); 172 | return `${filename}-${hash}.js`; 173 | } 174 | 175 | export function writePrettyJSON(path: string, data: any): void { 176 | return writeFileSync(path, JSON.stringify(data, null, ' ') + '\n'); 177 | } 178 | 179 | export function resolveNodeFrom(start: string, id: string): string | null { 180 | const rootPath = parse(start).root; 181 | let basePath = resolve(start); 182 | let modulePath; 183 | while (true) { 184 | modulePath = join(basePath, 'node_modules', id); 185 | if (existsSync(modulePath)) { 186 | return modulePath; 187 | } 188 | if (basePath === rootPath) { 189 | return null; 190 | } 191 | basePath = dirname(basePath); 192 | } 193 | } 194 | 195 | export function resolveElectronPlugin(plugin: Plugin): string | null { 196 | if (plugin.manifest?.electron?.src) { 197 | return join(plugin.rootPath, plugin.manifest.electron.src, 'dist/plugin.js'); 198 | } else { 199 | return null; 200 | } 201 | } 202 | 203 | export type TaskInfoProvider = (messsage: string) => void; 204 | 205 | export async function runTask(title: string, fn: (info: TaskInfoProvider) => Promise): Promise { 206 | let spinner: Ora = ora(title).start(`${title}`); 207 | try { 208 | spinner = spinner.start(`${title}: ${chalk.dim('start 🚀')}`); 209 | const start = process.hrtime(); 210 | const value = await fn((message: string) => { 211 | spinner = spinner.info(); 212 | spinner = spinner.start(`${title}: ${chalk.dim(message)}`); 213 | }); 214 | spinner = spinner.info(); 215 | const elapsed = process.hrtime(start); 216 | spinner = spinner.succeed(`${title}: ${chalk.dim('completed in ' + formatHrTime(elapsed))}`); 217 | return value; 218 | } catch (e) { 219 | spinner = spinner.fail(`${title}: ${e.message ? e.message : ''}`); 220 | spinner = spinner.stop(); 221 | throw e; 222 | } 223 | } 224 | 225 | const TIME_UNITS = ['s', 'ms', 'μp']; 226 | 227 | function formatHrTime(hrtime: any) { 228 | let time = hrtime[0] + hrtime[1] / 1e9; 229 | let index = 0; 230 | for (; index < TIME_UNITS.length - 1; index++, time *= 1000) { 231 | if (time >= 1) { 232 | break; 233 | } 234 | } 235 | return time.toFixed(2) + TIME_UNITS[index]; 236 | } 237 | -------------------------------------------------------------------------------- /src/cli-scripts/copy.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { copySync, removeSync } from 'fs-extra'; 3 | import { join } from 'path'; 4 | 5 | import type { TaskInfoProvider } from './common'; 6 | import { errorLog } from './common'; 7 | 8 | export async function doCopy(taskInfoMessageProvider: TaskInfoProvider): Promise { 9 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 10 | const usersProjectDir = process.env.CAPACITOR_ROOT_DIR!; 11 | // const configData = JSON.parse(process.env.CAPACITOR_CONFIG!); 12 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 13 | const builtWebAppDir = process.env.CAPACITOR_WEB_DIR!; 14 | const destDir = join(usersProjectDir, 'electron', 'app'); 15 | try { 16 | if (existsSync(destDir)) removeSync(destDir); 17 | taskInfoMessageProvider(`Copying ${builtWebAppDir} into ${destDir}`); 18 | copySync(builtWebAppDir, destDir); 19 | } catch (e) { 20 | errorLog(e.message); 21 | throw e; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/cli-scripts/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { doAdd } from './add'; 3 | import { runTask } from './common'; 4 | import { doCopy } from './copy'; 5 | import { doOpen } from './open'; 6 | import { doUpdate } from './update'; 7 | 8 | async function doUpdateTask() { 9 | return await runTask('Updating Electron plugins', async (taskInfoMessageProvider) => { 10 | return await doUpdate(taskInfoMessageProvider); 11 | }); 12 | } 13 | 14 | async function doAddTask() { 15 | return await runTask('Adding Electron platform', async (taskInfoMessageProvider) => { 16 | return doAdd(taskInfoMessageProvider); 17 | }); 18 | } 19 | 20 | async function doCopyTask() { 21 | return await runTask('Copying Web App to Electron platform', async (taskInfoMessageProvider) => { 22 | return await doCopy(taskInfoMessageProvider); 23 | }); 24 | } 25 | 26 | async function doOpenTask() { 27 | return await runTask('Opening Electron platform', async (taskInfoMessageProvider) => { 28 | return await doOpen(taskInfoMessageProvider); 29 | }); 30 | } 31 | 32 | (async () => { 33 | const scriptToRun = process.argv[2] ? process.argv[2] : null; 34 | if (scriptToRun !== null) { 35 | switch (scriptToRun) { 36 | case 'add': 37 | await doAddTask(); 38 | await doCopyTask(); 39 | await doUpdateTask(); 40 | break; 41 | case 'copy': 42 | await doCopyTask(); 43 | break; 44 | case 'run': 45 | await doOpenTask(); 46 | break; 47 | case 'open': 48 | await doOpenTask(); 49 | break; 50 | case 'update': 51 | await doUpdateTask(); 52 | break; 53 | case 'sync': 54 | await doCopyTask(); 55 | await doUpdateTask(); 56 | break; 57 | default: 58 | throw new Error(`Invalid script chosen: ${scriptToRun}`); 59 | } 60 | } else { 61 | throw new Error(`Invalid script chosen: ${scriptToRun}`); 62 | } 63 | })(); 64 | -------------------------------------------------------------------------------- /src/cli-scripts/open.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import type { TaskInfoProvider } from './common'; 4 | import { runExec, errorLog } from './common'; 5 | 6 | export async function doOpen(taskInfoMessageProvider: TaskInfoProvider): Promise { 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 8 | const usersProjectDir = process.env.CAPACITOR_ROOT_DIR!; 9 | const destDir = join(usersProjectDir, 'electron'); 10 | try { 11 | taskInfoMessageProvider('building electron app'); 12 | taskInfoMessageProvider('running electron app'); 13 | await runExec(`cd ${destDir} && npm run electron:start-live`); 14 | } catch (e) { 15 | errorLog(e.message); 16 | throw e; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/cli-scripts/update.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'fs'; 2 | import { copySync } from 'fs-extra'; 3 | import { join, isAbsolute, resolve, relative } from 'path'; 4 | 5 | import type { TaskInfoProvider, Plugin } from './common'; 6 | import { getPlugins, readJSON, resolveElectronPlugin, runExec } from './common'; 7 | 8 | export async function doUpdate(taskInfoMessageProvider: TaskInfoProvider): Promise { 9 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 10 | const usersProjectDir = process.env.CAPACITOR_ROOT_DIR!; 11 | 12 | const userProjectPackageJsonPath = join(usersProjectDir, 'package.json'); 13 | 14 | const webAppPackageJson = await readJSON(userProjectPackageJsonPath); 15 | const dependencies = webAppPackageJson.dependencies ? webAppPackageJson.dependencies : {}; 16 | const devDependencies = webAppPackageJson.devDependencies ? webAppPackageJson.devDependencies : {}; 17 | const deps = { 18 | ...dependencies, 19 | ...devDependencies, 20 | }; 21 | 22 | taskInfoMessageProvider('searching for plugins'); 23 | 24 | //console.log(`\n\n${userProjectPackageJsonPath}\n\n`); 25 | 26 | // get all cap plugins installed 27 | const plugins = await getPlugins(userProjectPackageJsonPath); 28 | //console.log('\n\n'); 29 | //console.log(plugins); 30 | //console.log('\n'); 31 | 32 | // Get only the ones with electron "native" plugins 33 | const pluginMap: { 34 | name: string; 35 | path: string | null; 36 | installStr: string; 37 | id: string; 38 | }[] = plugins 39 | .filter((plugin: Plugin | null): plugin is Plugin => plugin !== null) 40 | .map((plugin: Plugin) => { 41 | const installStr: string = (() => { 42 | // Consider cases when package is not installed via npm 43 | if (deps[plugin?.id]) { 44 | if (deps[plugin.id].startsWith('file:')) { 45 | const pkgPath = deps[plugin?.id].replace(/^file:/, ''); 46 | const pkgAbsPath = isAbsolute(pkgPath) ? pkgPath : resolve(usersProjectDir, pkgPath); 47 | 48 | return relative(join(usersProjectDir, 'electron'), pkgAbsPath); // try to use relative path as much as possible 49 | } else if (deps[plugin.id].match(/^(https?|git):/)) { 50 | return deps[plugin.id]; 51 | } 52 | } 53 | 54 | return `${plugin?.id}@${plugin?.version}`; 55 | })(); 56 | 57 | const path = resolveElectronPlugin(plugin); 58 | const name = plugin?.name; 59 | const id = plugin?.id; 60 | return { name, path, installStr, id }; 61 | }) 62 | .filter((plugin) => plugin.path !== null); 63 | 64 | let npmIStr = ''; 65 | 66 | //console.log('\n'); 67 | //console.log(pluginMap); 68 | //console.log('\n'); 69 | 70 | taskInfoMessageProvider('generating electron-plugins.js'); 71 | 72 | const capacitorElectronRuntimeFilePath = join(usersProjectDir, 'electron', 'src', 'rt'); 73 | 74 | let outStr = `/* eslint-disable @typescript-eslint/no-var-requires */\n`; 75 | for (const electronPlugin of pluginMap) { 76 | npmIStr += ` ${electronPlugin.installStr}`; 77 | const tmpPath = join( 78 | relative(capacitorElectronRuntimeFilePath, usersProjectDir), 79 | 'node_modules', 80 | electronPlugin.id, 81 | 'electron', 82 | 'dist/plugin.js' 83 | ); 84 | outStr += `const ${electronPlugin.name} = require('${tmpPath.replace(/\\/g, '\\\\')}');\n`; 85 | } 86 | outStr += '\nmodule.exports = {\n'; 87 | for (const electronPlugin of pluginMap) { 88 | outStr += ` ${electronPlugin.name},\n`; 89 | } 90 | outStr += '}'; 91 | 92 | writeFileSync(join(capacitorElectronRuntimeFilePath, 'electron-plugins.js'), outStr, { encoding: 'utf-8' }); 93 | 94 | let usersProjectCapConfigFile: string | undefined = undefined; 95 | let configFileName: string | undefined = undefined; 96 | const configFileOptions = { 97 | ts: join(usersProjectDir, 'capacitor.config.ts'), 98 | js: join(usersProjectDir, 'capacitor.config.js'), 99 | json: join(usersProjectDir, 'capacitor.config.json'), 100 | }; 101 | if (existsSync(configFileOptions.ts)) { 102 | usersProjectCapConfigFile = configFileOptions.ts; 103 | configFileName = 'capacitor.config.ts'; 104 | } else if (existsSync(configFileOptions.js)) { 105 | usersProjectCapConfigFile = configFileOptions.js; 106 | configFileName = 'capacitor.config.js'; 107 | } else { 108 | usersProjectCapConfigFile = configFileOptions.json; 109 | configFileName = 'capacitor.config.json'; 110 | } 111 | copySync(usersProjectCapConfigFile, join(usersProjectDir, 'electron', configFileName), { overwrite: true }); 112 | 113 | if (npmIStr.length > 0) { 114 | taskInfoMessageProvider('installing electron plugin files'); 115 | console.log(`\n\nWill install:${npmIStr}\n\n`); 116 | await runExec(`cd ${join(usersProjectDir, 'electron')} && npm i${npmIStr}`); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/electron-platform-template/.gitignore: -------------------------------------------------------------------------------- 1 | # NPM renames .gitignore to .npmignore 2 | # In order to prevent that, we remove the initial "." 3 | # And the CLI then renames it 4 | app 5 | node_modules 6 | build 7 | dist 8 | logs 9 | -------------------------------------------------------------------------------- /src/electron-platform-template/assets/appIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capacitor-community/electron/9324279315db09474714ae088102a3d87293d01a/src/electron-platform-template/assets/appIcon.ico -------------------------------------------------------------------------------- /src/electron-platform-template/assets/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capacitor-community/electron/9324279315db09474714ae088102a3d87293d01a/src/electron-platform-template/assets/appIcon.png -------------------------------------------------------------------------------- /src/electron-platform-template/assets/splash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capacitor-community/electron/9324279315db09474714ae088102a3d87293d01a/src/electron-platform-template/assets/splash.gif -------------------------------------------------------------------------------- /src/electron-platform-template/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capacitor-community/electron/9324279315db09474714ae088102a3d87293d01a/src/electron-platform-template/assets/splash.png -------------------------------------------------------------------------------- /src/electron-platform-template/electron-builder.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.yourdoamnin.yourapp", 3 | "directories": { 4 | "buildResources": "resources" 5 | }, 6 | "files": [ 7 | "assets/**/*", 8 | "build/**/*", 9 | "capacitor.config.*", 10 | "app/**/*" 11 | ], 12 | "publish": { 13 | "provider": "github" 14 | }, 15 | "nsis": { 16 | "allowElevation": true, 17 | "oneClick": false, 18 | "allowToChangeInstallationDirectory": true 19 | }, 20 | "win": { 21 | "target": "nsis", 22 | "icon": "assets/appIcon.ico" 23 | }, 24 | "mac": { 25 | "category": "your.app.category.type", 26 | "target": "dmg" 27 | } 28 | } -------------------------------------------------------------------------------- /src/electron-platform-template/live-runner.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const cp = require('child_process'); 4 | const chokidar = require('chokidar'); 5 | const electron = require('electron'); 6 | 7 | let child = null; 8 | const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; 9 | const reloadWatcher = { 10 | debouncer: null, 11 | ready: false, 12 | watcher: null, 13 | restarting: false, 14 | }; 15 | 16 | ///* 17 | function runBuild() { 18 | return new Promise((resolve, _reject) => { 19 | let tempChild = cp.spawn(npmCmd, ['run', 'build']); 20 | tempChild.once('exit', () => { 21 | resolve(); 22 | }); 23 | tempChild.stdout.pipe(process.stdout); 24 | }); 25 | } 26 | //*/ 27 | 28 | async function spawnElectron() { 29 | if (child !== null) { 30 | child.stdin.pause(); 31 | child.kill(); 32 | child = null; 33 | await runBuild(); 34 | } 35 | child = cp.spawn(electron, ['--inspect=5858', './']); 36 | child.on('exit', () => { 37 | if (!reloadWatcher.restarting) { 38 | process.exit(0); 39 | } 40 | }); 41 | child.stdout.pipe(process.stdout); 42 | } 43 | 44 | function setupReloadWatcher() { 45 | reloadWatcher.watcher = chokidar 46 | .watch('./src/**/*', { 47 | ignored: /[/\\]\./, 48 | persistent: true, 49 | }) 50 | .on('ready', () => { 51 | reloadWatcher.ready = true; 52 | }) 53 | .on('all', (_event, _path) => { 54 | if (reloadWatcher.ready) { 55 | clearTimeout(reloadWatcher.debouncer); 56 | reloadWatcher.debouncer = setTimeout(async () => { 57 | console.log('Restarting'); 58 | reloadWatcher.restarting = true; 59 | await spawnElectron(); 60 | reloadWatcher.restarting = false; 61 | reloadWatcher.ready = false; 62 | clearTimeout(reloadWatcher.debouncer); 63 | reloadWatcher.debouncer = null; 64 | reloadWatcher.watcher = null; 65 | setupReloadWatcher(); 66 | }, 500); 67 | } 68 | }); 69 | } 70 | 71 | (async () => { 72 | await runBuild(); 73 | await spawnElectron(); 74 | setupReloadWatcher(); 75 | })(); 76 | -------------------------------------------------------------------------------- /src/electron-platform-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capacitor-app", 3 | "version": "1.0.0", 4 | "description": "An Amazing Capacitor App", 5 | "author": { 6 | "name": "", 7 | "email": "" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "" 12 | }, 13 | "license": "MIT", 14 | "main": "build/src/index.js", 15 | "scripts": { 16 | "build": "tsc && electron-rebuild", 17 | "electron:start-live": "node ./live-runner.js", 18 | "electron:start": "npm run build && electron --inspect=5858 ./", 19 | "electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json", 20 | "electron:make": "npm run build && electron-builder build -c ./electron-builder.config.json -p always" 21 | }, 22 | "dependencies": { 23 | "@capacitor-community/electron": "^5.0.0", 24 | "chokidar": "~3.5.3", 25 | "electron-is-dev": "~2.0.0", 26 | "electron-serve": "~1.1.0", 27 | "electron-unhandled": "~4.0.1", 28 | "electron-updater": "^5.3.0", 29 | "electron-window-state": "^5.0.3" 30 | }, 31 | "devDependencies": { 32 | "electron": "^26.2.2", 33 | "electron-builder": "~23.6.0", 34 | "electron-rebuild": "^3.2.9", 35 | "typescript": "^5.0.4" 36 | }, 37 | "keywords": [ 38 | "capacitor", 39 | "electron" 40 | ] 41 | } -------------------------------------------------------------------------------- /src/electron-platform-template/resources/electron-publisher-custom.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const electronPublish = require('electron-publish'); 4 | 5 | class Publisher extends electronPublish.Publisher { 6 | async upload(task) { 7 | console.log('electron-publisher-custom', task.file); 8 | } 9 | } 10 | module.exports = Publisher; 11 | -------------------------------------------------------------------------------- /src/electron-platform-template/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { CapacitorElectronConfig } from '@capacitor-community/electron'; 2 | import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron'; 3 | import type { MenuItemConstructorOptions } from 'electron'; 4 | import { app, MenuItem } from 'electron'; 5 | import electronIsDev from 'electron-is-dev'; 6 | import unhandled from 'electron-unhandled'; 7 | import { autoUpdater } from 'electron-updater'; 8 | 9 | import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup'; 10 | 11 | // Graceful handling of unhandled errors. 12 | unhandled(); 13 | 14 | // Define our menu templates (these are optional) 15 | const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })]; 16 | const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ 17 | { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, 18 | { role: 'viewMenu' }, 19 | ]; 20 | 21 | // Get Config options from capacitor.config 22 | const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig(); 23 | 24 | // Initialize our app. You can pass menu templates into the app here. 25 | // const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); 26 | const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); 27 | 28 | // If deeplinking is enabled then we will set it up here. 29 | if (capacitorFileConfig.electron?.deepLinkingEnabled) { 30 | setupElectronDeepLinking(myCapacitorApp, { 31 | customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp', 32 | }); 33 | } 34 | 35 | // If we are in Dev mode, use the file watcher components. 36 | if (electronIsDev) { 37 | setupReloadWatcher(myCapacitorApp); 38 | } 39 | 40 | // Run Application 41 | (async () => { 42 | // Wait for electron app to be ready. 43 | await app.whenReady(); 44 | // Security - Set Content-Security-Policy based on whether or not we are in dev mode. 45 | setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); 46 | // Initialize our app, build windows, and load content. 47 | await myCapacitorApp.init(); 48 | // Check for updates if we are in a packaged app. 49 | autoUpdater.checkForUpdatesAndNotify(); 50 | })(); 51 | 52 | // Handle when all of our windows are close (platforms have their own expectations). 53 | app.on('window-all-closed', function () { 54 | // On OS X it is common for applications and their menu bar 55 | // to stay active until the user quits explicitly with Cmd + Q 56 | if (process.platform !== 'darwin') { 57 | app.quit(); 58 | } 59 | }); 60 | 61 | // When the dock icon is clicked. 62 | app.on('activate', async function () { 63 | // On OS X it's common to re-create a window in the app when the 64 | // dock icon is clicked and there are no other windows open. 65 | if (myCapacitorApp.getMainWindow().isDestroyed()) { 66 | await myCapacitorApp.init(); 67 | } 68 | }); 69 | 70 | // Place all ipc or other electron api calls and custom functionality under this line 71 | -------------------------------------------------------------------------------- /src/electron-platform-template/src/preload.ts: -------------------------------------------------------------------------------- 1 | require('./rt/electron-rt'); 2 | ////////////////////////////// 3 | // User Defined Preload scripts below 4 | console.log('User Preload!'); 5 | -------------------------------------------------------------------------------- /src/electron-platform-template/src/rt/electron-plugins.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /src/electron-platform-template/src/rt/electron-rt.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { ipcRenderer, contextBridge } from 'electron'; 3 | import { EventEmitter } from 'events'; 4 | 5 | //////////////////////////////////////////////////////// 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const plugins = require('./electron-plugins'); 8 | 9 | const randomId = (length = 5) => randomBytes(length).toString('hex'); 10 | 11 | const contextApi: { 12 | [plugin: string]: { [functionName: string]: () => Promise }; 13 | } = {}; 14 | 15 | Object.keys(plugins).forEach((pluginKey) => { 16 | Object.keys(plugins[pluginKey]) 17 | .filter((className) => className !== 'default') 18 | .forEach((classKey) => { 19 | const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter( 20 | (v) => v !== 'constructor' 21 | ); 22 | 23 | if (!contextApi[classKey]) { 24 | contextApi[classKey] = {}; 25 | } 26 | 27 | functionList.forEach((functionName) => { 28 | if (!contextApi[classKey][functionName]) { 29 | contextApi[classKey][functionName] = (...args) => ipcRenderer.invoke(`${classKey}-${functionName}`, ...args); 30 | } 31 | }); 32 | 33 | // Events 34 | if (plugins[pluginKey][classKey].prototype instanceof EventEmitter) { 35 | const listeners: { [key: string]: { type: string; listener: (...args: any[]) => void } } = {}; 36 | const listenersOfTypeExist = (type) => 37 | !!Object.values(listeners).find((listenerObj) => listenerObj.type === type); 38 | 39 | Object.assign(contextApi[classKey], { 40 | addListener(type: string, callback: (...args) => void) { 41 | const id = randomId(); 42 | 43 | // Deduplicate events 44 | if (!listenersOfTypeExist(type)) { 45 | ipcRenderer.send(`event-add-${classKey}`, type); 46 | } 47 | 48 | const eventHandler = (_, ...args) => callback(...args); 49 | 50 | ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler); 51 | listeners[id] = { type, listener: eventHandler }; 52 | 53 | return id; 54 | }, 55 | removeListener(id: string) { 56 | if (!listeners[id]) { 57 | throw new Error('Invalid id'); 58 | } 59 | 60 | const { type, listener } = listeners[id]; 61 | 62 | ipcRenderer.removeListener(`event-${classKey}-${type}`, listener); 63 | 64 | delete listeners[id]; 65 | 66 | if (!listenersOfTypeExist(type)) { 67 | ipcRenderer.send(`event-remove-${classKey}-${type}`); 68 | } 69 | }, 70 | removeAllListeners(type: string) { 71 | Object.entries(listeners).forEach(([id, listenerObj]) => { 72 | if (!type || listenerObj.type === type) { 73 | ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener); 74 | ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`); 75 | delete listeners[id]; 76 | } 77 | }); 78 | }, 79 | }); 80 | } 81 | }); 82 | }); 83 | 84 | contextBridge.exposeInMainWorld('CapacitorCustomPlatform', { 85 | name: 'electron', 86 | plugins: contextApi, 87 | }); 88 | //////////////////////////////////////////////////////// 89 | -------------------------------------------------------------------------------- /src/electron-platform-template/src/setup.ts: -------------------------------------------------------------------------------- 1 | import type { CapacitorElectronConfig } from '@capacitor-community/electron'; 2 | import { 3 | CapElectronEventEmitter, 4 | CapacitorSplashScreen, 5 | setupCapacitorElectronPlugins, 6 | } from '@capacitor-community/electron'; 7 | import chokidar from 'chokidar'; 8 | import type { MenuItemConstructorOptions } from 'electron'; 9 | import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron'; 10 | import electronIsDev from 'electron-is-dev'; 11 | import electronServe from 'electron-serve'; 12 | import windowStateKeeper from 'electron-window-state'; 13 | import { join } from 'path'; 14 | 15 | // Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. 16 | const reloadWatcher = { 17 | debouncer: null, 18 | ready: false, 19 | watcher: null, 20 | }; 21 | export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void { 22 | reloadWatcher.watcher = chokidar 23 | .watch(join(app.getAppPath(), 'app'), { 24 | ignored: /[/\\]\./, 25 | persistent: true, 26 | }) 27 | .on('ready', () => { 28 | reloadWatcher.ready = true; 29 | }) 30 | .on('all', (_event, _path) => { 31 | if (reloadWatcher.ready) { 32 | clearTimeout(reloadWatcher.debouncer); 33 | reloadWatcher.debouncer = setTimeout(async () => { 34 | electronCapacitorApp.getMainWindow().webContents.reload(); 35 | reloadWatcher.ready = false; 36 | clearTimeout(reloadWatcher.debouncer); 37 | reloadWatcher.debouncer = null; 38 | reloadWatcher.watcher = null; 39 | setupReloadWatcher(electronCapacitorApp); 40 | }, 1500); 41 | } 42 | }); 43 | } 44 | 45 | // Define our class to manage our app. 46 | export class ElectronCapacitorApp { 47 | private MainWindow: BrowserWindow | null = null; 48 | private SplashScreen: CapacitorSplashScreen | null = null; 49 | private TrayIcon: Tray | null = null; 50 | private CapacitorFileConfig: CapacitorElectronConfig; 51 | private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ 52 | new MenuItem({ label: 'Quit App', role: 'quit' }), 53 | ]; 54 | private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ 55 | { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, 56 | { role: 'viewMenu' }, 57 | ]; 58 | private mainWindowState; 59 | private loadWebApp; 60 | private customScheme: string; 61 | 62 | constructor( 63 | capacitorFileConfig: CapacitorElectronConfig, 64 | trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[], 65 | appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[] 66 | ) { 67 | this.CapacitorFileConfig = capacitorFileConfig; 68 | 69 | this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron'; 70 | 71 | if (trayMenuTemplate) { 72 | this.TrayMenuTemplate = trayMenuTemplate; 73 | } 74 | 75 | if (appMenuBarMenuTemplate) { 76 | this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate; 77 | } 78 | 79 | // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. 80 | this.loadWebApp = electronServe({ 81 | directory: join(app.getAppPath(), 'app'), 82 | scheme: this.customScheme, 83 | }); 84 | } 85 | 86 | // Helper function to load in the app. 87 | private async loadMainWindow(thisRef: any) { 88 | await thisRef.loadWebApp(thisRef.MainWindow); 89 | } 90 | 91 | // Expose the mainWindow ref for use outside of the class. 92 | getMainWindow(): BrowserWindow { 93 | return this.MainWindow; 94 | } 95 | 96 | getCustomURLScheme(): string { 97 | return this.customScheme; 98 | } 99 | 100 | async init(): Promise { 101 | const icon = nativeImage.createFromPath( 102 | join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png') 103 | ); 104 | this.mainWindowState = windowStateKeeper({ 105 | defaultWidth: 1000, 106 | defaultHeight: 800, 107 | }); 108 | // Setup preload script path and construct our main window. 109 | const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); 110 | this.MainWindow = new BrowserWindow({ 111 | icon, 112 | show: false, 113 | x: this.mainWindowState.x, 114 | y: this.mainWindowState.y, 115 | width: this.mainWindowState.width, 116 | height: this.mainWindowState.height, 117 | webPreferences: { 118 | nodeIntegration: true, 119 | contextIsolation: true, 120 | // Use preload to inject the electron varriant overrides for capacitor plugins. 121 | // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), 122 | preload: preloadPath, 123 | }, 124 | }); 125 | this.mainWindowState.manage(this.MainWindow); 126 | 127 | if (this.CapacitorFileConfig.backgroundColor) { 128 | this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); 129 | } 130 | 131 | // If we close the main window with the splashscreen enabled we need to destory the ref. 132 | this.MainWindow.on('closed', () => { 133 | if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) { 134 | this.SplashScreen.getSplashWindow().close(); 135 | } 136 | }); 137 | 138 | // When the tray icon is enabled, setup the options. 139 | if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) { 140 | this.TrayIcon = new Tray(icon); 141 | this.TrayIcon.on('double-click', () => { 142 | if (this.MainWindow) { 143 | if (this.MainWindow.isVisible()) { 144 | this.MainWindow.hide(); 145 | } else { 146 | this.MainWindow.show(); 147 | this.MainWindow.focus(); 148 | } 149 | } 150 | }); 151 | this.TrayIcon.on('click', () => { 152 | if (this.MainWindow) { 153 | if (this.MainWindow.isVisible()) { 154 | this.MainWindow.hide(); 155 | } else { 156 | this.MainWindow.show(); 157 | this.MainWindow.focus(); 158 | } 159 | } 160 | }); 161 | this.TrayIcon.setToolTip(app.getName()); 162 | this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate)); 163 | } 164 | 165 | // Setup the main manu bar at the top of our window. 166 | Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate)); 167 | 168 | // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. 169 | if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { 170 | this.SplashScreen = new CapacitorSplashScreen({ 171 | imageFilePath: join( 172 | app.getAppPath(), 173 | 'assets', 174 | this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png' 175 | ), 176 | windowWidth: 400, 177 | windowHeight: 400, 178 | }); 179 | this.SplashScreen.init(this.loadMainWindow, this); 180 | } else { 181 | this.loadMainWindow(this); 182 | } 183 | 184 | // Security 185 | this.MainWindow.webContents.setWindowOpenHandler((details) => { 186 | if (!details.url.includes(this.customScheme)) { 187 | return { action: 'deny' }; 188 | } else { 189 | return { action: 'allow' }; 190 | } 191 | }); 192 | this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { 193 | if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { 194 | event.preventDefault(); 195 | } 196 | }); 197 | 198 | // Link electron plugins into the system. 199 | setupCapacitorElectronPlugins(); 200 | 201 | // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. 202 | this.MainWindow.webContents.on('dom-ready', () => { 203 | if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { 204 | this.SplashScreen.getSplashWindow().hide(); 205 | } 206 | if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { 207 | this.MainWindow.show(); 208 | } 209 | setTimeout(() => { 210 | if (electronIsDev) { 211 | this.MainWindow.webContents.openDevTools(); 212 | } 213 | CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); 214 | }, 400); 215 | }); 216 | } 217 | } 218 | 219 | // Set a CSP up for our application based on the custom scheme 220 | export function setupContentSecurityPolicy(customScheme: string): void { 221 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 222 | callback({ 223 | responseHeaders: { 224 | ...details.responseHeaders, 225 | 'Content-Security-Policy': [ 226 | electronIsDev 227 | ? `default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data:` 228 | : `default-src ${customScheme}://* 'unsafe-inline' data:`, 229 | ], 230 | }, 231 | }); 232 | }); 233 | } 234 | -------------------------------------------------------------------------------- /src/electron-platform-template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"], 4 | "compilerOptions": { 5 | "outDir": "./build", 6 | "importHelpers": true, 7 | "target": "ES2017", 8 | "module": "CommonJS", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "typeRoots": ["./node_modules/@types"], 12 | "allowJs": true, 13 | "rootDir": "." 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/electron-platform/ElectronDeepLinking.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | 3 | import type { ElectronCapacitorDeeplinkingConfig } from './definitions'; 4 | import { CapElectronEventEmitter } from './util'; 5 | 6 | export function setupElectronDeepLinking( 7 | capacitorElectronApp: any, 8 | config: ElectronCapacitorDeeplinkingConfig 9 | ): ElectronCapacitorDeeplinking { 10 | return new ElectronCapacitorDeeplinking(capacitorElectronApp, config); 11 | } 12 | 13 | export class ElectronCapacitorDeeplinking { 14 | private customProtocol = 'mycapacitorapp'; 15 | private lastPassedUrl: null | string = null; 16 | private customHandler: null | ((url: string) => void) = null; 17 | private capacitorAppRef: any = null; 18 | 19 | constructor(capacitorApp: any, config: ElectronCapacitorDeeplinkingConfig) { 20 | this.capacitorAppRef = capacitorApp; 21 | this.customProtocol = config.customProtocol; 22 | if (config.customHandler) this.customHandler = config.customHandler; 23 | 24 | CapElectronEventEmitter.on('CAPELECTRON_DeeplinkListenerInitialized', () => { 25 | if ( 26 | this.capacitorAppRef?.getMainWindow() && 27 | !this.capacitorAppRef.getMainWindow().isDestroyed() && 28 | this.lastPassedUrl !== null && 29 | this.lastPassedUrl.length > 0 30 | ) 31 | this.capacitorAppRef.getMainWindow().webContents.send('appUrlOpen', this.lastPassedUrl); 32 | this.lastPassedUrl = null; 33 | }); 34 | 35 | const instanceLock = app.requestSingleInstanceLock(); 36 | if (instanceLock) { 37 | app.on('second-instance', (_event, argv) => { 38 | if (process.platform === 'win32') { 39 | this.lastPassedUrl = argv.slice(1).toString(); 40 | this.internalHandler(this.lastPassedUrl); 41 | } 42 | if (!this.capacitorAppRef.getMainWindow().isDestroyed()) { 43 | if (this.capacitorAppRef.getMainWindow().isMinimized()) this.capacitorAppRef.getMainWindow().restore(); 44 | this.capacitorAppRef.getMainWindow().focus(); 45 | } else { 46 | this.capacitorAppRef.init(); 47 | } 48 | }); 49 | } else { 50 | app.quit(); 51 | } 52 | 53 | if (!app.isDefaultProtocolClient(this.customProtocol)) app.setAsDefaultProtocolClient(this.customProtocol); 54 | app.on('open-url', (event, url) => { 55 | event.preventDefault(); 56 | this.lastPassedUrl = url; 57 | this.internalHandler(url); 58 | if (this.capacitorAppRef?.getMainWindow()?.isDestroyed()) this.capacitorAppRef.init(); 59 | }); 60 | 61 | if (process.platform === 'win32') { 62 | this.lastPassedUrl = process.argv.slice(1).toString(); 63 | this.internalHandler(this.lastPassedUrl); 64 | } 65 | } 66 | 67 | private internalHandler(urlLink: string | null) { 68 | if (urlLink !== null && urlLink.length > 0) { 69 | const paramsArr = urlLink.split(','); 70 | let url = ''; 71 | for (const item of paramsArr) { 72 | if (item.indexOf(this.customProtocol) >= 0) { 73 | url = item; 74 | break; 75 | } 76 | } 77 | if (url.length > 0) { 78 | if (this.customHandler !== null && url !== null) this.customHandler(url); 79 | if (this.capacitorAppRef?.getMainWindow() && !this.capacitorAppRef.getMainWindow().isDestroyed()) 80 | this.capacitorAppRef.getMainWindow().webContents.send('appUrlOpen', url); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/electron-platform/ElectronSplashScreen.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import type Electron from 'electron'; 3 | import { app, BrowserWindow, ipcMain } from 'electron'; 4 | import { join } from 'path'; 5 | 6 | import type { SplashOptions } from './definitions'; 7 | import { encodeFromFile } from './util'; 8 | 9 | export class CapacitorSplashScreen { 10 | private splashWin: Electron.BrowserWindow | null = null; 11 | private splashOptions: SplashOptions = { 12 | imageFilePath: join(app.getAppPath(), 'assets', 'splash.png'), 13 | windowWidth: 400, 14 | windowHeight: 400, 15 | }; 16 | 17 | constructor(splashOptions?: SplashOptions) { 18 | if (splashOptions) this.splashOptions = { ...splashOptions }; 19 | 20 | ipcMain.on('showCapacitorSplashScreen', (_event: any, _options: any) => { 21 | this.splashWin!.show(); 22 | }); 23 | 24 | ipcMain.on('hideCapacitorSplashScreen', (_event: any, _options: any) => { 25 | this.splashWin!.hide(); 26 | }); 27 | } 28 | 29 | async init(loadMainWindowCallback: any, mainWindowThisRef: any): Promise { 30 | this.splashWin = new BrowserWindow({ 31 | width: this.splashOptions.windowWidth, 32 | height: this.splashOptions.windowHeight, 33 | frame: false, 34 | show: false, 35 | webPreferences: { 36 | webSecurity: true, 37 | }, 38 | resizable: false, 39 | }); 40 | 41 | let imageUrl = ''; 42 | try { 43 | imageUrl = await encodeFromFile(this.splashOptions.imageFilePath ?? ''); 44 | } catch (err) { 45 | imageUrl = `data:image/png;base64,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`; 46 | } 47 | 48 | const splashHtml = `
 
`; 49 | 50 | this.splashWin.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(splashHtml)}`); 51 | 52 | this.splashWin.webContents.on('dom-ready', async () => { 53 | this.splashWin!.show(); 54 | setTimeout(() => { 55 | loadMainWindowCallback(mainWindowThisRef); 56 | }, 1000); 57 | }); 58 | } 59 | 60 | getSplashWindow(): Electron.BrowserWindow { 61 | return this.splashWin!; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/electron-platform/definitions.ts: -------------------------------------------------------------------------------- 1 | import type { CapacitorConfig } from '@capacitor/cli'; 2 | 3 | export interface SplashOptions { 4 | imageFilePath?: string; 5 | windowWidth?: number; 6 | windowHeight?: number; 7 | } 8 | 9 | export interface ElectronCapacitorDeeplinkingConfig { 10 | customProtocol: string; 11 | customHandler?: (url: string) => void; 12 | } 13 | 14 | export interface ElectronConfig { 15 | customUrlScheme?: string; 16 | trayIconAndMenuEnabled?: boolean; 17 | splashScreenEnabled?: boolean; 18 | splashScreenImageName?: string; 19 | hideMainWindowOnLaunch?: boolean; 20 | deepLinkingEnabled?: boolean; 21 | deepLinkingCustomProtocol?: string; 22 | backgroundColor?: string; 23 | appId?: string; 24 | appName?: string; 25 | } 26 | 27 | export type CapacitorElectronConfig = CapacitorConfig & { 28 | electron?: ElectronConfig; 29 | }; 30 | -------------------------------------------------------------------------------- /src/electron-platform/index.ts: -------------------------------------------------------------------------------- 1 | import { setupElectronDeepLinking } from './ElectronDeepLinking'; 2 | import { CapacitorSplashScreen } from './ElectronSplashScreen'; 3 | import { CapElectronEventEmitter, getCapacitorElectronConfig, setupCapacitorElectronPlugins } from './util'; 4 | 5 | export * from './definitions'; 6 | 7 | export { 8 | CapacitorSplashScreen, 9 | CapElectronEventEmitter, 10 | getCapacitorElectronConfig, 11 | setupCapacitorElectronPlugins, 12 | setupElectronDeepLinking, 13 | }; 14 | -------------------------------------------------------------------------------- /src/electron-platform/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* eslint-disable @typescript-eslint/prefer-for-of */ 3 | import { app, ipcMain } from 'electron'; 4 | import EventEmitter from 'events'; 5 | import { existsSync, readFileSync } from 'fs'; 6 | import mimeTypes from 'mime-types'; 7 | import { join } from 'path'; 8 | 9 | import type { CapacitorElectronConfig } from './definitions'; 10 | 11 | class CapElectronEmitter extends EventEmitter {} 12 | let config: CapacitorElectronConfig = {}; 13 | 14 | export const CapElectronEventEmitter = new CapElectronEmitter(); 15 | 16 | export function deepMerge(target: any, _objects: any[] = []): any { 17 | // Credit for origanal function: Josh Cole(saikojosh)[https://github.com/saikojosh] 18 | const quickCloneArray = function (input: any) { 19 | return input.map(cloneValue); 20 | }; 21 | const cloneValue = function (value: any) { 22 | if (getTypeOf(value) === 'object') return quickCloneObject(value); 23 | else if (getTypeOf(value) === 'array') return quickCloneArray(value); 24 | return value; 25 | }; 26 | const getTypeOf = function (input: any) { 27 | if (input === null) return 'null'; 28 | else if (typeof input === 'undefined') return 'undefined'; 29 | else if (typeof input === 'object') return Array.isArray(input) ? 'array' : 'object'; 30 | return typeof input; 31 | }; 32 | const quickCloneObject = function (input: any) { 33 | const output: any = {}; 34 | for (const key in input) { 35 | // eslint-disable-next-line no-prototype-builtins 36 | if (!input.hasOwnProperty(key)) { 37 | continue; 38 | } 39 | output[key] = cloneValue(input[key]); 40 | } 41 | return output; 42 | }; 43 | const objects = _objects.map((object) => object || {}); 44 | const output = target || {}; 45 | for (let oindex = 0; oindex < objects.length; oindex++) { 46 | const object = objects[oindex]; 47 | const keys = Object.keys(object); 48 | for (let kindex = 0; kindex < keys.length; kindex++) { 49 | const key = keys[kindex]; 50 | const value = object[key]; 51 | const type = getTypeOf(value); 52 | const existingValueType = getTypeOf(output[key]); 53 | if (type === 'object') { 54 | if (existingValueType !== 'undefined') { 55 | const existingValue = existingValueType === 'object' ? output[key] : {}; 56 | output[key] = deepMerge({}, [existingValue, quickCloneObject(value)]); 57 | } else { 58 | output[key] = quickCloneObject(value); 59 | } 60 | } else if (type === 'array') { 61 | if (existingValueType === 'array') { 62 | const newValue = quickCloneArray(value); 63 | output[key] = newValue; 64 | } else { 65 | output[key] = quickCloneArray(value); 66 | } 67 | } else { 68 | output[key] = value; 69 | } 70 | } 71 | } 72 | return output; 73 | } 74 | 75 | export function pick(object: Record, keys: string[]): Record { 76 | return Object.fromEntries(Object.entries(object).filter(([key]) => keys.includes(key))); 77 | } 78 | 79 | export function deepClone(object: Record): Record { 80 | if (globalThis?.structuredClone) { 81 | return globalThis.structuredClone(object); 82 | } 83 | 84 | return JSON.parse(JSON.stringify(object)); 85 | } 86 | 87 | const pluginInstanceRegistry: { [pluginClassName: string]: { [functionName: string]: any } } = {}; 88 | 89 | export function setupCapacitorElectronPlugins(): void { 90 | console.log('in setupCapacitorElectronPlugins'); 91 | const rtPluginsPath = join(app.getAppPath(), 'build', 'src', 'rt', 'electron-plugins.js'); 92 | // eslint-disable-next-line @typescript-eslint/no-var-requires 93 | const plugins: { 94 | [pluginName: string]: { [className: string]: any }; 95 | } = require(rtPluginsPath); 96 | 97 | console.log(plugins); 98 | for (const pluginKey of Object.keys(plugins)) { 99 | console.log(`${pluginKey}`); 100 | for (const classKey of Object.keys(plugins[pluginKey]).filter((className) => className !== 'default')) { 101 | console.log(`-> ${classKey}`); 102 | 103 | if (!pluginInstanceRegistry[classKey]) { 104 | pluginInstanceRegistry[classKey] = new plugins[pluginKey][classKey](deepClone(config as Record)); 105 | } 106 | 107 | const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter( 108 | (v) => v !== 'constructor' 109 | ); 110 | 111 | for (const functionName of functionList) { 112 | console.log(`--> ${functionName}`); 113 | 114 | ipcMain.handle(`${classKey}-${functionName}`, (_event, ...args) => { 115 | console.log(`called ipcMain.handle: ${classKey}-${functionName}`); 116 | const pluginRef = pluginInstanceRegistry[classKey]; 117 | 118 | return pluginRef[functionName](...args); 119 | }); 120 | } 121 | 122 | // For every Plugin which extends EventEmitter, start listening for 'event-add-{classKey}' 123 | if (pluginInstanceRegistry[classKey] instanceof EventEmitter) { 124 | // Listen for calls about adding event listeners (types) to this particular class 125 | // This is only called by renderer when the first addListener of a particular type is requested 126 | ipcMain.on(`event-add-${classKey}`, (event, type) => { 127 | const eventHandler = (...data: any[]) => event.sender.send(`event-${classKey}-${type}`, ...data); 128 | 129 | (pluginInstanceRegistry[classKey] as EventEmitter).addListener(type, eventHandler); 130 | 131 | ipcMain.once(`event-remove-${classKey}-${type}`, () => { 132 | (pluginInstanceRegistry[classKey] as EventEmitter).removeListener(type, eventHandler); 133 | }); 134 | }); 135 | } 136 | } 137 | } 138 | } 139 | 140 | export async function encodeFromFile(filePath: string): Promise { 141 | if (!filePath) { 142 | throw new Error('filePath is required.'); 143 | } 144 | let mediaType: string | boolean = mimeTypes.lookup(filePath); 145 | if (mediaType === false) { 146 | throw new Error('Media type unrecognized.'); 147 | } else if (typeof mediaType !== 'string') { 148 | throw new Error('Media type is not string.'); 149 | } 150 | const fileData = readFileSync(filePath); 151 | mediaType = /\//.test(mediaType) ? mediaType : 'image/' + mediaType; 152 | const dataBase64 = Buffer.isBuffer(fileData) ? fileData.toString('base64') : Buffer.from(fileData).toString('base64'); 153 | return 'data:' + mediaType + ';base64,' + dataBase64; 154 | } 155 | 156 | export function getCapacitorElectronConfig(): CapacitorElectronConfig { 157 | let capFileConfig: any = {}; 158 | if (existsSync(join(app.getAppPath(), 'build', 'capacitor.config.js'))) { 159 | capFileConfig = require(join(app.getAppPath(), 'build', 'capacitor.config.js')).default; 160 | } else { 161 | capFileConfig = JSON.parse(readFileSync(join(app.getAppPath(), 'capacitor.config.json')).toString()); 162 | } 163 | if (capFileConfig.electron) config = deepMerge(config, [capFileConfig]); 164 | return deepClone(config as Record) as CapacitorElectronConfig; 165 | } 166 | --------------------------------------------------------------------------------