├── .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 |
18 |
19 |
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 |
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 |
--------------------------------------------------------------------------------