Compare commits

..

108 Commits

Author SHA1 Message Date
Florian Rappl
a0c0523c7d Change default framework version to net10.0
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-07 09:18:27 +02:00
Florian Rappl
6d94cf9538 Improved template 2026-09-07 08:52:56 +02:00
Florian Rappl
521e7c935a Removed references to electronize 2026-09-07 08:25:18 +02:00
Florian Rappl
730eacbd52 Improved templates for preview version 2026-09-04 21:06:47 +02:00
Florian Rappl
deca8166ec Added template #414 2026-09-04 20:44:55 +02:00
Florian Rappl
a04ca0b9bc Completed APIs from #956 2026-09-04 16:57:44 +02:00
Florian Rappl
fffd7dfb79 Added more missing APIs 2026-09-04 16:51:51 +02:00
Florian Rappl
98c72af3d6 Updated zoom level 2026-09-04 16:03:43 +02:00
Florian Rappl
703cf8f880 Updated to use nsis on Windows 2026-09-04 15:48:29 +02:00
Florian Rappl
248d4bd664 Updated and improved migration checks #946 2026-09-04 15:19:07 +02:00
Florian Rappl
d5da1dbc9b Fixed #1029 2026-09-04 15:05:55 +02:00
Florian Rappl
672ff6c40d Implemented #1003 2026-09-04 14:29:38 +02:00
Florian Rappl
7cd552a78f Finished #1106 2026-09-04 14:05:36 +02:00
Florian Rappl
042d8e6c70 Infra enhancements 2026-09-04 13:26:27 +02:00
Florian Rappl
0140749753 Fixed #1103 2026-09-04 11:52:21 +02:00
Florian Rappl
7af2e954c7 Fixed 1040 2026-09-04 11:37:42 +02:00
Florian Rappl
da0e1f17db Fixes #1097 2026-09-03 17:04:21 +02:00
Florian Rappl
9cb210040d Fixed #1105 2026-09-03 16:25:47 +02:00
Florian Rappl
7e8f45b16d Updated changelog 2026-09-03 16:17:19 +02:00
Florian Rappl
76dc687269 Merged 2026-09-03 11:28:37 +02:00
Florian Rappl
dcf4585510 Merge pull request #1108 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/humanfs/node-0.16.8
Bump @humanfs/node from 0.16.7 to 0.16.8 in /src/ElectronNET.WebApp/ElectronHostHook
2026-09-03 08:54:37 +02:00
dependabot[bot]
a6d7434adf Bump @humanfs/node in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 00:19:56 +00:00
Florian Rappl
76ff44f67e Merge pull request #1104 from epsitec-sa/feat/#1095
chore(build): allow customization of target framwork version in dev mode (#1095)
2026-08-14 13:07:48 +02:00
Samuel Nussbaum
20ab82b16d chore(build): fix error when props file is missing 2026-08-14 11:25:19 +02:00
Samuel Nussbaum
b27f705a6a chore(build): Change import barriere 2026-08-14 10:52:10 +02:00
Samuel Nussbaum
8697c4de44 chore(build): passthrough props barriere 2026-08-14 10:07:58 +02:00
Florian Rappl
376a791e48 Merge pull request #1099 from epsitec-sa/bug/#1096
chore(build): fix #1096 by depends on resolve unrelated target
2026-08-11 09:45:16 +02:00
Florian Rappl
195a29052e Merge pull request #1100 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/js-yaml-4.3.1
Bump js-yaml from 4.3.0 to 4.3.1 in /src/ElectronNET.WebApp/ElectronHostHook
2026-08-11 09:31:59 +02:00
Florian Rappl
f577a53227 Merge pull request #1102 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/electron-39.8.10
Bump electron from 30.5.1 to 39.8.10 in /src/ElectronNET.Host
2026-08-11 08:52:04 +02:00
Florian Rappl
382d544468 Merge pull request #1101 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/brace-expansion-1.1.18
Bump brace-expansion from 1.1.14 to 1.1.18 in /src/ElectronNET.WebApp/ElectronHostHook
2026-08-11 08:51:42 +02:00
dependabot[bot]
18f08f9aff Bump electron from 30.5.1 to 39.8.10 in /src/ElectronNET.Host
Bumps [electron](https://github.com/electron/electron) from 30.5.1 to 39.8.10.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v30.5.1...v39.8.10)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 39.8.10
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:17:12 +00:00
dependabot[bot]
def289ab54 Bump brace-expansion in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.14 to 1.1.18.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.18)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:17:02 +00:00
dependabot[bot]
4011f3d30b Bump js-yaml in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:17:01 +00:00
Samuel Nussbaum
26b9d86ee4 chore(build): target frameworks customization 2026-08-10 10:33:00 +02:00
Florian Rappl
3b57de976a Merge pull request #1098 from epsitec-sa/bug/#1097
fix(packaging): Restore cross-compile on same platform and packaging dir name behavior
2026-08-10 10:19:45 +02:00
Samuel Nussbaum
d9e9b3d2e6 fix(packaging): Architecture must be defined in electron-builder compile args in order to allow cross-compile on same platform. Enforce default-arch to be the default arch in order to avoid arch in package directory name. 2026-08-10 09:22:21 +02:00
Samuel Nussbaum
1ec1fde3c6 chore(build): fix #1096 by depends on not resolve related target 2026-08-07 14:37:13 +02:00
Florian Rappl
e5ccf75c9f Merge pull request #1094 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/ElectronHostHook/socket.io-parser-4.2.7
Bump socket.io-parser from 4.2.6 to 4.2.7 in /src/ElectronNET.Host/ElectronHostHook
2026-08-05 09:08:03 +02:00
dependabot[bot]
819e643ab8 Bump socket.io-parser in /src/ElectronNET.Host/ElectronHostHook
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.6 to 4.2.7.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.6...socket.io-parser@4.2.7)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 05:07:46 +00:00
Florian Rappl
7fe3f9b5b6 Merge pull request #1093 from ElectronNET/develop
Release 0.5.2
2026-08-03 14:53:48 +02:00
Florian Rappl
16cbc5f2a4 Improved platform agnostic for test 2026-08-03 14:36:20 +02:00
Florian Rappl
3a2c997e6a Ensure windows are not retained after being destroyed #1008 2026-08-03 14:25:47 +02:00
Florian Rappl
f88f5f8b18 Added tests, improved icon implementation 2026-08-03 14:19:25 +02:00
Florian Rappl
4e39f5f377 Added support for MacOS modern app icon #1047 2026-08-03 14:12:09 +02:00
Florian Rappl
7fa5ec5b0b Fixed #1050 2026-08-03 14:09:01 +02:00
Florian Rappl
2f8f717dbb Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-03 11:13:32 +02:00
Florian Rappl
525d6185fb Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-03 11:13:08 +02:00
Florian Rappl
73703819be Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-03 11:12:56 +02:00
Florian Rappl
09249f74c6 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-03 11:12:41 +02:00
Florian Rappl
cfd8be36f3 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-03 11:12:19 +02:00
Florian Rappl
6055635eb5 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-03 11:11:15 +02:00
Florian Rappl
74a734ae58 Merge pull request #1092 from epsitec-sa/bug/#1091
Bug/#1091 - Fix CA1416 on AspNet project
2026-07-29 11:05:29 +02:00
Samuel Nussbaum
d4402d1040 chore(build): Apply same NoWarn regardless of current runtime, platform or architecture in order to avoid compile issue when used in development mode 2026-07-29 09:47:00 +02:00
Florian Rappl
4b65ed26a4 Merge pull request #1089 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/flatted-3.4.3
Bump flatted from 3.3.3 to 3.4.3 in /src/ElectronNET.WebApp/ElectronHostHook
2026-07-28 09:07:22 +02:00
Florian Rappl
e12145004e Merge pull request #1090 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/socket.io-parser-4.2.7
Bump socket.io-parser from 4.2.5 to 4.2.7 in /src/ElectronNET.Host
2026-07-28 09:07:11 +02:00
Florian Rappl
3e8ebde05b Merge pull request #1088 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/multi-32449639ce
Bump builder-util-runtime and electron-updater in /src/ElectronNET.Host
2026-07-28 09:06:58 +02:00
Florian Rappl
820d7bbcec Merge pull request #1087 from epsitec-sa/bug/#1084
Bug/#1084 - Visual studio loading issue
2026-07-28 09:05:52 +02:00
Samuel Nussbaum
5eeab1bd24 fix(warning): remove warining CS4014 2026-07-28 06:54:48 +02:00
Roger Vuistiner
1fff688098 chore: consolidate MSBuild configuration barriers
- Moved Directory.Build.* barriers from nuke to root folder to cover all projects.
- Added a Directory.Packages.props barrier to stop central package management.
- Removed obsolete 'ManagePackageVersionsCentrally' properties.
2026-07-28 06:54:43 +02:00
dependabot[bot]
9b701f5258 Bump socket.io-parser from 4.2.5 to 4.2.7 in /src/ElectronNET.Host
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.5 to 4.2.7.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.5...socket.io-parser@4.2.7)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 03:41:41 +00:00
dependabot[bot]
b41c7e51b1 Bump flatted in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.3.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.3)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 03:41:32 +00:00
dependabot[bot]
1d24067f4b Bump builder-util-runtime and electron-updater in /src/ElectronNET.Host
Bumps [builder-util-runtime](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/builder-util-runtime) to 9.7.0 and updates ancestor dependency [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater). These dependencies need to be updated together.


Updates `builder-util-runtime` from 9.5.1 to 9.7.0
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/builder-util-runtime/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/HEAD/packages/builder-util-runtime)

Updates `electron-updater` from 6.8.3 to 6.8.9
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.8.9/packages/electron-updater)

---
updated-dependencies:
- dependency-name: builder-util-runtime
  dependency-version: 9.7.0
  dependency-type: indirect
- dependency-name: electron-updater
  dependency-version: 6.8.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 03:41:30 +00:00
Samuel Nussbaum
44e5c4e230 chore(build): runtime identifier is not required anymore 2026-07-27 16:45:52 +02:00
Samuel Nussbaum
0b3053477d chore(dx): visual studio load regression, let electron choose the required runtime. 2026-07-27 16:45:52 +02:00
Samuel Nussbaum
3fecc0ee1b fix: Visual studio load issue 2026-07-27 16:45:51 +02:00
Florian Rappl
95089e1721 Merge pull request #1085 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/js-yaml-4.3.0
Bump js-yaml from 4.1.1 to 4.3.0 in /src/ElectronNET.WebApp/ElectronHostHook
2026-07-22 07:12:18 +02:00
Florian Rappl
cec178cf50 Merge pull request #1086 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/brace-expansion-1.1.16
Bump brace-expansion from 1.1.12 to 1.1.16 in /src/ElectronNET.Host
2026-07-22 07:12:07 +02:00
dependabot[bot]
20af8a692c Bump brace-expansion from 1.1.12 to 1.1.16 in /src/ElectronNET.Host
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.16.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.16)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-22 02:19:22 +00:00
dependabot[bot]
4e69bcf030 Bump js-yaml in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-22 02:19:18 +00:00
Florian Rappl
5ae5bf0df7 Merge pull request #1083 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/js-yaml-4.3.0
Bump js-yaml from 4.1.1 to 4.3.0 in /src/ElectronNET.Host
2026-07-15 07:12:13 +02:00
dependabot[bot]
5da5d1c618 Bump js-yaml from 4.1.1 to 4.3.0 in /src/ElectronNET.Host
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-14 22:27:36 +00:00
Florian Rappl
b6e496b2c6 Merge pull request #1082 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/js-yaml-4.3.0
Bump js-yaml from 4.1.1 to 4.3.0 in /src/ElectronNET.WebApp/ElectronHostHook
2026-07-09 15:33:25 +02:00
Florian Rappl
5974cb9e43 Merge branch 'develop' into dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/js-yaml-4.3.0 2026-07-09 10:42:03 +02:00
dependabot[bot]
d4d4e9dddd Bump js-yaml in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-08 23:02:16 +00:00
Florian Rappl
3b5f21c724 Merge pull request #1081 from epsitec-sa/bug/default-runtime
chore: use default sdk runtime identifier
2026-07-06 15:17:02 +02:00
Samuel Nussbaum
5aebe807c3 change to less brittle solution 2026-07-06 11:02:48 +02:00
Samuel Nussbaum
bdee008cc8 use default sdk runtime identifier 2026-07-06 10:44:05 +02:00
Florian Rappl
7fc5c9b611 Merge pull request #1080 from epsitec-sa/bug/#1058
fix(shore): bloated asar and corrupted packaged directory
2026-07-05 11:08:43 +02:00
Samuel Nussbaum
86e5d74c03 dump to latest version 2026-07-03 15:24:49 +02:00
Samuel Nussbaum
e79b029e16 Fix scoped css naming issue 2026-07-03 15:09:49 +02:00
Samuel Nussbaum
5bdf6230b9 fix examples build and publish 2026-07-03 15:08:57 +02:00
Samuel Nussbaum
8c2382a05c Split packaging to avoid duplication in .asar archive 2026-07-03 11:28:52 +02:00
Florian Rappl
10b1b686cb Merge pull request #1079 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/js-yaml-4.2.0
Bump js-yaml from 4.1.1 to 4.2.0 in /src/ElectronNET.WebApp/ElectronHostHook
2026-06-24 14:51:00 +02:00
Florian Rappl
d7d2521d0c Merge pull request #1078 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/multi-5f1280885e
Bump ws, engine.io and socket.io-adapter in /src/ElectronNET.Host
2026-06-24 14:50:35 +02:00
Florian Rappl
392419e5a7 Merge branch 'develop' into dependabot/npm_and_yarn/src/ElectronNET.Host/multi-5f1280885e 2026-06-24 14:50:10 +02:00
dependabot[bot]
f92f07a378 Bump js-yaml in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-24 07:11:36 +00:00
dependabot[bot]
9b045cbf01 Bump ws, engine.io and socket.io-adapter in /src/ElectronNET.Host
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/socket.io) and [socket.io-adapter](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.20.1 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.20.1...8.21.0)

Updates `engine.io` from 6.6.8 to 6.6.9
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io@6.6.8...engine.io@6.6.9)

Updates `socket.io-adapter` from 2.5.7 to 2.5.8
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.7...socket.io-adapter@2.5.8)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
- dependency-name: engine.io
  dependency-version: 6.6.9
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-version: 2.5.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-24 03:30:33 +00:00
Florian Rappl
94ff3c1c4d Updated token usage 2026-06-22 23:23:44 +02:00
Florian Rappl
773937448d Updated version 2026-06-22 23:23:34 +02:00
Florian Rappl
b094051dcb Merge pull request #1077 from AeonSake/1076-add-overloads-for-useelectron
Add Overloads for `UseElectron`
2026-06-21 21:46:56 +02:00
Aeon
3a10e7e98d Add missing null-check 2026-06-20 01:38:58 +02:00
Aeon
056661028b Remove unused property 2026-06-20 01:26:55 +02:00
Aeon
27a8c69a9a Add missing extension method overloads 2026-06-20 01:26:25 +02:00
Aeon
8e900655b9 Replace static property with resolver service 2026-06-20 01:26:00 +02:00
Aeon
066efa80e2 Add service for resolving on-app-ready callback 2026-06-20 01:24:30 +02:00
Florian Rappl
0e83af032c Merge branch 'develop' of https://github.com/ElectronNET/Electron.NET into develop 2026-06-07 14:48:15 +02:00
Florian Rappl
1ec84cfef0 Use path directly 2026-06-07 14:48:07 +02:00
Florian Rappl
1620c50627 Merge pull request #1071 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/multi-8c9a216550
Bump ws, engine.io and socket.io-adapter in /src/ElectronNET.WebApp/ElectronHostHook
2026-06-07 13:44:13 +02:00
Florian Rappl
295b250e58 Merge pull request #1070 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/multi-8c9a216550
Bump ws, engine.io and socket.io-adapter in /src/ElectronNET.Host
2026-06-07 13:44:04 +02:00
Florian Rappl
644628c803 Merge pull request #1069 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.WebApp/ElectronHostHook/multi-19724e71a7
Bump tmp and exceljs in /src/ElectronNET.WebApp/ElectronHostHook
2026-06-07 13:43:45 +02:00
Florian Rappl
a18944d4a4 Removed wrongly placed line 2026-06-07 13:41:21 +02:00
Florian Rappl
b5858a697a Added support for Electron 42+ 2026-06-07 13:22:25 +02:00
Florian Rappl
58b9ad0125 Improved argument forwarding 2026-06-06 21:10:19 +02:00
dependabot[bot]
e44927a655 Bump ws, engine.io and socket.io-adapter
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/socket.io) and [socket.io-adapter](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.18.3 to 8.20.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.20.1)

Updates `engine.io` from 6.6.5 to 6.6.8
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io@6.6.5...engine.io@6.6.8)

Updates `socket.io-adapter` from 2.5.6 to 2.5.7
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.6...socket.io-adapter@2.5.7)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
- dependency-name: engine.io
  dependency-version: 6.6.8
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-version: 2.5.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 21:29:26 +00:00
dependabot[bot]
ed9a24bb5e Bump ws, engine.io and socket.io-adapter in /src/ElectronNET.Host
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/socket.io) and [socket.io-adapter](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.18.3 to 8.20.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.20.1)

Updates `engine.io` from 6.6.5 to 6.6.8
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io@6.6.5...engine.io@6.6.8)

Updates `socket.io-adapter` from 2.5.6 to 2.5.7
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.6...socket.io-adapter@2.5.7)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
- dependency-name: engine.io
  dependency-version: 6.6.8
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-version: 2.5.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 03:59:46 +00:00
dependabot[bot]
94df47efa6 Bump tmp and exceljs in /src/ElectronNET.WebApp/ElectronHostHook
Bumps [tmp](https://github.com/raszi/node-tmp) to 0.2.7 and updates ancestor dependency [exceljs](https://github.com/exceljs/exceljs). These dependencies need to be updated together.


Updates `tmp` from 0.1.0 to 0.2.7
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.1.0...v0.2.7)

Updates `exceljs` from 1.15.0 to 4.4.0
- [Release notes](https://github.com/exceljs/exceljs/releases)
- [Commits](https://github.com/exceljs/exceljs/compare/v1.15.0...v4.4.0)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.7
  dependency-type: indirect
- dependency-name: exceljs
  dependency-version: 4.4.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 00:35:30 +00:00
Florian Rappl
d9f3da87a8 Merge pull request #1068 from ElectronNET/dependabot/npm_and_yarn/src/ElectronNET.Host/ElectronHostHook/multi-8c9a216550
Bump ws, engine.io and socket.io-adapter in /src/ElectronNET.Host/ElectronHostHook
2026-05-25 22:22:22 +02:00
dependabot[bot]
a33d76ae2f Bump ws, engine.io and socket.io-adapter
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/socket.io) and [socket.io-adapter](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.17.1 to 8.20.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.1...8.20.1)

Updates `engine.io` from 6.6.4 to 6.6.8
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io@6.6.4...engine.io@6.6.8)

Updates `socket.io-adapter` from 2.5.5 to 2.5.7
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.5...socket.io-adapter@2.5.7)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
- dependency-name: engine.io
  dependency-version: 6.6.8
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-version: 2.5.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 20:11:16 +00:00
98 changed files with 3518 additions and 358 deletions

View File

@@ -1,3 +1,45 @@
# 0.6.0
## ElectronNET.Core
- Updated dependencies
- Updated `WebContents` zoom level APIs to use `double` instead of `int` (#956)
- Improved migration checks to honor custom output paths and to detect `ProjectGuid` in publish profiles (#946)
- Fixed single instance handling on macOS (#1040)
- Fixed slow socket bridge startup by binding to an explicit loopback address (#1103)
- Fixed socket bridge connection when a system proxy is configured (#1105)
- Fixed electron-builder using the host RID instead of the target RID (#1097)
- Fixed cross-compilation behavior on same platform (#1098) @epsnm
- Fixed resolution of unrelated target (#1099) @epsnm
- Fixed false alarm for `ELECTRON001` on a root `package-lock.json` (#946)
- Added `ElectronNET.Core.Templates` package with a `dotnet new electron-blazor` template (#414)
- Added `WebContents.OnZoomChanged` event (#956)
- Added `WebContents` page loading APIs `LoadFileAsync`, `IsLoadingAsync`, `IsLoadingMainFrameAsync`, `IsWaitingForResponseAsync`, `Reload`, `ReloadIgnoringCache` and `Stop` (#956)
- Added `WebContents.InsertCSSAsync` and `WebContents.RemoveInsertedCSSAsync` for dynamic CSS (#956)
- Added `WebContents` editing and selection APIs (undo, redo, cut, copy, paste, insert text, select, ...) (#956)
- Added `WebContents.FindInPageAsync`, `WebContents.StopFindInPage` and the `OnFoundInPage` event (#956)
- Added `WebContents` audio events `OnAudioStateChanged`, `OnMediaStartedPlaying` and `OnMediaPaused` (#956)
- Added `WebContents.ScrollToTop` and `WebContents.ScrollToBottom` (#956)
- Added target framework customization (#1095) @epsnm
- Added configurable Electron root directory for custom packaging layouts (#1106) @DYH1319
- Added ability for `custom_main.js` to modify command line switches (#1029) @AeonSake
- Added migration checks for incomplete `ElectronHostHook` folders (`ELECTRON010`, `ELECTRON011`) (#946)
- Added `ElectronExecutableName` to separate the product name from the executable name (#1003) @AeonSake
- Added build extensibility properties `ElectronSkipExecCommands` and `ElectronIntermediatePublishDir` (#1106) @DYH1319
# 0.5.2
## ElectronNET.Core
- Fixed token param being appended to external URLs (#1075)
- Fixed startup mode discriminator in case of existing `wwwroot` (#1050)
- Fixed CA1416 on ASP.NET project (#1091) @epsnm
- Fixed loading issue in plain Visual Studio (#1084) @epsnm
- Fixed bloated ASAR file (#1080) @epsnm
- Improved selection of runtime identifier (#1081) @epsnm
- Added more variants for `UseElectron` (#1076) @AeonSake
- Added support for modern MacOS app icon (#1047)
# 0.5.1 # 0.5.1
## ElectronNET.Core ## ElectronNET.Core

15
Directory.Build.props Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.props file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />-->
<PropertyGroup>
<ElectronNetBuildProps>$([MSBuild]::GetPathOfFileAbove('ElectronNet.Build.props', '$(MSBuildThisFileDirectory)../'))</ElectronNetBuildProps>
</PropertyGroup>
<!-- Only import specific project related MSBuild file -->
<Import Project="$(ElectronNetBuildProps)" Condition=" '$(ElectronNetBuildProps)' != '' " />
</Project>

View File

@@ -2,7 +2,7 @@
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files --> <!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.props file --> <!-- Uncomment to include parent Directory.Packages.props file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />--> <!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" />-->
</Project> </Project>

View File

@@ -23,13 +23,14 @@ While the classic Electron.NET setup (using an ASP.NET host run by the Electron
* ElectronNET.Core: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.API/) * ElectronNET.Core: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.API/)
* ElectronNET.Core.API: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.API.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.API/) * ElectronNET.Core.API: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.API.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.API/)
* ElectronNET.Core.AspNet: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.AspNet.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.AspNet/) * ElectronNET.Core.AspNet: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.AspNet.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.AspNet/)
* ElectronNET.Core.Templates: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.Templates.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.Templates/)
## 🛠 Requirements to Run ## 🛠 Requirements to Run
You should have installed: You should have installed:
* .NET 6/8 or later. * .NET 8/10 or later.
* The minimum base OS is the same as [.NET 6](https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md) / [.NET 8](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md). * The minimum base OS is the same as [.NET 8](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md) / [.NET 10](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md).
* Node.JS using at least [Version 22.x](https://nodejs.org). * Node.JS using at least [Version 22.x](https://nodejs.org).
## 👩‍🏫 Usage with ASP.NET ## 👩‍🏫 Usage with ASP.NET
@@ -202,6 +203,8 @@ Alternatively, consider using a GitHub sponsorship for the core maintainers:
- [Gregor Biswanger](https://github.com/sponsors/GregorBiswanger) - [Gregor Biswanger](https://github.com/sponsors/GregorBiswanger)
- [Florian Rappl](https://github.com/sponsors/FlorianRappl) - [Florian Rappl](https://github.com/sponsors/FlorianRappl)
Or just [buy Florian a coffee](https://buymeacoffee.com/florianrappl) :coffee:.
Any support appreciated! 🍻 Any support appreciated! 🍻
## 🎉 License ## 🎉 License

View File

@@ -58,6 +58,23 @@ Works for both BrowserWindows and BrowserViews.
- `isBrowserWindow` - Whether the webContents belong to a BrowserWindow or not (the other option is a BrowserView) - `isBrowserWindow` - Whether the webContents belong to a BrowserWindow or not (the other option is a BrowserView)
- `path` - Absolute path to the CSS file location - `path` - Absolute path to the CSS file location
#### 🧊 `Task<string> InsertCSSAsync(string css, string cssOrigin = null)`
Injects CSS into the current web page and returns a unique key for the inserted style sheet.
**Parameters:**
- `css` - The style sheet to inject
- `cssOrigin` - Can be either `user` or `author`, defaults to `author`
**Returns:**
The key of the inserted style sheet, to be used with `RemoveInsertedCSSAsync`.
#### 🧊 `Task RemoveInsertedCSSAsync(string key)`
Removes a previously inserted style sheet from the current web page.
**Parameters:**
- `key` - The key returned by `InsertCSSAsync`
#### 🧊 `Task LoadURLAsync(string url)` #### 🧊 `Task LoadURLAsync(string url)`
Loads the url in the window. The url must contain the protocol prefix. Loads the url in the window. The url must contain the protocol prefix.
@@ -113,6 +130,106 @@ Prints window's web page as PDF with Chromium's preview printing custom settings
Whether the PDF generation succeeded. Whether the PDF generation succeeded.
#### 🧊 `Task<double> GetZoomFactorAsync()`
Returns the current zoom factor. A factor of `1.0` means 100%.
#### 🧊 `void SetZoomFactor(double factor)`
Changes the zoom factor to the specified factor. The zoom factor is the zoom percent divided by 100, so 300% = `3.0`. The factor must be greater than `0.0`.
**Parameters:**
- `factor` - The zoom factor
#### 🧊 `Task<double> GetZoomLevelAsync()`
Returns the current zoom level.
#### 🧊 `void SetZoomLevel(double level)`
Changes the zoom level to the specified level. The original size is `0` and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of the original size, respectively.
**Parameters:**
- `level` - The zoom level
#### 🧊 `Task SetVisualZoomLevelLimitsAsync(double minimumLevel, double maximumLevel)`
Sets the maximum and minimum pinch-to-zoom level.
**Parameters:**
- `minimumLevel` - The minimum pinch-to-zoom level
- `maximumLevel` - The maximum pinch-to-zoom level
#### 🧊 `Task LoadFileAsync(string filePath, LoadFileOptions options = null)`
Loads the given HTML file, relative to the root of the application.
**Parameters:**
- `filePath` - Path to the HTML file
- `options` - Optional `Query`, `Search` and `Hash` parts of the resulting URL
#### 🧊 `void SetAudioMuted(bool muted)`
Mutes or unmutes the audio on the current web page.
#### 🧊 `Task<bool> IsAudioMutedAsync()`
Whether this page has been muted.
#### 🧊 `Task<bool> IsCurrentlyAudibleAsync()`
Whether audio is currently playing.
#### 🧊 `Task<string> GetUserAgentAsync()` / `void SetUserAgent(string userAgent)`
Gets or overrides the user agent for this web page.
#### 🧊 `Task<bool> IsLoadingAsync()`
Whether the web page is still loading resources.
#### 🧊 `Task<bool> IsLoadingMainFrameAsync()`
Whether the main frame (and not just iframes or frames within it) is still loading.
#### 🧊 `Task<bool> IsWaitingForResponseAsync()`
Whether the web page is waiting for a first response from the main resource of the page.
#### 🧊 `void Reload()`
Reloads the current web page.
#### 🧊 `void ReloadIgnoringCache()`
Reloads the current web page and ignores the cache.
#### 🧊 `void Stop()`
Stops any pending navigation.
#### 🧊 `void Undo()` / `void Redo()`
Executes the `undo` / `redo` editing command in the web page.
#### 🧊 `void Cut()` / `void Copy()` / `void Paste()` / `void PasteAndMatchStyle()` / `void Delete()`
Executes the corresponding editing command in the web page.
#### 🧊 `void CopyImageAt(int x, int y)`
Copies the image at the given position to the clipboard.
#### 🧊 `void SelectAll()` / `void Unselect()` / `void CenterSelection()`
Selects all content, clears the selection, or scrolls to the current selection.
#### 🧊 `void ScrollToTop()` / `void ScrollToBottom()`
Scrolls to the top or the bottom of the current web page.
#### 🧊 `void AdjustSelection(AdjustSelectionOptions options)`
Adjusts the start and end points of the current text selection by the given amounts. Negative amounts move towards the beginning of the document.
#### 🧊 `Task InsertTextAsync(string text)`
Inserts text into the focused element.
#### 🧊 `void Replace(string text)` / `void ReplaceMisspelling(string text)`
Replaces the current selection, or the currently misspelled word, with the given text.
#### 🧊 `Task<int> FindInPageAsync(string text, FindInPageOptions options = null)`
Starts a request to find all matches for the text in the web page. Results are reported via the `OnFoundInPage` event.
**Parameters:**
- `text` - Content to be searched, must not be empty
- `options` - Optional `Forward`, `FindNext` and `MatchCase` flags
**Returns:**
The request id of the find request.
#### 🧊 `void StopFindInPage(StopFindInPageAction action)`
Stops any `FindInPageAsync` request with the given action (`ClearSelection`, `KeepSelection` or `ActivateSelection`).
## Events ## Events
#### ⚡ `InputEvent` #### ⚡ `InputEvent`
@@ -142,6 +259,21 @@ Emitted when the document in the top-level frame is loaded.
#### ⚡ `OnWillRedirect` #### ⚡ `OnWillRedirect`
Emitted when a server side redirect occurs during navigation. Emitted when a server side redirect occurs during navigation.
#### ⚡ `OnZoomChanged`
Emitted when the user changes the zoom level using the mouse wheel or the keyboard. The handler receives the `ZoomDirection` (`In` or `Out`).
#### ⚡ `OnFoundInPage`
Emitted when a result is available for a `FindInPageAsync` request. The handler receives a `FoundInPageResult`.
#### ⚡ `OnAudioStateChanged`
Emitted when media becomes audible or inaudible. The handler receives `true` if one or more frames or child web contents are emitting audio.
#### ⚡ `OnMediaStartedPlaying`
Emitted when media starts playing.
#### ⚡ `OnMediaPaused`
Emitted when media is paused or done playing.
## Usage Examples ## Usage Examples
### Page Loading ### Page Loading
@@ -279,6 +411,82 @@ webContents.OnCrashed += (killed) =>
}; };
``` ```
### Zoom Control
```csharp
// Set the zoom to 150%
webContents.SetZoomFactor(1.5);
var factor = await webContents.GetZoomFactorAsync();
Console.WriteLine($"Current zoom: {factor * 100}%");
// Zoom levels are relative: each step is 20% larger/smaller, 0 is the original size
webContents.SetZoomLevel(2);
// Restrict pinch-to-zoom
await webContents.SetVisualZoomLevelLimitsAsync(1, 3);
// React to zoom changes triggered by the user
webContents.OnZoomChanged += (direction) =>
{
Console.WriteLine($"User zoomed {direction}");
};
```
> **Note:** The zoom factor is shared by all windows using the same session partition. Assign a unique `WebPreferences.Partition` per window if each window should keep its own zoom.
### Dynamic CSS
```csharp
var key = await webContents.InsertCSSAsync("body { background-color: #202020; }");
// ... later
await webContents.RemoveInsertedCSSAsync(key);
```
### Editing and Selection
```csharp
await webContents.InsertTextAsync("Hello from .NET");
webContents.SelectAll();
webContents.Copy();
webContents.Unselect();
```
### Find in Page
```csharp
webContents.OnFoundInPage += (result) =>
{
Console.WriteLine($"{result.ActiveMatchOrdinal}/{result.Matches} matches");
if (result.FinalUpdate)
{
webContents.StopFindInPage(StopFindInPageAction.ClearSelection);
}
};
var requestId = await webContents.FindInPageAsync("electron", new FindInPageOptions { MatchCase = false });
```
### Audio
```csharp
webContents.SetAudioMuted(true);
var muted = await webContents.IsAudioMutedAsync();
var audible = await webContents.IsCurrentlyAudibleAsync();
webContents.OnAudioStateChanged += (isAudible) =>
{
Console.WriteLine(isAudible ? "Page started emitting audio" : "Page went silent");
};
webContents.OnMediaStartedPlaying += () => Console.WriteLine("Media playing");
webContents.OnMediaPaused += () => Console.WriteLine("Media paused");
```
## Related APIs ## Related APIs
- [Electron.WindowManager](WindowManager.md) - Windows containing web contents - [Electron.WindowManager](WindowManager.md) - Windows containing web contents

View File

@@ -17,6 +17,10 @@ When you build an Electron.NET project, the following validation checks are perf
| [ELECTRON005](#4-parent-paths-not-allowed-in-electron-builderjson) | Parent paths not allowed | Checks for `..` references in config | | [ELECTRON005](#4-parent-paths-not-allowed-in-electron-builderjson) | Parent paths not allowed | Checks for `..` references in config |
| [ELECTRON006](#5-publish-profile-validation) | ASP.NET publish profile mismatch | Warns when ASP.NET projects have console-style profiles | | [ELECTRON006](#5-publish-profile-validation) | ASP.NET publish profile mismatch | Warns when ASP.NET projects have console-style profiles |
| [ELECTRON007](#5-publish-profile-validation) | Console publish profile mismatch | Warns when console projects have ASP.NET-style profiles | | [ELECTRON007](#5-publish-profile-validation) | Console publish profile mismatch | Warns when console projects have ASP.NET-style profiles |
| [ELECTRON010](#6-electronhosthook-folder-validation) | ElectronHostHook without package.json | Warns when hook TypeScript files exist without a `package.json` |
| [ELECTRON011](#6-electronhosthook-folder-validation) | ElectronHostHook without tsconfig.json | Warns when hook TypeScript files exist without a `tsconfig.json` |
All checks skip the build output folders (`bin`, `obj`, `publish`, `.electron`, `node_modules`) as well as any custom output paths configured via `BaseOutputPath` / `BaseIntermediateOutputPath` (for example the .NET `artifacts` output layout).
--- ---
@@ -32,7 +36,7 @@ Rules:
- **ELECTRON001**: `package.json` / `package-lock.json` must not exist in the project directory or subdirectories - **ELECTRON001**: `package.json` / `package-lock.json` must not exist in the project directory or subdirectories
- Exception: `ElectronHostHook` folder is allowed - Exception: `ElectronHostHook` folder is allowed
- Note: a **root** `package.json` is **excluded** from `ELECTRON001` and validated by `ELECTRON008` / `ELECTRON009` - Note: a **root** `package.json` (and its `package-lock.json`) is **excluded** from `ELECTRON001` and validated by `ELECTRON008` / `ELECTRON009`
- **ELECTRON008**: If a root `package.json` exists, it must **not** contain electron-related dependencies or configuration. - **ELECTRON008**: If a root `package.json` exists, it must **not** contain electron-related dependencies or configuration.
@@ -113,6 +117,10 @@ The `electron.manifest.json` file format is deprecated. All configuration should
"mac": { "mac": {
"icon": "Assets/app.icns", "icon": "Assets/app.icns",
"target": ["dmg", "zip"] "target": ["dmg", "zip"]
},
"nsis": {
"oneClick": true,
"perMachine": false
} }
} }
``` ```
@@ -208,7 +216,7 @@ The build system examines `.pubxml` files in the `Properties/PublishProfiles` fo
- **ELECTRON006**: For **ASP.NET projects** (using `Microsoft.NET.Sdk.Web`), checks that publish profiles include `WebPublishMethod`. This property is required for proper ASP.NET publishing. - **ELECTRON006**: For **ASP.NET projects** (using `Microsoft.NET.Sdk.Web`), checks that publish profiles include `WebPublishMethod`. This property is required for proper ASP.NET publishing.
- **ELECTRON007**: For **console/other projects** (not using the Web SDK), checks that publish profiles do NOT include the `WebPublishMethod` property. These ASP.NET-specific properties are incorrect for non-web applications. - **ELECTRON007**: For **console/other projects** (not using the Web SDK), checks that publish profiles do NOT include the `WebPublishMethod` or `ProjectGuid` property. These ASP.NET-specific properties are incorrect for non-web applications.
### Why this matters ### Why this matters
@@ -233,9 +241,54 @@ For correct publish profile examples for both ASP.NET and Console applications,
--- ---
## 6. ElectronHostHook Folder Validation
**Warning Codes:** `ELECTRON010`, `ELECTRON011`
### What is checked
If the project contains an `ElectronHostHook` folder with TypeScript files:
- **ELECTRON010**: The folder must contain a `package.json`
- **ELECTRON011**: The folder must contain a `tsconfig.json`
### Why this matters
Custom host hook code is only compiled and packaged when the `ElectronHostHook` folder declares its npm dependencies in a `package.json`. If that file is missing, the hook is silently dropped and the corresponding `Electron.HostHook` calls fail at runtime. The `tsconfig.json` is required so the hook sources are compiled with the expected settings.
### How to fix
Add the missing files to the `ElectronHostHook` folder:
```json
{
"name": "electron-host-hook",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"socket.io": "^4.8.1"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}
```
> **See also:** [HostHook API](../API/HostHook.md)
---
## Disabling Migration Checks ## Disabling Migration Checks
If you need to disable specific migration checks (not recommended), you can set the following properties in your `.csproj` file: Individual checks can be downgraded to messages by their warning code:
```xml
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);ELECTRON005</MSBuildWarningsAsMessages>
</PropertyGroup>
```
If you need to disable all migration checks (not recommended), you can set the following property in your `.csproj` file:
```xml ```xml
<PropertyGroup> <PropertyGroup>

View File

@@ -48,17 +48,19 @@ You can also manually edit `electron-builder.json`:
```json ```json
{ {
"linux": { "linux": {
"target": [ "target": ["tar.xz"]
"tar.xz"
]
}, },
"win": { "win": {
"target": [ "target": [
{ {
"target": "portable", "target": "nsis",
"arch": "x64" "arch": "x64"
} }
] ]
},
"nsis": {
"oneClick": true,
"perMachine": false
} }
} }
``` ```

View File

@@ -10,6 +10,11 @@ See [System Requirements](../GettingStarted/System-Requirements.md).
## 🚀 Quick Start ## 🚀 Quick Start
> [!Tip]
> To skip the manual setup below, scaffold a ready-to-run app with the
> [project templates](Templates.md): `dotnet new install ElectronNET.Core.Templates` followed by
> `dotnet new electron-blazor -n MyDesktopApp`.
### 1. Create ASP.NET Core Project ### 1. Create ASP.NET Core Project
#### Visual Studio #### Visual Studio

View File

@@ -54,7 +54,7 @@ Add the Electron.NET configuration to your `.csproj` file:
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.5.0" /> <PackageReference Include="ElectronNET.Core" Version="0.6.0" />
</ItemGroup> </ItemGroup>
``` ```

View File

@@ -0,0 +1,88 @@
# Project Templates
Electron.NET ships a `dotnet new` template package so you can scaffold a ready-to-run desktop app instead of wiring up an ASP.NET project by hand.
## 🛠 System Requirements
See [System Requirements](../GettingStarted/System-Requirements.md).
## 📦 Install the Templates
```bash
dotnet new install ElectronNET.Core.Templates
```
To update to the latest version, run the same command again. To remove them:
```bash
dotnet new uninstall ElectronNET.Core.Templates
```
## 🚀 Available Templates
| Template | Short name | Description |
|----------|------------|-------------|
| Electron.NET Blazor App | `electron-blazor` | A Blazor Server app hosted in an Electron shell |
## 🧱 Create a Blazor App
```bash
dotnet new electron-blazor -n MyDesktopApp
cd MyDesktopApp
```
Then start it in the Electron shell:
```bash
dotnet build
dotnet run
```
### Options
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| `-f`, `--framework` | `net8.0`, `net10.0` | `net10.0` | The target framework of the generated project |
| `-e`, `--electron-version` | any Electron version | `38.2.2` | The Electron version the app is built against |
| `-p`, `--port` | port number | `8001` | The port the ASP.NET server uses during development |
| `--no-restore` | — | — | Skip the automatic `dotnet restore` |
Example:
```bash
dotnet new electron-blazor -n MyDesktopApp -f net10.0 -e 30.4.0 -p 8123
```
## 📁 What You Get
```
MyDesktopApp/
├── Components/
│ ├── Layout/ MainLayout.razor, NavMenu.razor
│ ├── Pages/ Home.razor, Counter.razor
│ ├── App.razor
│ ├── Routes.razor
│ └── _Imports.razor
├── Properties/
│ ├── PublishProfiles/ win-x64, linux-x64 and osx-arm64 folder profiles
│ ├── electron-builder.json
│ └── launchSettings.json
├── wwwroot/app.css
├── appsettings.json
├── Program.cs
└── MyDesktopApp.csproj
```
The project already references `ElectronNET.Core` and `ElectronNET.Core.AspNet`, calls `builder.UseElectron(...)` in `Program.cs`, and passes all [Migration Checks](../Core/Migration-Checks.md) out of the box.
> [!Note]
> `ElectronNET.API` also defines a type named `App`, which collides with the Blazor root
> component. That is why the template calls `app.MapRazorComponents<MyDesktopApp.Components.App>()`
> with a fully qualified type name.
## 🚀 Next Steps
- **[Configuration](../Using/Configuration.md)** - Adjust app metadata and Electron settings
- **[Debugging](../Using/Debugging.md)** - Debug the .NET and Electron sides
- **[Package Building](../Using/Package-Building.md)** - Create distributable packages

View File

@@ -28,9 +28,77 @@ These are the current default values when you don't make any changes:
<ElectronPackageId>$(MSBuildProjectName.Replace(".", "-").ToLower())</ElectronPackageId> <ElectronPackageId>$(MSBuildProjectName.Replace(".", "-").ToLower())</ElectronPackageId>
<ElectronBuilderJson>electron-builder.json</ElectronBuilderJson> <ElectronBuilderJson>electron-builder.json</ElectronBuilderJson>
<Title>$(MSBuildProjectName)</Title> <Title>$(MSBuildProjectName)</Title>
<ElectronExecutableName></ElectronExecutableName>
<ElectronRootDir>../..</ElectronRootDir>
<ElectronSkipExecCommands>false</ElectronSkipExecCommands>
</PropertyGroup> </PropertyGroup>
``` ```
### Product Name and Executable Name
`Title` is the product name of the application. It is used for the window title, the installer, the macOS app bundle and the Linux desktop entry, and it may contain spaces and other characters that are awkward in a file name.
`ElectronExecutableName` controls the name of the executable inside the package (electron-builder's `executableName`). When it is not set, it defaults to the following behavior:
| Target | Default |
| --- | --- |
| Windows | `$(Title)` |
| Linux / macOS | `$(ElectronPackageId)` |
So to ship a product called `My Great App` with a `mygreatapp` binary on every platform:
```xml
<PropertyGroup>
<Title>My Great App</Title>
<ElectronExecutableName>mygreatapp</ElectronExecutableName>
</PropertyGroup>
```
> [!NOTE]
> `ElectronExecutable` is a separate, advanced setting: it is the binary Electron.NET launches when the .NET app starts first. It defaults to `ElectronExecutableName` and only needs to be changed when the package contains a launcher stub (for example added by an electron-builder hook) that should be started instead of the Electron binary.
### Custom Packaging Layout
`ElectronRootDir` tells the .NET app where to find the Electron binary when it is started first (DotNet-First startup of a packaged app). The path is resolved relative to the directory of the .NET executable; absolute paths are used as-is.
The default `../..` matches the standard Electron-First packaging layout produced by `dotnet publish`, where the .NET app is placed in `resources/bin` of the Electron package:
```
<install-root>/
MyApp # Electron executable
resources/
bin/
MyApp # .NET executable
```
If your own packaging pipeline places the .NET executable at the package root and Electron in a subdirectory, set `ElectronRootDir` accordingly:
```xml
<ElectronRootDir>electron</ElectronRootDir>
```
```
<install-root>/
MyApp # .NET executable
electron/
MyApp # Electron executable
resources/
locales/
```
The property is also used to detect whether the app runs packaged or unpackaged, so it must point to the directory that actually contains the Electron binary.
### Build Extensibility
These properties are useful when Electron.NET is embedded into a custom build or CI pipeline:
| Property | Default | Description |
| --- | --- | --- |
| `ElectronSkipExecCommands` | `false` | Skips all `npm`/`npx` invocations of the Electron build and publish targets. Use it when the dependencies and the Electron distribution are provided by the pipeline itself (for example from a cache or an offline mirror). |
| `ElectronIntermediatePublishDir` | `$(IntermediateOutputPath)PubTmp\` | Overrides the intermediate directory the .NET app is published to before it is assembled into the Electron package. |
`ElectronPackageId` and `Title` only fall back to the project name when they are not already set, so both can be defined by a `Directory.Build.props` or passed on the command line.
### Relation to package.json ### Relation to package.json
ElectronNET.Core does not work with an `electron-manifest.json` file anymore. ElectronNET.Core does not work with an `electron-manifest.json` file anymore.
@@ -42,11 +110,17 @@ Since electron builder still expects a `package.json` file to exist, ElectronNET
"productName": "$(ElectronTitle)", "productName": "$(ElectronTitle)",
"build": { "build": {
"appId": "$(ElectronPackageId)", "appId": "$(ElectronPackageId)",
"win": {
"executableName": "$(ElectronExecutableName)"
},
"mac": {
"executableName": "$(ElectronExecutableName)"
},
"linux": { "linux": {
"desktop": { "desktop": {
"entry": { "Name": "$(Title)" } "entry": { "Name": "$(Title)" }
}, },
"executableName": "$(ElectronPackageId)" "executableName": "$(ElectronExecutableName)"
}, },
"deb": { "deb": {
"desktop": { "desktop": {
@@ -70,6 +144,12 @@ Since electron builder still expects a `package.json` file to exist, ElectronNET
} }
``` ```
### App Icon Path
The `ElectronIcon` property supports classic icon files (such as `.ico` and `.icns`) and modern macOS `.icon` app icon packages.
For `.icon`, provide the folder path (for example `Assets/MyApp.icon`). During build/publish, Electron.NET copies the full directory into the Electron output so `electron-builder.json` can reference it (for example `"mac": { "icon": "MyApp.icon" }`).
### Node.js Integration ### Node.js Integration
Electron.NET requires Node.js integration to be enabled for IPC to function. If you are not using the IPC functionality you can disable Node.js integration like so: Electron.NET requires Node.js integration to be enabled for IPC to function. If you are not using the IPC functionality you can disable Node.js integration like so:

View File

@@ -7,6 +7,7 @@ This guide explains how to include and use a `custom_main.js` file in your Elect
- Register custom protocol handlers (e.g., `myapp://`) — protocols must be registered before the app is fully initialized - Register custom protocol handlers (e.g., `myapp://`) — protocols must be registered before the app is fully initialized
- Integrate Node.js modules (e.g., telemetry, OS APIs) - Integrate Node.js modules (e.g., telemetry, OS APIs)
- Control startup logic (abort, environment checks) - Control startup logic (abort, environment checks)
- Modify Chromium / Electron.NET command line switches before they are evaluated
- Set up IPC messaging or preload scripts - Set up IPC messaging or preload scripts
## Step-by-Step Process ## Step-by-Step Process
@@ -65,10 +66,29 @@ Use environment variables to control features:
if (env === 'Development') { /* enable dev features */ } if (env === 'Development') { /* enable dev features */ }
``` ```
### Modifying Command Line Switches
`onStartup` is invoked before Electron.NET reads any of its own switches (`manifest`, `unpackedelectron`, `unpackeddotnet`, `dotnetpacked`, `electronforcedport`, `electronurl`), so the hook can append or override them:
```javascript
module.exports.onStartup = function (host) {
const { app } = require('electron');
// Force a fixed socket bridge port
app.commandLine.appendSwitch('electronforcedport', '8000');
// Chromium switches, e.g. to disable the GPU sandbox
app.commandLine.appendSwitch('disable-gpu-sandbox');
return true;
};
```
## Notes ## Notes
- `custom_main.js` must use CommonJS syntax (`module.exports.onStartup = ...`). - `custom_main.js` must use CommonJS syntax (`module.exports.onStartup = ...`).
- Place the file in your source directory and copy it to `.electron` using `.csproj`. - Place the file in your source directory and copy it to `.electron` using `.csproj`.
- Electron.NET will abort startup if `onStartup` returns `false`. - Electron.NET will abort startup if `onStartup` returns `false`.
- `onStartup` runs before the Electron.NET command line switches are evaluated, so switches set there are picked up by the host.
### Complete example is available here [ElectronNetSampleApp](https://github.com/niteshsinghal85/ElectronNetSampleApp) ### Complete example is available here [ElectronNetSampleApp](https://github.com/niteshsinghal85/ElectronNetSampleApp)

View File

@@ -222,6 +222,15 @@ The publish process will:
> macOS builds can't be created on Windows machines because they require symlinks that aren't supported on Windows (per [this Electron issue](https://github.com/electron-userland/electron-packager/issues/71)). macOS builds can be produced on either Linux or macOS machines. > macOS builds can't be created on Windows machines because they require symlinks that aren't supported on Windows (per [this Electron issue](https://github.com/electron-userland/electron-packager/issues/71)). macOS builds can be produced on either Linux or macOS machines.
## Custom Packaging Pipelines
The default layout is Electron-First: electron-builder produces the Electron package and the .NET app is placed in `resources/bin`. If you assemble the package yourself and put the .NET executable at the package root with Electron in a subdirectory, set `ElectronRootDir` so the .NET app can locate the Electron binary at runtime.
For pipelines that provide the npm dependencies themselves, `ElectronSkipExecCommands` suppresses all `npm`/`npx` invocations, and `ElectronIntermediatePublishDir` redirects the intermediate publish output.
See [Configuration](Configuration.md#custom-packaging-layout) for details.
## 🚀 Next Steps ## 🚀 Next Steps
- **[Startup Methods](Startup-Methods.md)** - Understanding different launch modes for packaged apps - **[Startup Methods](Startup-Methods.md)** - Understanding different launch modes for packaged apps

View File

@@ -15,6 +15,7 @@
# Getting Started # Getting Started
- [System Requirements](GettingStarted/System-Requirements.md) - [System Requirements](GettingStarted/System-Requirements.md)
- [Project Templates](GettingStarted/Templates.md)
- [With ASP.Net](GettingStarted/ASP.Net.md) - [With ASP.Net](GettingStarted/ASP.Net.md)
- [With a Console App](GettingStarted/Console-App.md) - [With a Console App](GettingStarted/Console-App.md)

View File

@@ -0,0 +1,18 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for <see cref="WebContents.AdjustSelection"/>.
/// </summary>
public class AdjustSelectionOptions
{
/// <summary>
/// Amount to shift the start index of the current selection.
/// </summary>
public int? Start { get; set; }
/// <summary>
/// Amount to shift the end index of the current selection.
/// </summary>
public int? End { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for <see cref="WebContents.FindInPageAsync"/>.
/// </summary>
public class FindInPageOptions
{
/// <summary>
/// Whether to search forward or backward, defaults to true.
/// </summary>
public bool? Forward { get; set; }
/// <summary>
/// Whether to begin a new text finding session with this request. Should be true
/// for initial requests, and false for subsequent requests. Defaults to false.
/// </summary>
public bool? FindNext { get; set; }
/// <summary>
/// Whether search should be case-sensitive, defaults to false.
/// </summary>
public bool? MatchCase { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// The result of a text finding session, reported by the found-in-page event.
/// </summary>
public class FoundInPageResult
{
/// <summary>
/// The identifier of the request returned by FindInPageAsync.
/// </summary>
public int RequestId { get; set; }
/// <summary>
/// Position of the active match.
/// </summary>
public int ActiveMatchOrdinal { get; set; }
/// <summary>
/// Number of matches.
/// </summary>
public int Matches { get; set; }
/// <summary>
/// Coordinates of the first match region.
/// </summary>
public Rectangle SelectionArea { get; set; }
/// <summary>
/// Indicates whether more responses are to follow.
/// </summary>
public bool FinalUpdate { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for loading a local file into a web page.
/// </summary>
public class LoadFileOptions
{
/// <summary>
/// Passed to url.format().
/// </summary>
public Dictionary<string, string> Query { get; set; }
/// <summary>
/// Passed to url.format().
/// </summary>
public string Search { get; set; }
/// <summary>
/// Passed to url.format().
/// </summary>
public string Hash { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Determines what happens to the selection when a text finding session is stopped.
/// </summary>
public enum StopFindInPageAction
{
/// <summary>
/// Clear the selection.
/// </summary>
ClearSelection,
/// <summary>
/// Translate the selection into a normal selection.
/// </summary>
KeepSelection,
/// <summary>
/// Focus and click the selection node.
/// </summary>
ActivateSelection
}
}

View File

@@ -0,0 +1,18 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// The direction of a zoom change triggered by the user.
/// </summary>
public enum ZoomDirection
{
/// <summary>
/// The user zoomed in.
/// </summary>
In,
/// <summary>
/// The user zoomed out.
/// </summary>
Out
}
}

View File

@@ -113,6 +113,52 @@ public class WebContents : ApiBase
remove => RemoveEvent(value, Id); remove => RemoveEvent(value, Id);
} }
/// <summary>
/// Emitted when the user is requesting to change the zoom level using the mouse wheel or the keyboard.
/// </summary>
public event Action<ZoomDirection> OnZoomChanged
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when a result is available for a <see cref="FindInPageAsync(string, FindInPageOptions)"/> request.
/// </summary>
public event Action<FoundInPageResult> OnFoundInPage
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when media becomes audible or inaudible. The parameter is true if one or more
/// frames or child web contents are emitting audio.
/// </summary>
public event Action<bool> OnAudioStateChanged
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when media starts playing.
/// </summary>
public event Action OnMediaStartedPlaying
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when media is paused or done playing.
/// </summary>
public event Action OnMediaPaused
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
internal WebContents(int id) internal WebContents(int id)
{ {
Id = id; Id = id;
@@ -304,6 +350,68 @@ public class WebContents : ApiBase
return tcs.Task; return tcs.Task;
} }
/// <summary>
/// Loads the given file in the window. The file path must be a path to an HTML file
/// relative to the root of your application.
/// </summary>
/// <param name="filePath">Path to the HTML file.</param>
/// <param name="options">Optional query, search and hash parts of the resulting URL.</param>
public Task LoadFileAsync(string filePath, LoadFileOptions options = null)
{
var tcs = new TaskCompletionSource();
BridgeConnector.Socket.Once("webContents-loadFile-complete" + Id, () =>
{
BridgeConnector.Socket.Off("webContents-loadFile-error" + Id);
tcs.SetResult();
});
BridgeConnector.Socket.Once<string>("webContents-loadFile-error" + Id, (error) => { tcs.SetException(new InvalidOperationException(error)); });
BridgeConnector.Socket.Emit("webContents-loadFile", Id, filePath, options);
return tcs.Task;
}
/// <summary>
/// Returns boolean - Whether web page is still loading resources.
/// </summary>
public Task<bool> IsLoadingAsync() => InvokeAsync<bool>();
/// <summary>
/// Returns boolean - Whether the main frame (and not just iframes or frames within it) is still loading.
/// </summary>
public Task<bool> IsLoadingMainFrameAsync() => InvokeAsync<bool>();
/// <summary>
/// Returns boolean - Whether the web page is waiting for a first-response from the main resource of the page.
/// </summary>
public Task<bool> IsWaitingForResponseAsync() => InvokeAsync<bool>();
/// <summary>
/// Reloads the current web page.
/// </summary>
public void Reload()
{
BridgeConnector.Socket.Emit("webContents-reload", Id);
}
/// <summary>
/// Reloads the current page and ignores cache.
/// </summary>
public void ReloadIgnoringCache()
{
BridgeConnector.Socket.Emit("webContents-reloadIgnoringCache", Id);
}
/// <summary>
/// Stops any pending navigation.
/// </summary>
public void Stop()
{
BridgeConnector.Socket.Emit("webContents-stop", Id);
}
/// <summary> /// <summary>
/// Inserts CSS into the web page. /// Inserts CSS into the web page.
/// See: https://www.electronjs.org/docs/api/web-contents#contentsinsertcsscss-options /// See: https://www.electronjs.org/docs/api/web-contents#contentsinsertcsscss-options
@@ -316,6 +424,37 @@ public class WebContents : ApiBase
BridgeConnector.Socket.Emit("webContents-insertCSS", Id, isBrowserWindow, path); BridgeConnector.Socket.Emit("webContents-insertCSS", Id, isBrowserWindow, path);
} }
/// <summary>
/// Injects CSS into the current web page and returns a unique key for the inserted
/// stylesheet, which can be used with <see cref="RemoveInsertedCSSAsync"/>.
/// </summary>
/// <param name="css">The style sheet to inject.</param>
/// <param name="cssOrigin">Can be either 'user' or 'author'. Defaults to 'author'.</param>
/// <returns>The key of the inserted style sheet.</returns>
public Task<string> InsertCSSAsync(string css, string cssOrigin = null)
{
var tcs = new TaskCompletionSource<string>();
BridgeConnector.Socket.Once<string>("webContents-insertCSSText-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-insertCSSText", Id, css, cssOrigin);
return tcs.Task;
}
/// <summary>
/// Removes the inserted CSS from the current web page.
/// </summary>
/// <param name="key">The key returned by <see cref="InsertCSSAsync"/>.</param>
public Task RemoveInsertedCSSAsync(string key)
{
var tcs = new TaskCompletionSource();
BridgeConnector.Socket.Once("webContents-removeInsertedCSS-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-removeInsertedCSS", Id, key);
return tcs.Task;
}
/// <summary> /// <summary>
/// Returns number - The current zoom factor. /// Returns number - The current zoom factor.
/// </summary> /// </summary>
@@ -337,14 +476,14 @@ public class WebContents : ApiBase
/// Returns number - The current zoom level. /// Returns number - The current zoom level.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public Task<int> GetZoomLevelAsync() => InvokeAsync<int>(); public Task<double> GetZoomLevelAsync() => InvokeAsync<double>();
/// <summary> /// <summary>
/// Changes the zoom level to the specified level. /// Changes the zoom level to the specified level.
/// The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively. /// The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively.
/// </summary> /// </summary>
/// <param name="level"></param> /// <param name="level"></param>
public void SetZoomLevel(int level) public void SetZoomLevel(double level)
{ {
BridgeConnector.Socket.Emit("webContents-setZoomLevel", Id, level); BridgeConnector.Socket.Emit("webContents-setZoomLevel", Id, level);
} }
@@ -354,7 +493,7 @@ public class WebContents : ApiBase
/// </summary> /// </summary>
/// <param name="minimumLevel"></param> /// <param name="minimumLevel"></param>
/// <param name="maximumLevel"></param> /// <param name="maximumLevel"></param>
public Task SetVisualZoomLevelLimitsAsync(int minimumLevel, int maximumLevel) public Task SetVisualZoomLevelLimitsAsync(double minimumLevel, double maximumLevel)
{ {
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
@@ -399,4 +538,178 @@ public class WebContents : ApiBase
{ {
BridgeConnector.Socket.Emit("webContents-setUserAgent", Id, userAgent); BridgeConnector.Socket.Emit("webContents-setUserAgent", Id, userAgent);
} }
/// <summary>
/// Executes the editing command undo in web page.
/// </summary>
public void Undo()
{
BridgeConnector.Socket.Emit("webContents-undo", Id);
}
/// <summary>
/// Executes the editing command redo in web page.
/// </summary>
public void Redo()
{
BridgeConnector.Socket.Emit("webContents-redo", Id);
}
/// <summary>
/// Executes the editing command cut in web page.
/// </summary>
public void Cut()
{
BridgeConnector.Socket.Emit("webContents-cut", Id);
}
/// <summary>
/// Executes the editing command copy in web page.
/// </summary>
public void Copy()
{
BridgeConnector.Socket.Emit("webContents-copy", Id);
}
/// <summary>
/// Copies the image at the given position to the clipboard.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public void CopyImageAt(int x, int y)
{
BridgeConnector.Socket.Emit("webContents-copyImageAt", Id, x, y);
}
/// <summary>
/// Executes the editing command paste in web page.
/// </summary>
public void Paste()
{
BridgeConnector.Socket.Emit("webContents-paste", Id);
}
/// <summary>
/// Executes the editing command pasteAndMatchStyle in web page.
/// </summary>
public void PasteAndMatchStyle()
{
BridgeConnector.Socket.Emit("webContents-pasteAndMatchStyle", Id);
}
/// <summary>
/// Executes the editing command delete in web page.
/// </summary>
public void Delete()
{
BridgeConnector.Socket.Emit("webContents-delete", Id);
}
/// <summary>
/// Executes the editing command selectAll in web page.
/// </summary>
public void SelectAll()
{
BridgeConnector.Socket.Emit("webContents-selectAll", Id);
}
/// <summary>
/// Executes the editing command unselect in web page.
/// </summary>
public void Unselect()
{
BridgeConnector.Socket.Emit("webContents-unselect", Id);
}
/// <summary>
/// Adjusts the current text selection starting and ending points by the given amounts.
/// A negative amount moves the selection towards the beginning of the document.
/// </summary>
/// <param name="options"></param>
public void AdjustSelection(AdjustSelectionOptions options)
{
BridgeConnector.Socket.Emit("webContents-adjustSelection", Id, options);
}
/// <summary>
/// Scrolls to the current text selection.
/// </summary>
public void CenterSelection()
{
BridgeConnector.Socket.Emit("webContents-centerSelection", Id);
}
/// <summary>
/// Scrolls to the top of the current web page.
/// </summary>
public void ScrollToTop()
{
BridgeConnector.Socket.Emit("webContents-scrollToTop", Id);
}
/// <summary>
/// Scrolls to the bottom of the current web page.
/// </summary>
public void ScrollToBottom()
{
BridgeConnector.Socket.Emit("webContents-scrollToBottom", Id);
}
/// <summary>
/// Inserts text to the focused element.
/// </summary>
/// <param name="text">The text to be inserted.</param>
public Task InsertTextAsync(string text)
{
var tcs = new TaskCompletionSource();
BridgeConnector.Socket.Once("webContents-insertText-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-insertText", Id, text);
return tcs.Task;
}
/// <summary>
/// Executes the editing command replace in web page.
/// </summary>
/// <param name="text"></param>
public void Replace(string text)
{
BridgeConnector.Socket.Emit("webContents-replace", Id, text);
}
/// <summary>
/// Executes the editing command replaceMisspelling in web page.
/// </summary>
/// <param name="text"></param>
public void ReplaceMisspelling(string text)
{
BridgeConnector.Socket.Emit("webContents-replaceMisspelling", Id, text);
}
/// <summary>
/// Starts a request to find all matches for the text in the web page.
/// The result of the request can be obtained by subscribing to the <see cref="OnFoundInPage"/> event.
/// </summary>
/// <param name="text">Content to be searched, must not be empty.</param>
/// <param name="options"></param>
/// <returns>The request id used for the request.</returns>
public Task<int> FindInPageAsync(string text, FindInPageOptions options = null)
{
var tcs = new TaskCompletionSource<int>();
BridgeConnector.Socket.Once<int>("webContents-findInPage-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-findInPage", Id, text, options);
return tcs.Task;
}
/// <summary>
/// Stops any findInPage request for the web contents with the provided action.
/// </summary>
/// <param name="action"></param>
public void StopFindInPage(StopFindInPageAction action)
{
BridgeConnector.Socket.Emit("webContents-stopFindInPage", Id, action);
}
} }

View File

@@ -19,6 +19,7 @@ namespace ElectronNET.API
internal WindowManager() internal WindowManager()
{ {
EnsureBrowserWindowClosedSubscription();
} }
internal static WindowManager Instance internal static WindowManager Instance
@@ -106,17 +107,6 @@ namespace ElectronNET.API
tcs.SetResult(browserWindow); tcs.SetResult(browserWindow);
}); });
BridgeConnector.Socket.Once<int[]>("BrowserWindowClosed", (ids) =>
{
for (int index = 0; index < _browserWindows.Count; index++)
{
if (!ids.Contains(_browserWindows[index].Id))
{
_browserWindows.RemoveAt(index);
}
}
});
if (loadUrl.Equals("http://localhost", StringComparison.OrdinalIgnoreCase) && ElectronNetRuntime.AspNetWebPort.HasValue) if (loadUrl.Equals("http://localhost", StringComparison.OrdinalIgnoreCase) && ElectronNetRuntime.AspNetWebPort.HasValue)
{ {
loadUrl = $"{loadUrl}:{ElectronNetRuntime.AspNetWebPort}"; loadUrl = $"{loadUrl}:{ElectronNetRuntime.AspNetWebPort}";
@@ -149,6 +139,49 @@ namespace ElectronNET.API
return await tcs.Task.ConfigureAwait(false); return await tcs.Task.ConfigureAwait(false);
} }
private readonly object _browserWindowSubscriptionSync = new();
private bool _browserWindowClosedSubscribed;
private void EnsureBrowserWindowClosedSubscription()
{
if (_browserWindowClosedSubscribed)
{
return;
}
lock (_browserWindowSubscriptionSync)
{
if (_browserWindowClosedSubscribed)
{
return;
}
BridgeConnector.Socket.On<int[]>("BrowserWindowClosed", HandleBrowserWindowClosed);
BridgeConnector.Socket.On<int>("BrowserWindowRecreated", HandleBrowserWindowRecreated);
_browserWindowClosedSubscribed = true;
}
}
private void HandleBrowserWindowRecreated(int id)
{
if (_browserWindows.All(window => window.Id != id))
{
_browserWindows.Add(new BrowserWindow(id));
}
}
private void HandleBrowserWindowClosed(int[] ids)
{
if (ids == null || ids.Length == 0)
{
_browserWindows.Clear();
return;
}
var existingIds = ids.ToHashSet();
_browserWindows.RemoveAll(window => !existingIds.Contains(window.Id));
}
private bool IsWindows10() private bool IsWindows10()
{ {
return RuntimeInformation.OSDescription.Contains("Windows 10"); return RuntimeInformation.OSDescription.Contains("Windows 10");

View File

@@ -4,6 +4,7 @@ namespace ElectronNET.API;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using ElectronNET.API.Serialization; using ElectronNET.API.Serialization;
using SocketIO.Serializer.SystemTextJson; using SocketIO.Serializer.SystemTextJson;
@@ -18,13 +19,21 @@ internal class SocketIOConnection : ISocketConnection
public SocketIOConnection(string uri, string authorization) public SocketIOConnection(string uri, string authorization)
{ {
var opts = string.IsNullOrEmpty(authorization) ? new SocketIOOptions() : new SocketIOOptions var opts = new SocketIOOptions
{ {
ExtraHeaders = new Dictionary<string, string> // The bridge is a loopback IPC channel, so it must never go through a
// (system) proxy - otherwise the handshake never reaches the socket server.
Proxy = DirectProxy.Instance,
};
if (!string.IsNullOrEmpty(authorization))
{
opts.ExtraHeaders = new Dictionary<string, string>
{ {
["authorization"] = authorization ["authorization"] = authorization
}, };
}; }
_socket = new SocketIO(uri, opts); _socket = new SocketIO(uri, opts);
_socket.Serializer = new SystemTextJsonSerializer(ElectronJson.Options); _socket.Serializer = new SystemTextJsonSerializer(ElectronJson.Options);
// Use default System.Text.Json serializer from SocketIOClient. // Use default System.Text.Json serializer from SocketIOClient.
@@ -152,4 +161,15 @@ internal class SocketIOConnection : ISocketConnection
throw new ObjectDisposedException(nameof(SocketIOConnection)); throw new ObjectDisposedException(nameof(SocketIOConnection));
} }
} }
private sealed class DirectProxy : IWebProxy
{
public static readonly DirectProxy Instance = new DirectProxy();
public ICredentials Credentials { get; set; }
public Uri GetProxy(Uri destination) => destination;
public bool IsBypassed(Uri host) => true;
}
} }

View File

@@ -37,10 +37,18 @@
public ProcessRunner(string name) public ProcessRunner(string name)
{ {
this.Name = name; this.Name = name;
this.ExitWaitTimeout = DefaultExitWaitTimeout;
} }
public event EventHandler<EventArgs> ProcessExited; public event EventHandler<EventArgs> ProcessExited;
/// <summary>Gets or sets the value <see cref="ExitWaitTimeout"/> is initialized with.</summary>
public static int DefaultExitWaitTimeout { get; set; } = 5000;
/// <summary>Gets or sets the time in milliseconds to wait for the process and its I/O to finish
/// after the exited event has been raised. Values of zero or less wait indefinitely.</summary>
public int ExitWaitTimeout { get; set; }
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
private Process Process private Process Process
@@ -498,8 +506,17 @@
{ {
try try
{ {
var timeout = this.ExitWaitTimeout;
// This shouldn't throw here, but the mono process implementation doesn't always behave as it should. // This shouldn't throw here, but the mono process implementation doesn't always behave as it should.
this.process.WaitForExit(); if (timeout > 0)
{
this.process.WaitForExit(timeout);
}
else
{
this.process.WaitForExit();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -3,7 +3,7 @@
<Import Project="..\common.props" /> <Import Project="..\common.props" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks> <TargetFrameworks>$(ElectronNetTargetFrameworks)</TargetFrameworks>
<PackageOutputPath>..\..\artifacts</PackageOutputPath> <PackageOutputPath>..\..\artifacts</PackageOutputPath>
<PackageId>$(PackageNamePrefix).API</PackageId> <PackageId>$(PackageNamePrefix).API</PackageId>
<Title>$(PackageId)</Title> <Title>$(PackageId)</Title>
@@ -29,7 +29,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="SocketIOClient" Version="3.1.2" /> <PackageReference Include="SocketIOClient" Version="3.1.2" />
<PackageReference Include="System.Drawing.Common" Version="8.0.22" /> <PackageReference Include="System.Drawing.Common" Version="8.0.22" />
<PackageReference Include="System.Text.Json" Version="8.0.6" /> <PackageReference Include="System.Text.Json" Version="8.0.6" Condition=" '$(TargetFramework)' == 'net6.0' " />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -15,6 +15,7 @@
internal const int DefaultSocketPort = 8000; internal const int DefaultSocketPort = 8000;
internal const int DefaultWebPort = 8001; internal const int DefaultWebPort = 8001;
internal const string ElectronPortArgumentName = "electronPort"; internal const string ElectronPortArgumentName = "electronPort";
internal const string ElectronHostArgumentName = "electronHost";
internal const string ElectronPidArgumentName = "electronPID"; internal const string ElectronPidArgumentName = "electronPID";
internal const string ElectronAuthTokenArgumentName = "electronAuthToken"; internal const string ElectronAuthTokenArgumentName = "electronAuthToken";
@@ -31,6 +32,11 @@
public static int? ElectronSocketPort { get; internal set; } public static int? ElectronSocketPort { get; internal set; }
/// <summary>
/// The loopback address the Electron socket bridge is listening on.
/// </summary>
public static string ElectronSocketHost { get; internal set; }
public static int? AspNetWebPort { get; internal set; } public static int? AspNetWebPort { get; internal set; }
public static StartupMethod StartupMethod { get; internal set; } public static StartupMethod StartupMethod { get; internal set; }
@@ -50,8 +56,6 @@
internal static int? ElectronProcessId { get; set; } internal static int? ElectronProcessId { get; set; }
internal static Func<Task> OnAppReadyCallback { get; set; }
internal static ISocketConnection GetSocket() internal static ISocketConnection GetSocket()
{ {
return RuntimeControllerCore?.Socket; return RuntimeControllerCore?.Socket;

View File

@@ -6,6 +6,8 @@
public string ElectronVersion { get; internal set; } public string ElectronVersion { get; internal set; }
public string ElectronRootDir { get; internal set; }
public string RuntimeIdentifier { get; internal set; } public string RuntimeIdentifier { get; internal set; }
public bool ElectronSingleInstance { get; internal set; } public bool ElectronSingleInstance { get; internal set; }

View File

@@ -0,0 +1,31 @@
namespace ElectronNET.Runtime.Helpers
{
using System;
using System.IO;
/// <summary>
/// Resolves the directory that contains the Electron binary of a packaged application.
/// </summary>
internal static class ElectronRootDirResolver
{
/// <summary>The location of the Electron binary relative to the .NET application directory in the
/// default (Electron-first) layout, where the .NET application is placed in 'resources/bin'.</summary>
internal const string DefaultElectronRootDir = "../..";
/// <summary>Resolves the Electron root directory relative to the given .NET application directory.</summary>
/// <param name="baseDirectory">The directory containing the .NET application.</param>
/// <returns>The directory containing the Electron binary.</returns>
public static DirectoryInfo Resolve(DirectoryInfo baseDirectory)
{
var rootDir = ElectronNetRuntime.BuildInfo?.ElectronRootDir;
if (string.IsNullOrWhiteSpace(rootDir))
{
rootDir = DefaultElectronRootDir;
}
// An absolute ElectronRootDir is used as-is by Path.Combine.
return new DirectoryInfo(Path.GetFullPath(Path.Combine(baseDirectory.FullName, rootDir)));
}
}
}

View File

@@ -69,6 +69,15 @@
return false; return false;
} }
// Packaged, if the Electron binary is found where ElectronRootDir points to.
var electronExecutable = ElectronNetRuntime.ElectronExecutable;
if (!string.IsNullOrEmpty(electronExecutable) &&
File.Exists(Path.Combine(ElectronRootDirResolver.Resolve(dir).FullName, electronExecutable)))
{
return false;
}
if (dir.GetDirectories().Any(e => e.Name == ".electron")) if (dir.GetDirectories().Any(e => e.Name == ".electron"))
{ {
return true; return true;

View File

@@ -11,6 +11,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ElectronNET.Common; using ElectronNET.Common;
using ElectronNET.Runtime.Data; using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;
/// <summary> /// <summary>
/// Launches and manages the Electron app process. /// Launches and manages the Electron app process.
@@ -18,7 +19,7 @@
[Localizable(false)] [Localizable(false)]
internal class ElectronProcessActive : ElectronProcessBase internal class ElectronProcessActive : ElectronProcessBase
{ {
private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$"); private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at (\\S+) using ([a-f0-9]+)$");
private readonly bool isUnpackaged; private readonly bool isUnpackaged;
private readonly string electronBinaryName; private readonly string electronBinaryName;
@@ -87,7 +88,7 @@
} }
else else
{ {
dir = dir.Parent!.Parent!; dir = ElectronRootDirResolver.Resolve(dir);
startCmd = Path.Combine(dir.FullName, this.electronBinaryName); startCmd = Path.Combine(dir.FullName, this.electronBinaryName);
args = $"-dotnetpacked -electronforcedport={this.socketPort:D} " + this.extraArguments; args = $"-dotnetpacked -electronforcedport={this.socketPort:D} " + this.extraArguments;
workingDir = dir.FullName; workingDir = dir.FullName;
@@ -179,11 +180,13 @@
if (match?.Success ?? false) if (match?.Success ?? false)
{ {
var port = int.Parse(match.Groups[1].Value); var port = int.Parse(match.Groups[1].Value);
var token = match.Groups[2].Value; var host = match.Groups[2].Value;
var token = match.Groups[3].Value;
this.process.LineReceived -= Read_SocketIO_Parameters; this.process.LineReceived -= Read_SocketIO_Parameters;
ElectronNetRuntime.ElectronAuthToken = token; ElectronNetRuntime.ElectronAuthToken = token;
ElectronNetRuntime.ElectronSocketPort = port; ElectronNetRuntime.ElectronSocketPort = port;
ElectronNetRuntime.ElectronSocketHost = host;
tcs.SetResult(); tcs.SetResult();
} }
} }

View File

@@ -16,7 +16,19 @@
{ {
this.socketPort = socketPort; this.socketPort = socketPort;
this.authorization = authorization; this.authorization = authorization;
this.socketUrl = $"http://localhost:{this.socketPort}"; this.socketUrl = $"http://{FormatHost(ElectronNetRuntime.ElectronSocketHost)}:{this.socketPort}";
}
// The Electron host reports the loopback address it is actually listening on; only
// when it is unknown we have to fall back to the ambiguous hostname.
private static string FormatHost(string socketHost)
{
if (string.IsNullOrWhiteSpace(socketHost))
{
return "localhost";
}
return socketHost.Contains(':') ? $"[{socketHost}]" : socketHost;
} }
public int SocketPort => this.socketPort; public int SocketPort => this.socketPort;

View File

@@ -94,8 +94,20 @@
} }
} }
var pidArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronPidArgumentName, StringComparison.OrdinalIgnoreCase)); var hostArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronHostArgumentName, StringComparison.OrdinalIgnoreCase));
if (hostArg != null)
{
var parts = hostArg.Split('=', StringSplitOptions.TrimEntries);
if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]))
{
ElectronNetRuntime.ElectronSocketHost = parts[1];
Console.WriteLine("Use Electron Host: " + parts[1]);
}
}
var pidArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronPidArgumentName, StringComparison.OrdinalIgnoreCase));
if (pidArg != null) if (pidArg != null)
{ {
var parts = pidArg.Split('=', StringSplitOptions.TrimEntries); var parts = pidArg.Split('=', StringSplitOptions.TrimEntries);
@@ -166,6 +178,7 @@
{ {
buildInfo.ElectronExecutable = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronExecutable))?.Value; buildInfo.ElectronExecutable = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronExecutable))?.Value;
buildInfo.ElectronVersion = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronVersion))?.Value; buildInfo.ElectronVersion = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronVersion))?.Value;
buildInfo.ElectronRootDir = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronRootDir))?.Value;
buildInfo.RuntimeIdentifier = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.RuntimeIdentifier))?.Value; buildInfo.RuntimeIdentifier = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.RuntimeIdentifier))?.Value;
buildInfo.Title = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Title))?.Value; buildInfo.Title = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Title))?.Value;
buildInfo.Version = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Version))?.Value; buildInfo.Version = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Version))?.Value;

View File

@@ -44,5 +44,98 @@
return builder; return builder;
} }
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core application and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process, forwarded to Electron.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="WebApplicationBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// var builder = WebApplication.CreateBuilder(args)
/// .UseElectron(args, async (processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
///
/// var app = builder.Build();
/// app.MapRazorPages();
/// app.Run();
/// </code>
/// </example>
public static WebApplicationBuilder UseElectron(this WebApplicationBuilder builder, string[] args, Func<string[], Task> onAppReadyCallback)
{
builder.WebHost.UseElectron(args, onAppReadyCallback);
return builder;
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core application and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process, forwarded to Electron.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="WebApplicationBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// var builder = WebApplication.CreateBuilder(args)
/// .UseElectron(args, async (serviceProvider) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
///
/// var app = builder.Build();
/// app.MapRazorPages();
/// app.Run();
/// </code>
/// </example>
public static WebApplicationBuilder UseElectron(this WebApplicationBuilder builder, string[] args, Func<IServiceProvider, Task> onAppReadyCallback)
{
builder.WebHost.UseElectron(args, onAppReadyCallback);
return builder;
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core application and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process, forwarded to Electron.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="WebApplicationBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// var builder = WebApplication.CreateBuilder(args)
/// .UseElectron(args, async (serviceProvider, processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
///
/// var app = builder.Build();
/// app.MapRazorPages();
/// app.Run();
/// </code>
/// </example>
public static WebApplicationBuilder UseElectron(this WebApplicationBuilder builder, string[] args, Func<IServiceProvider, string[], Task> onAppReadyCallback)
{
builder.WebHost.UseElectron(args, onAppReadyCallback);
return builder;
}
} }
} }

View File

@@ -61,8 +61,154 @@
/// </example> /// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<Task> onAppReadyCallback) public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<Task> onAppReadyCallback)
{ {
ElectronNetRuntime.OnAppReadyCallback = onAppReadyCallback; builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(_ => new AppReadyCallbackResolver(onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// using Microsoft.AspNetCore.Hosting;
/// using Microsoft.Extensions.Hosting;
/// using ElectronNET.API;
///
/// public class Program
/// {
/// public static void Main(string[] args)
/// {
/// Host.CreateDefaultBuilder(args)
/// .ConfigureWebHostDefaults(webBuilder =>
/// {
/// webBuilder.UseStartup&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async (processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<string[], Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(_ => new AppReadyCallbackResolver(args, onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// using Microsoft.AspNetCore.Hosting;
/// using Microsoft.Extensions.Hosting;
/// using ElectronNET.API;
///
/// public class Program
/// {
/// public static void Main(string[] args)
/// {
/// Host.CreateDefaultBuilder(args)
/// .ConfigureWebHostDefaults(webBuilder =>
/// {
/// webBuilder.UseStartup&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async (serviceProvider) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<IServiceProvider, Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(provider => new AppReadyCallbackResolver(provider, onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// using Microsoft.AspNetCore.Hosting;
/// using Microsoft.Extensions.Hosting;
/// using ElectronNET.API;
///
/// public class Program
/// {
/// public static void Main(string[] args)
/// {
/// Host.CreateDefaultBuilder(args)
/// .ConfigureWebHostDefaults(webBuilder =>
/// {
/// webBuilder.UseStartup&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async (serviceProvider, processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<IServiceProvider, string[], Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(provider => new AppReadyCallbackResolver(provider, args, onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
private static IWebHostBuilder UseElectronCore(IWebHostBuilder builder, string[] args)
{
// no matter how this is set - let's unset to prevent Electron not starting as expected // no matter how this is set - let's unset to prevent Electron not starting as expected
// e.g., VS Code sets this env variable, but this will cause `require("electron")` to not // e.g., VS Code sets this env variable, but this will cause `require("electron")` to not
// work as expected, see issue #952 // work as expected, see issue #952
@@ -70,14 +216,16 @@
var webPort = ElectronNetRuntime.AspNetWebPort ?? 0; var webPort = ElectronNetRuntime.AspNetWebPort ?? 0;
// check for the content folder if its exists in base director otherwise no need to include // In packaged mode, static content is deployed alongside the app binaries, so we must
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there. // point content root to the process base directory. In unpackaged/watch scenarios we
// now we have implemented the live reload if app is run using /watch then we need to use the default project path. // keep the default project content root to preserve live reload behavior.
var isPackagedStartup = ElectronNetRuntime.StartupMethod == StartupMethod.PackagedElectronFirst ||
ElectronNetRuntime.StartupMethod == StartupMethod.PackagedDotnetFirst;
// For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost // For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost
var host = webPort == 0 ? "127.0.0.1" : "localhost"; var host = webPort == 0 ? "127.0.0.1" : "localhost";
if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot")) if (isPackagedStartup)
{ {
builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseUrls($"http://{host}:{webPort}"); .UseUrls($"http://{host}:{webPort}");

View File

@@ -3,7 +3,7 @@
<Import Project="..\common.props" /> <Import Project="..\common.props" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks> <TargetFrameworks>$(ElectronNetTargetFrameworks)</TargetFrameworks>
<PackageOutputPath>..\..\artifacts</PackageOutputPath> <PackageOutputPath>..\..\artifacts</PackageOutputPath>
<PackageId>$(PackageNamePrefix).AspNet</PackageId> <PackageId>$(PackageNamePrefix).AspNet</PackageId>
<Title>$(PackageId)</Title> <Title>$(PackageId)</Title>
@@ -13,23 +13,6 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<RootNamespace>ElectronNET</RootNamespace> <RootNamespace>ElectronNET</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net6.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net6.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0|AnyCPU'">
<NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn> <NoWarn>1701;1702;4014;CS4014;CA1416;CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -17,13 +17,15 @@
{ {
private readonly IServer server; private readonly IServer server;
private readonly AspNetLifetimeAdapter aspNetLifetimeAdapter; private readonly AspNetLifetimeAdapter aspNetLifetimeAdapter;
private readonly IAppReadyCallbackResolver callbackResolver;
private readonly IElectronAuthenticationService authenticationService; private readonly IElectronAuthenticationService authenticationService;
private SocketBridgeService socketBridge; private SocketBridgeService socketBridge;
protected RuntimeControllerAspNetBase(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IElectronAuthenticationService authenticationService = null) protected RuntimeControllerAspNetBase(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IAppReadyCallbackResolver callbackResolver, IElectronAuthenticationService authenticationService = null)
{ {
this.server = server; this.server = server;
this.aspNetLifetimeAdapter = aspNetLifetimeAdapter; this.aspNetLifetimeAdapter = aspNetLifetimeAdapter;
this.callbackResolver = callbackResolver;
this.authenticationService = authenticationService; this.authenticationService = authenticationService;
this.aspNetLifetimeAdapter.Ready += this.AspNetLifetimeAdapter_Ready; this.aspNetLifetimeAdapter.Ready += this.AspNetLifetimeAdapter_Ready;
this.aspNetLifetimeAdapter.Stopping += this.AspNetLifetimeAdapter_Stopping; this.aspNetLifetimeAdapter.Stopping += this.AspNetLifetimeAdapter_Stopping;
@@ -130,20 +132,20 @@
private async Task RunReadyCallback() private async Task RunReadyCallback()
{ {
if (ElectronNetRuntime.OnAppReadyCallback == null) if (!callbackResolver.HasCallback)
{ {
Console.WriteLine("Warning: Non OnReadyCallback provided in UseElectron() setup."); Console.WriteLine("Warning: No OnReadyCallback provided in UseElectron() setup.");
return; return;
} }
try try
{ {
await ElectronNetRuntime.OnAppReadyCallback().ConfigureAwait(false); await callbackResolver.Invoke().ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine("Exception while executing OnAppReadyCallback. Stopping...\n" + ex); Console.WriteLine("Exception while executing OnAppReadyCallback. Stopping...\n" + ex);
this.Stop(); _ = this.Stop();
} }
} }
} }

View File

@@ -14,7 +14,7 @@
{ {
private ElectronProcessBase electronProcess; private ElectronProcessBase electronProcess;
public RuntimeControllerAspNetDotnetFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, authenticationService) public RuntimeControllerAspNetDotnetFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IAppReadyCallbackResolver callbackResolver, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, callbackResolver, authenticationService)
{ {
} }

View File

@@ -11,7 +11,7 @@
{ {
private ElectronProcessBase electronProcess; private ElectronProcessBase electronProcess;
public RuntimeControllerAspNetElectronFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, authenticationService) public RuntimeControllerAspNetElectronFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IAppReadyCallbackResolver callbackResolver, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, callbackResolver, authenticationService)
{ {
} }

View File

@@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
namespace ElectronNET.AspNet.Runtime
{
internal class AppReadyCallbackResolver : IAppReadyCallbackResolver
{
private readonly Func<Task> _callback;
public AppReadyCallbackResolver()
{ }
public AppReadyCallbackResolver(Func<Task> callback)
{
_callback = callback;
}
public AppReadyCallbackResolver(string[] args, Func<string[], Task> callback)
{
if (callback != null)
{
_callback = () => callback.Invoke(args);
}
}
public AppReadyCallbackResolver(IServiceProvider serviceProvider, Func<IServiceProvider, Task> callback)
{
if (callback != null)
{
_callback = () => callback.Invoke(serviceProvider);
}
}
public AppReadyCallbackResolver(IServiceProvider serviceProvider, string[] args, Func<IServiceProvider, string[], Task> callback)
{
if (callback != null)
{
_callback = () => callback.Invoke(serviceProvider, args);
}
}
public bool HasCallback => _callback != null;
public Task Invoke() => _callback?.Invoke() ?? Task.CompletedTask;
}
}

View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;
namespace ElectronNET.AspNet.Runtime
{
internal interface IAppReadyCallbackResolver
{
public bool HasCallback { get; }
public Task Invoke();
}
}

View File

@@ -70,7 +70,7 @@
<ProjectReference Include="..\ElectronNET.API\ElectronNET.API.csproj" Condition="$(ElectronNetDevMode)" /> <ProjectReference Include="..\ElectronNET.API\ElectronNET.API.csproj" Condition="$(ElectronNetDevMode)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.5.0" Condition="'$(ElectronNetDevMode)' != 'true'" /> <PackageReference Include="ElectronNET.Core" Version="0.6.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
</ItemGroup> </ItemGroup>
<Import Project="..\ElectronNET\build\ElectronNET.Core.targets" Condition="$(ElectronNetDevMode)" /> <Import Project="..\ElectronNET\build\ElectronNET.Core.targets" Condition="$(ElectronNetDevMode)" />

View File

@@ -247,9 +247,9 @@
} }
}, },
"node_modules/socket.io-parser": { "node_modules/socket.io-parser": {
"version": "4.2.6", "version": "4.2.7",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.3864779"> <Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.6537497">
<ItemGroup> <ItemGroup>
<None Include=".vscode\tasks.json" /> <None Include=".vscode\tasks.json" />
</ItemGroup> </ItemGroup>

View File

@@ -39,7 +39,6 @@ const windows = (global["browserWindows"] =
global["browserWindows"] || []); global["browserWindows"] || []);
let readyToShowWindowsIds = []; let readyToShowWindowsIds = [];
let window; let window;
let lastOptions;
let electronSocket; let electronSocket;
const proxyToCredentialsMap = (global["proxyToCredentialsMap"] = global["proxyToCredentialsMap"] || []); const proxyToCredentialsMap = (global["proxyToCredentialsMap"] = global["proxyToCredentialsMap"] || []);
module.exports = (socket, app) => { module.exports = (socket, app) => {
@@ -203,6 +202,18 @@ module.exports = (socket, app) => {
}); });
}); });
socket.on("createBrowserWindow", (options, loadUrl) => { socket.on("createBrowserWindow", (options, loadUrl) => {
createWindow(options, loadUrl, "BrowserWindowCreated");
});
// Allows the host (main.js) to bring the main window back when the app is activated
// again on macOS after all windows have been closed.
global["recreateMainWindow"] = () => {
if (electron_1.BrowserWindow.getAllWindows().length > 0 || !app["mainWindowOptions"]) {
return false;
}
createWindow(app["mainWindowOptions"], app["mainWindowURL"], "BrowserWindowRecreated");
return true;
};
function createWindow(options, loadUrl, createdEventName) {
if (options.webPreferences && if (options.webPreferences &&
!("nodeIntegration" in options.webPreferences)) { !("nodeIntegration" in options.webPreferences)) {
options = { options = {
@@ -226,18 +237,19 @@ module.exports = (socket, app) => {
delete options.isRunningBlazor; delete options.isRunningBlazor;
// we dont want to recreate the window when watch is ready. // we dont want to recreate the window when watch is ready.
if (app.commandLine.hasSwitch("watch") && if (app.commandLine.hasSwitch("watch") &&
app["mainWindowURL"] === loadUrl) { app["mainWindowURL"] === loadUrl &&
app["mainWindow"] &&
!app["mainWindow"].isDestroyed()) {
window = app["mainWindow"]; window = app["mainWindow"];
if (window) { window.reload();
window.reload(); synchronizeWindowRegistry();
if (!windows.some((entry) => tryGetWindowId(entry) === window.id)) {
windows.push(window); windows.push(window);
electronSocket.emit("BrowserWindowCreated", window.id);
return;
} }
electronSocket.emit(createdEventName, window.id);
return;
} }
else { window = new electron_1.BrowserWindow(options);
window = new electron_1.BrowserWindow(options);
}
if (options.proxy) { if (options.proxy) {
window.webContents.session.setProxy({ proxyRules: options.proxy }); window.webContents.session.setProxy({ proxyRules: options.proxy });
} }
@@ -252,36 +264,28 @@ module.exports = (socket, app) => {
readyToShowWindowsIds.push(window.id); readyToShowWindowsIds.push(window.id);
} }
}); });
lastOptions = options; window.on("closed", () => {
window.on("closed", (sender) => { synchronizeWindowRegistry();
for (let index = 0; index < windows.length; index++) { emitBrowserWindowClosed();
const windowItem = windows[index];
try {
windowItem.id;
}
catch (error) {
if (error.message === "Object has been destroyed") {
windows.splice(index, 1);
const ids = [];
windows.forEach((x) => ids.push(x.id));
electronSocket.emit("BrowserWindowClosed", ids);
}
}
}
});
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (window === null && lastOptions) {
window = new electron_1.BrowserWindow(lastOptions);
}
}); });
if (loadUrl) { if (loadUrl) {
// Append authentication token to initial URL if available // Append authentication token to initial URL if available
const token = global["authToken"]; const token = global["authToken"];
if (token) { if (token) {
const separator = loadUrl.includes("?") ? "&" : "?"; try {
window.loadURL(`${loadUrl}${separator}token=${token}`); const url = new URL(loadUrl);
const isLocal = url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname === "::1";
if (isLocal) {
url.searchParams.set("token", token);
}
window.loadURL(url.toString());
}
catch {
// Handle invalid URLs or file:// URLs if needed
window.loadURL(loadUrl);
}
} }
else { else {
window.loadURL(loadUrl); window.loadURL(loadUrl);
@@ -296,10 +300,14 @@ module.exports = (socket, app) => {
if (app["mainWindowURL"] == undefined || app["mainWindowURL"] == "") { if (app["mainWindowURL"] == undefined || app["mainWindowURL"] == "") {
app["mainWindowURL"] = loadUrl; app["mainWindowURL"] = loadUrl;
app["mainWindow"] = window; app["mainWindow"] = window;
app["mainWindowOptions"] = options;
}
else if (app["mainWindowURL"] === loadUrl) {
app["mainWindow"] = window;
} }
windows.push(window); windows.push(window);
electronSocket.emit("BrowserWindowCreated", window.id); electronSocket.emit(createdEventName, window.id);
}); }
socket.on("browserWindowDestroy", (id) => { socket.on("browserWindowDestroy", (id) => {
getWindowById(id).destroy(); getWindowById(id).destroy();
}); });
@@ -691,12 +699,47 @@ module.exports = (socket, app) => {
getWindowById(id).setBrowserView((0, browserView_1.browserViewMediateService)(browserViewId)); getWindowById(id).setBrowserView((0, browserView_1.browserViewMediateService)(browserViewId));
}); });
function getWindowById(id) { function getWindowById(id) {
const runtimeWindow = electron_1.BrowserWindow.fromId(id);
if (runtimeWindow) {
return runtimeWindow;
}
synchronizeWindowRegistry();
for (let index = 0; index < windows.length; index++) { for (let index = 0; index < windows.length; index++) {
const element = windows[index]; const element = windows[index];
if (element.id === id) { if (tryGetWindowId(element) === id) {
return element; return element;
} }
} }
throw new Error(`BrowserWindow with id '${id}' was not found.`);
}
function tryGetWindowId(element) {
try {
return element.id;
}
catch {
return null;
}
}
function synchronizeWindowRegistry() {
const runtimeWindows = electron_1.BrowserWindow.getAllWindows();
const runtimeWindowIds = new Set(runtimeWindows.map((entry) => entry.id));
for (let index = windows.length - 1; index >= 0; index--) {
const windowId = tryGetWindowId(windows[index]);
if (windowId === null || !runtimeWindowIds.has(windowId)) {
windows.splice(index, 1);
}
}
readyToShowWindowsIds = readyToShowWindowsIds.filter((entryId) => runtimeWindowIds.has(entryId));
}
function emitBrowserWindowClosed() {
const ids = [];
for (const entry of windows) {
const windowId = tryGetWindowId(entry);
if (windowId !== null) {
ids.push(windowId);
}
}
electronSocket.emit("BrowserWindowClosed", ids);
} }
}; };
//# sourceMappingURL=browserWindows.js.map //# sourceMappingURL=browserWindows.js.map

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,6 @@ const windows: Electron.BrowserWindow[] = (global["browserWindows"] =
let readyToShowWindowsIds: number[] = []; let readyToShowWindowsIds: number[] = [];
let window; let window;
let lastOptions;
let electronSocket; let electronSocket;
const proxyToCredentialsMap: { [proxy: string]: string } = (global[ const proxyToCredentialsMap: { [proxy: string]: string } = (global[
@@ -217,6 +216,26 @@ export = (socket: Socket, app: Electron.App) => {
}); });
socket.on("createBrowserWindow", (options, loadUrl) => { socket.on("createBrowserWindow", (options, loadUrl) => {
createWindow(options, loadUrl, "BrowserWindowCreated");
});
// Allows the host (main.js) to bring the main window back when the app is activated
// again on macOS after all windows have been closed.
global["recreateMainWindow"] = () => {
if (BrowserWindow.getAllWindows().length > 0 || !app["mainWindowOptions"]) {
return false;
}
createWindow(
app["mainWindowOptions"],
app["mainWindowURL"],
"BrowserWindowRecreated",
);
return true;
};
function createWindow(options, loadUrl, createdEventName: string) {
if ( if (
options.webPreferences && options.webPreferences &&
!("nodeIntegration" in options.webPreferences) !("nodeIntegration" in options.webPreferences)
@@ -250,19 +269,24 @@ export = (socket: Socket, app: Electron.App) => {
// we dont want to recreate the window when watch is ready. // we dont want to recreate the window when watch is ready.
if ( if (
app.commandLine.hasSwitch("watch") && app.commandLine.hasSwitch("watch") &&
app["mainWindowURL"] === loadUrl app["mainWindowURL"] === loadUrl &&
app["mainWindow"] &&
!app["mainWindow"].isDestroyed()
) { ) {
window = app["mainWindow"]; window = app["mainWindow"];
if (window) { window.reload();
window.reload(); synchronizeWindowRegistry();
if (!windows.some((entry) => tryGetWindowId(entry) === window.id)) {
windows.push(window); windows.push(window);
electronSocket.emit("BrowserWindowCreated", window.id);
return;
} }
} else {
window = new BrowserWindow(options); electronSocket.emit(createdEventName, window.id);
return;
} }
window = new BrowserWindow(options);
if (options.proxy) { if (options.proxy) {
window.webContents.session.setProxy({ proxyRules: options.proxy }); window.webContents.session.setProxy({ proxyRules: options.proxy });
} }
@@ -281,31 +305,9 @@ export = (socket: Socket, app: Electron.App) => {
} }
}); });
lastOptions = options; window.on("closed", () => {
synchronizeWindowRegistry();
window.on("closed", (sender) => { emitBrowserWindowClosed();
for (let index = 0; index < windows.length; index++) {
const windowItem = windows[index];
try {
windowItem.id;
} catch (error) {
if (error.message === "Object has been destroyed") {
windows.splice(index, 1);
const ids = [];
windows.forEach((x) => ids.push(x.id));
electronSocket.emit("BrowserWindowClosed", ids);
}
}
}
});
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (window === null && lastOptions) {
window = new BrowserWindow(lastOptions);
}
}); });
if (loadUrl) { if (loadUrl) {
@@ -313,8 +315,23 @@ export = (socket: Socket, app: Electron.App) => {
const token = global["authToken"]; const token = global["authToken"];
if (token) { if (token) {
const separator = loadUrl.includes("?") ? "&" : "?"; try {
window.loadURL(`${loadUrl}${separator}token=${token}`); const url = new URL(loadUrl);
const isLocal =
url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname === "::1";
if (isLocal) {
url.searchParams.set("token", token);
}
window.loadURL(url.toString());
} catch {
// Handle invalid URLs or file:// URLs if needed
window.loadURL(loadUrl);
}
} else { } else {
window.loadURL(loadUrl); window.loadURL(loadUrl);
} }
@@ -332,11 +349,14 @@ export = (socket: Socket, app: Electron.App) => {
if (app["mainWindowURL"] == undefined || app["mainWindowURL"] == "") { if (app["mainWindowURL"] == undefined || app["mainWindowURL"] == "") {
app["mainWindowURL"] = loadUrl; app["mainWindowURL"] = loadUrl;
app["mainWindow"] = window; app["mainWindow"] = window;
app["mainWindowOptions"] = options;
} else if (app["mainWindowURL"] === loadUrl) {
app["mainWindow"] = window;
} }
windows.push(window); windows.push(window);
electronSocket.emit("BrowserWindowCreated", window.id); electronSocket.emit(createdEventName, window.id);
}); }
socket.on("browserWindowDestroy", (id) => { socket.on("browserWindowDestroy", (id) => {
getWindowById(id).destroy(); getWindowById(id).destroy();
@@ -892,11 +912,57 @@ export = (socket: Socket, app: Electron.App) => {
}); });
function getWindowById(id: number): Electron.BrowserWindow { function getWindowById(id: number): Electron.BrowserWindow {
const runtimeWindow = BrowserWindow.fromId(id);
if (runtimeWindow) {
return runtimeWindow;
}
synchronizeWindowRegistry();
for (let index = 0; index < windows.length; index++) { for (let index = 0; index < windows.length; index++) {
const element = windows[index]; const element = windows[index];
if (element.id === id) { if (tryGetWindowId(element) === id) {
return element; return element;
} }
} }
throw new Error(`BrowserWindow with id '${id}' was not found.`);
}
function tryGetWindowId(element: Electron.BrowserWindow): number | null {
try {
return element.id;
} catch {
return null;
}
}
function synchronizeWindowRegistry(): void {
const runtimeWindows = BrowserWindow.getAllWindows();
const runtimeWindowIds = new Set(runtimeWindows.map((entry) => entry.id));
for (let index = windows.length - 1; index >= 0; index--) {
const windowId = tryGetWindowId(windows[index]);
if (windowId === null || !runtimeWindowIds.has(windowId)) {
windows.splice(index, 1);
}
}
readyToShowWindowsIds = readyToShowWindowsIds.filter((entryId) =>
runtimeWindowIds.has(entryId),
);
}
function emitBrowserWindowClosed(): void {
const ids: number[] = [];
for (const entry of windows) {
const windowId = tryGetWindowId(entry);
if (windowId !== null) {
ids.push(windowId);
}
}
electronSocket.emit("BrowserWindowClosed", ids);
} }
}; };

View File

@@ -111,6 +111,41 @@ module.exports = (socket) => {
electronSocket.emit("webContents-domReady" + id); electronSocket.emit("webContents-domReady" + id);
}); });
}); });
socket.on("register-webContents-zoomChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("zoom-changed");
browserWindow.webContents.on("zoom-changed", (_, zoomDirection) => {
electronSocket.emit("webContents-zoomChanged" + id, zoomDirection);
});
});
socket.on("register-webContents-foundInPage", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("found-in-page");
browserWindow.webContents.on("found-in-page", (_, result) => {
electronSocket.emit("webContents-foundInPage" + id, result);
});
});
socket.on("register-webContents-audioStateChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("audio-state-changed");
browserWindow.webContents.on("audio-state-changed", (event) => {
electronSocket.emit("webContents-audioStateChanged" + id, event.audible);
});
});
socket.on("register-webContents-mediaStartedPlaying", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-started-playing");
browserWindow.webContents.on("media-started-playing", () => {
electronSocket.emit("webContents-mediaStartedPlaying" + id);
});
});
socket.on("register-webContents-mediaPaused", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-paused");
browserWindow.webContents.on("media-paused", () => {
electronSocket.emit("webContents-mediaPaused" + id);
});
});
socket.on("webContents-openDevTools", (id, options) => { socket.on("webContents-openDevTools", (id, options) => {
if (options) { if (options) {
getWindowById(id).webContents.openDevTools(options); getWindowById(id).webContents.openDevTools(options);
@@ -308,6 +343,36 @@ module.exports = (socket) => {
electronSocket.emit("webContents-loadURL-error" + id, error); electronSocket.emit("webContents-loadURL-error" + id, error);
}); });
}); });
socket.on("webContents-loadFile", (id, filePath, options) => {
const browserWindow = getWindowById(id);
browserWindow.webContents
.loadFile(filePath, options ?? undefined)
.then(() => {
electronSocket.emit("webContents-loadFile-complete" + id);
})
.catch((error) => {
console.error(error);
electronSocket.emit("webContents-loadFile-error" + id, error);
});
});
socket.on("webContents-isLoading", (id) => {
electronSocket.emit("webContents-isLoading-completed", getWindowById(id).webContents.isLoading());
});
socket.on("webContents-isLoadingMainFrame", (id) => {
electronSocket.emit("webContents-isLoadingMainFrame-completed", getWindowById(id).webContents.isLoadingMainFrame());
});
socket.on("webContents-isWaitingForResponse", (id) => {
electronSocket.emit("webContents-isWaitingForResponse-completed", getWindowById(id).webContents.isWaitingForResponse());
});
socket.on("webContents-reload", (id) => {
getWindowById(id).webContents.reload();
});
socket.on("webContents-reloadIgnoringCache", (id) => {
getWindowById(id).webContents.reloadIgnoringCache();
});
socket.on("webContents-stop", (id) => {
getWindowById(id).webContents.stop();
});
socket.on("webContents-insertCSS", (id, isBrowserWindow, path) => { socket.on("webContents-insertCSS", (id, isBrowserWindow, path) => {
if (isBrowserWindow) { if (isBrowserWindow) {
const browserWindow = getWindowById(id); const browserWindow = getWindowById(id);
@@ -330,6 +395,14 @@ module.exports = (socket) => {
} }
} }
}); });
socket.on("webContents-insertCSSText", async (id, css, cssOrigin) => {
const key = await getWindowById(id).webContents.insertCSS(css, cssOrigin ? { cssOrigin } : undefined);
electronSocket.emit("webContents-insertCSSText-completed" + id, key);
});
socket.on("webContents-removeInsertedCSS", async (id, key) => {
await getWindowById(id).webContents.removeInsertedCSS(key);
electronSocket.emit("webContents-removeInsertedCSS-completed" + id);
});
socket.on("webContents-session-getAllExtensions", (id) => { socket.on("webContents-session-getAllExtensions", (id) => {
const browserWindow = getWindowById(id); const browserWindow = getWindowById(id);
const extensionsList = browserWindow.webContents.session.getAllExtensions(); const extensionsList = browserWindow.webContents.session.getAllExtensions();
@@ -403,6 +476,65 @@ module.exports = (socket) => {
socket.on("webContents-setUserAgent", (id, userAgent) => { socket.on("webContents-setUserAgent", (id, userAgent) => {
getWindowById(id).webContents.setUserAgent(userAgent); getWindowById(id).webContents.setUserAgent(userAgent);
}); });
socket.on("webContents-undo", (id) => {
getWindowById(id).webContents.undo();
});
socket.on("webContents-redo", (id) => {
getWindowById(id).webContents.redo();
});
socket.on("webContents-cut", (id) => {
getWindowById(id).webContents.cut();
});
socket.on("webContents-copy", (id) => {
getWindowById(id).webContents.copy();
});
socket.on("webContents-copyImageAt", (id, x, y) => {
getWindowById(id).webContents.copyImageAt(x, y);
});
socket.on("webContents-paste", (id) => {
getWindowById(id).webContents.paste();
});
socket.on("webContents-pasteAndMatchStyle", (id) => {
getWindowById(id).webContents.pasteAndMatchStyle();
});
socket.on("webContents-delete", (id) => {
getWindowById(id).webContents.delete();
});
socket.on("webContents-selectAll", (id) => {
getWindowById(id).webContents.selectAll();
});
socket.on("webContents-unselect", (id) => {
getWindowById(id).webContents.unselect();
});
socket.on("webContents-adjustSelection", (id, options) => {
getWindowById(id).webContents.adjustSelection(options ?? {});
});
socket.on("webContents-centerSelection", (id) => {
getWindowById(id).webContents.centerSelection();
});
socket.on("webContents-scrollToTop", (id) => {
getWindowById(id).webContents.scrollToTop();
});
socket.on("webContents-scrollToBottom", (id) => {
getWindowById(id).webContents.scrollToBottom();
});
socket.on("webContents-insertText", async (id, text) => {
await getWindowById(id).webContents.insertText(text);
electronSocket.emit("webContents-insertText-completed" + id);
});
socket.on("webContents-replace", (id, text) => {
getWindowById(id).webContents.replace(text);
});
socket.on("webContents-replaceMisspelling", (id, text) => {
getWindowById(id).webContents.replaceMisspelling(text);
});
socket.on("webContents-findInPage", (id, text, options) => {
const requestId = getWindowById(id).webContents.findInPage(text, options ?? undefined);
electronSocket.emit("webContents-findInPage-completed" + id, requestId);
});
socket.on("webContents-stopFindInPage", (id, action) => {
getWindowById(id).webContents.stopFindInPage(action);
});
function getWindowById(id) { function getWindowById(id) {
if (id >= 1000) { if (id >= 1000) {
return (0, browserView_1.browserViewMediateService)(id - 1000); return (0, browserView_1.browserViewMediateService)(id - 1000);

File diff suppressed because one or more lines are too long

View File

@@ -103,6 +103,51 @@ export = (socket: Socket) => {
}); });
}); });
socket.on("register-webContents-zoomChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("zoom-changed");
browserWindow.webContents.on("zoom-changed", (_, zoomDirection) => {
electronSocket.emit("webContents-zoomChanged" + id, zoomDirection);
});
});
socket.on("register-webContents-foundInPage", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("found-in-page");
browserWindow.webContents.on("found-in-page", (_, result) => {
electronSocket.emit("webContents-foundInPage" + id, result);
});
});
socket.on("register-webContents-audioStateChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("audio-state-changed");
browserWindow.webContents.on("audio-state-changed", (event) => {
electronSocket.emit("webContents-audioStateChanged" + id, event.audible);
});
});
socket.on("register-webContents-mediaStartedPlaying", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-started-playing");
browserWindow.webContents.on("media-started-playing", () => {
electronSocket.emit("webContents-mediaStartedPlaying" + id);
});
});
socket.on("register-webContents-mediaPaused", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-paused");
browserWindow.webContents.on("media-paused", () => {
electronSocket.emit("webContents-mediaPaused" + id);
});
});
socket.on("webContents-openDevTools", (id, options) => { socket.on("webContents-openDevTools", (id, options) => {
if (options) { if (options) {
getWindowById(id).webContents.openDevTools(options); getWindowById(id).webContents.openDevTools(options);
@@ -409,6 +454,52 @@ export = (socket: Socket) => {
}); });
}); });
socket.on("webContents-loadFile", (id, filePath, options) => {
const browserWindow = getWindowById(id);
browserWindow.webContents
.loadFile(filePath, options ?? undefined)
.then(() => {
electronSocket.emit("webContents-loadFile-complete" + id);
})
.catch((error) => {
console.error(error);
electronSocket.emit("webContents-loadFile-error" + id, error);
});
});
socket.on("webContents-isLoading", (id) => {
electronSocket.emit(
"webContents-isLoading-completed",
getWindowById(id).webContents.isLoading(),
);
});
socket.on("webContents-isLoadingMainFrame", (id) => {
electronSocket.emit(
"webContents-isLoadingMainFrame-completed",
getWindowById(id).webContents.isLoadingMainFrame(),
);
});
socket.on("webContents-isWaitingForResponse", (id) => {
electronSocket.emit(
"webContents-isWaitingForResponse-completed",
getWindowById(id).webContents.isWaitingForResponse(),
);
});
socket.on("webContents-reload", (id) => {
getWindowById(id).webContents.reload();
});
socket.on("webContents-reloadIgnoringCache", (id) => {
getWindowById(id).webContents.reloadIgnoringCache();
});
socket.on("webContents-stop", (id) => {
getWindowById(id).webContents.stop();
});
socket.on("webContents-insertCSS", (id, isBrowserWindow, path) => { socket.on("webContents-insertCSS", (id, isBrowserWindow, path) => {
if (isBrowserWindow) { if (isBrowserWindow) {
const browserWindow = getWindowById(id); const browserWindow = getWindowById(id);
@@ -431,6 +522,19 @@ export = (socket: Socket) => {
} }
}); });
socket.on("webContents-insertCSSText", async (id, css, cssOrigin) => {
const key = await getWindowById(id).webContents.insertCSS(
css,
cssOrigin ? { cssOrigin } : undefined,
);
electronSocket.emit("webContents-insertCSSText-completed" + id, key);
});
socket.on("webContents-removeInsertedCSS", async (id, key) => {
await getWindowById(id).webContents.removeInsertedCSS(key);
electronSocket.emit("webContents-removeInsertedCSS-completed" + id);
});
socket.on("webContents-session-getAllExtensions", (id) => { socket.on("webContents-session-getAllExtensions", (id) => {
const browserWindow = getWindowById(id); const browserWindow = getWindowById(id);
const extensionsList = browserWindow.webContents.session.getAllExtensions(); const extensionsList = browserWindow.webContents.session.getAllExtensions();
@@ -557,6 +661,87 @@ export = (socket: Socket) => {
getWindowById(id).webContents.setUserAgent(userAgent); getWindowById(id).webContents.setUserAgent(userAgent);
}); });
socket.on("webContents-undo", (id) => {
getWindowById(id).webContents.undo();
});
socket.on("webContents-redo", (id) => {
getWindowById(id).webContents.redo();
});
socket.on("webContents-cut", (id) => {
getWindowById(id).webContents.cut();
});
socket.on("webContents-copy", (id) => {
getWindowById(id).webContents.copy();
});
socket.on("webContents-copyImageAt", (id, x, y) => {
getWindowById(id).webContents.copyImageAt(x, y);
});
socket.on("webContents-paste", (id) => {
getWindowById(id).webContents.paste();
});
socket.on("webContents-pasteAndMatchStyle", (id) => {
getWindowById(id).webContents.pasteAndMatchStyle();
});
socket.on("webContents-delete", (id) => {
getWindowById(id).webContents.delete();
});
socket.on("webContents-selectAll", (id) => {
getWindowById(id).webContents.selectAll();
});
socket.on("webContents-unselect", (id) => {
getWindowById(id).webContents.unselect();
});
socket.on("webContents-adjustSelection", (id, options) => {
getWindowById(id).webContents.adjustSelection(options ?? {});
});
socket.on("webContents-centerSelection", (id) => {
getWindowById(id).webContents.centerSelection();
});
socket.on("webContents-scrollToTop", (id) => {
getWindowById(id).webContents.scrollToTop();
});
socket.on("webContents-scrollToBottom", (id) => {
getWindowById(id).webContents.scrollToBottom();
});
socket.on("webContents-insertText", async (id, text) => {
await getWindowById(id).webContents.insertText(text);
electronSocket.emit("webContents-insertText-completed" + id);
});
socket.on("webContents-replace", (id, text) => {
getWindowById(id).webContents.replace(text);
});
socket.on("webContents-replaceMisspelling", (id, text) => {
getWindowById(id).webContents.replaceMisspelling(text);
});
socket.on("webContents-findInPage", (id, text, options) => {
const requestId = getWindowById(id).webContents.findInPage(
text,
options ?? undefined,
);
electronSocket.emit("webContents-findInPage-completed" + id, requestId);
});
socket.on("webContents-stopFindInPage", (id, action) => {
getWindowById(id).webContents.stopFindInPage(action);
});
function getWindowById( function getWindowById(
id: number, id: number,
): Electron.BrowserWindow | Electron.BrowserView { ): Electron.BrowserWindow | Electron.BrowserView {

View File

@@ -29,32 +29,11 @@ let electronforcedport;
let electronUrl; let electronUrl;
let authToken = randomUUID().split('-').join(''); let authToken = randomUUID().split('-').join('');
if (app.commandLine.hasSwitch('manifest')) {
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
}
if (app.commandLine.hasSwitch('unpackedelectron')) {
unpackedelectron = true;
}
else if (app.commandLine.hasSwitch('unpackeddotnet')) {
unpackeddotnet = true;
}
else if (app.commandLine.hasSwitch('dotnetpacked')) {
dotnetpacked = true;
}
if (app.commandLine.hasSwitch('electronforcedport')) {
electronforcedport = +app.commandLine.getSwitchValue('electronforcedport');
}
// Store in global for access by browser windows // Store in global for access by browser windows
global.authToken = authToken; global.authToken = authToken;
if (app.commandLine.hasSwitch('electronurl')) {
electronUrl = app.commandLine.getSwitchValue('electronurl');
}
// Custom startup hook: look for custom_main.js and invoke its onStartup(host) if present. // Custom startup hook: look for custom_main.js and invoke its onStartup(host) if present.
// Runs before any command line switch is evaluated so the hook can still modify them.
// If the hook returns false, abort Electron startup. // If the hook returns false, abort Electron startup.
try { try {
const fs = require('fs'); const fs = require('fs');
@@ -77,6 +56,28 @@ try {
console.error('Error while executing custom_main.js:', err); console.error('Error while executing custom_main.js:', err);
} }
if (app.commandLine.hasSwitch('manifest')) {
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
}
if (app.commandLine.hasSwitch('unpackedelectron')) {
unpackedelectron = true;
}
else if (app.commandLine.hasSwitch('unpackeddotnet')) {
unpackeddotnet = true;
}
else if (app.commandLine.hasSwitch('dotnetpacked')) {
dotnetpacked = true;
}
if (app.commandLine.hasSwitch('electronforcedport')) {
electronforcedport = +app.commandLine.getSwitchValue('electronforcedport');
}
if (app.commandLine.hasSwitch('electronurl')) {
electronUrl = app.commandLine.getSwitchValue('electronurl');
}
const currentPath = __dirname; const currentPath = __dirname;
let currentBinPath = path.join(currentPath.replace('app.asar', ''), 'bin'); let currentBinPath = path.join(currentPath.replace('app.asar', ''), 'bin');
let manifestJsonFilePath = path.join(currentPath, manifestJsonFileName); let manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
@@ -103,9 +104,44 @@ app.on('will-finish-launching', () => {
const manifestJsonFile = require(manifestJsonFilePath); const manifestJsonFile = require(manifestJsonFilePath);
// Brings the app back to the foreground: focuses an already existing window or - if
// all windows have been closed (which keeps the app alive on macOS) - recreates the
// main window.
function activateApp() {
const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed());
if (!windows.length) {
return typeof global.recreateMainWindow === 'function' && global.recreateMainWindow();
}
const target = windows.find((window) => window.isVisible()) || windows[0];
if (target.isMinimized()) {
target.restore();
}
if (!target.isVisible()) {
target.show();
}
target.focus();
if (platform() === 'darwin') {
// On macOS focusing a window does not necessarily bring the app itself to the front
app.focus({ steal: true });
}
return true;
}
if (manifestJsonFile.singleInstance) { if (manifestJsonFile.singleInstance) {
const mainInstance = app.requestSingleInstanceLock(); if (!app.requestSingleInstanceLock()) {
app.on('second-instance', (events, args = []) => { // Another instance already owns the lock. Exit right away so that neither the
// socket bridge nor the .NET backend process of this instance is started.
app.exit(0);
}
app.on('second-instance', (event, args = []) => {
args.forEach((parameter) => { args.forEach((parameter) => {
const words = parameter.split('='); const words = parameter.split('=');
@@ -116,20 +152,16 @@ if (manifestJsonFile.singleInstance) {
} }
}); });
const windows = BrowserWindow.getAllWindows(); activateApp();
if (windows.length) {
if (windows[0].isMinimized()) {
windows[0].restore();
}
windows[0].focus();
}
}); });
if (!mainInstance) {
app.quit();
}
} }
// On macOS launching an already running app (or clicking its dock icon) does not
// start a second instance - the 'activate' event is raised instead.
app.on('activate', () => {
activateApp();
});
// Collect user supplied command line args (excluding those handled by Electron host itself) // Collect user supplied command line args (excluding those handled by Electron host itself)
function getForwardedArgs() { function getForwardedArgs() {
const skipSwitches = new Set(['unpackedelectron', 'unpackeddotnet', 'dotnetpacked']); const skipSwitches = new Set(['unpackedelectron', 'unpackeddotnet', 'dotnetpacked']);
@@ -144,6 +176,7 @@ function getForwardedArgs() {
if (cleaned.startsWith('remote-debugging-port')) return false; if (cleaned.startsWith('remote-debugging-port')) return false;
// We add /electronPort ourselves later // We add /electronPort ourselves later
if (cleaned.startsWith('electronPort=')) return false; if (cleaned.startsWith('electronPort=')) return false;
if (cleaned.startsWith('electronHost=')) return false;
if (cleaned.startsWith('electronWebPort=')) return false; if (cleaned.startsWith('electronWebPort=')) return false;
return true; return true;
}); });
@@ -264,7 +297,6 @@ function startSocketApiBridge(port) {
// otherwise the Windows Firewall will be triggered // otherwise the Windows Firewall will be triggered
console.debug('Electron Socket: starting...'); console.debug('Electron Socket: starting...');
server = createServer(); server = createServer();
const host = !port ? '127.0.0.1' : 'localhost';
let hostHook; let hostHook;
io = new Server({ io = new Server({
pingTimeout: 60000, // in ms, default is 5000 pingTimeout: 60000, // in ms, default is 5000
@@ -272,16 +304,35 @@ function startSocketApiBridge(port) {
}); });
io.attach(server); io.attach(server);
server.listen(port, host); // Never bind to the 'localhost' hostname: it may resolve to ::1 and 127.0.0.1 in any
// order, so server and client can end up on different stacks - which costs a failed
// connection attempt (or a DNS lookup) on every startup.
const hostCandidates = ['127.0.0.1', '::1'];
let hostIndex = 0;
server.on('error', (error) => {
const isUnavailable = error.code === 'EADDRNOTAVAIL' || error.code === 'EAFNOSUPPORT' || error.code === 'EINVAL';
if (isUnavailable && hostIndex + 1 < hostCandidates.length) {
console.warn(`Electron Socket: cannot bind to ${hostCandidates[hostIndex]} (${error.code}), falling back to ${hostCandidates[hostIndex + 1]}.`);
hostIndex++;
server.listen(port, hostCandidates[hostIndex]);
return;
}
console.error('Electron Socket: ' + error.message);
});
server.listen(port, hostCandidates[hostIndex]);
server.on('listening', function () { server.on('listening', function () {
const addr = server.address(); const addr = server.address();
console.info(`Electron Socket: listening on port ${addr.port} at ${addr.address} using ${authToken}`); console.info(`Electron Socket: listening on port ${addr.port} at ${addr.address} using ${authToken}`);
// Now that socket connection is established, we can guarantee port will not be open for portscanner // Now that socket connection is established, we can guarantee port will not be open for portscanner
if (unpackedelectron) { if (unpackedelectron) {
startAspCoreBackendUnpackaged(addr.port); startAspCoreBackendUnpackaged(addr.port, addr.address);
} else if (!unpackeddotnet && !dotnetpacked) { } else if (!unpackeddotnet && !dotnetpacked) {
startAspCoreBackend(addr.port); startAspCoreBackend(addr.port, addr.address);
} }
}); });
@@ -385,7 +436,7 @@ function startSocketApiBridge(port) {
}); });
} }
function startAspCoreBackend(electronPort) { function startAspCoreBackend(electronPort, electronHost) {
startBackend(); startBackend();
function startBackend() { function startBackend() {
@@ -394,6 +445,7 @@ function startAspCoreBackend(electronPort) {
const parameters = [ const parameters = [
envParam, envParam,
`/electronPort=${electronPort}`, `/electronPort=${electronPort}`,
`/electronHost=${electronHost}`,
`/electronPID=${process.pid}`, `/electronPID=${process.pid}`,
`/electronAuthToken=${authToken}`, `/electronAuthToken=${authToken}`,
// forward user supplied args (avoid duplicate environment) // forward user supplied args (avoid duplicate environment)
@@ -416,7 +468,7 @@ function startAspCoreBackend(electronPort) {
} }
} }
function startAspCoreBackendUnpackaged(electronPort) { function startAspCoreBackendUnpackaged(electronPort, electronHost) {
startBackend(); startBackend();
function startBackend() { function startBackend() {
@@ -425,6 +477,7 @@ function startAspCoreBackendUnpackaged(electronPort) {
const parameters = [ const parameters = [
envParam, envParam,
`/electronPort=${electronPort}`, `/electronPort=${electronPort}`,
`/electronHost=${electronHost}`,
`/electronPID=${process.pid}`, `/electronPID=${process.pid}`,
`/electronAuthToken=${authToken}`, `/electronAuthToken=${authToken}`,
...forwardedArgs.filter(a => !(envParam && a.startsWith('--environment='))) ...forwardedArgs.filter(a => !(envParam && a.startsWith('--environment=')))

View File

@@ -11,13 +11,13 @@
"dependencies": { "dependencies": {
"dasherize": "^2.0.0", "dasherize": "^2.0.0",
"electron-host-hook": "file:./ElectronHostHook", "electron-host-hook": "file:./ElectronHostHook",
"electron-updater": "^6.6.2", "electron-updater": "^6.8.9",
"image-size": "^1.2.1", "image-size": "^1.2.1",
"socket.io": "^4.8.1" "socket.io": "^4.8.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.18", "@types/node": "^22.18",
"electron": "^30.0.3", "electron": "^39.8.10",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
@@ -476,9 +476,9 @@
"optional": true "optional": true
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -497,9 +497,9 @@
} }
}, },
"node_modules/builder-util-runtime": { "node_modules/builder-util-runtime": {
"version": "9.5.1", "version": "9.7.0",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -762,15 +762,15 @@
"optional": true "optional": true
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "30.5.1", "version": "39.8.10",
"resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.10.tgz",
"integrity": "sha512-AhL7+mZ8Lg14iaNfoYTkXQ2qee8mmsQyllKdqxlpv/zrKgfxz6jNVtcRRbQtLxtF8yzcImWdfTQROpYiPumdbw==", "integrity": "sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^20.9.0", "@types/node": "^22.7.7",
"extract-zip": "^2.0.1" "extract-zip": "^2.0.1"
}, },
"bin": { "bin": {
@@ -785,12 +785,12 @@
"link": true "link": true
}, },
"node_modules/electron-updater": { "node_modules/electron-updater": {
"version": "6.8.3", "version": "6.8.9",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz",
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"builder-util-runtime": "9.5.1", "builder-util-runtime": "9.7.0",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lazy-val": "^1.0.5", "lazy-val": "^1.0.5",
@@ -847,16 +847,6 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/electron/node_modules/@types/node": {
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -868,9 +858,9 @@
} }
}, },
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.6.8", "version": "6.6.9",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz",
"integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==", "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
@@ -882,7 +872,7 @@
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.4.1", "debug": "~4.4.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
"ws": "~8.20.1" "ws": "~8.21.0"
}, },
"engines": { "engines": {
"node": ">=10.2.0" "node": ">=10.2.0"
@@ -1495,9 +1485,19 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -1957,9 +1957,9 @@
} }
}, },
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.4", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=11.0.0" "node": ">=11.0.0"
@@ -2042,19 +2042,19 @@
} }
}, },
"node_modules/socket.io-adapter": { "node_modules/socket.io-adapter": {
"version": "2.5.7", "version": "2.5.8",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
"integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==", "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "~4.4.1", "debug": "~4.4.1",
"ws": "~8.20.1" "ws": "~8.21.0"
} }
}, },
"node_modules/socket.io-parser": { "node_modules/socket.io-parser": {
"version": "4.2.5", "version": "4.2.7",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
@@ -2227,9 +2227,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.20.1", "version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@@ -19,12 +19,12 @@
"dasherize": "^2.0.0", "dasherize": "^2.0.0",
"electron-host-hook": "file:./ElectronHostHook", "electron-host-hook": "file:./ElectronHostHook",
"image-size": "^1.2.1", "image-size": "^1.2.1",
"electron-updater": "^6.6.2", "electron-updater": "^6.8.9",
"socket.io": "^4.8.1" "socket.io": "^4.8.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.18", "@types/node": "^22.18",
"electron": "^30.0.3", "electron": "^39.8.10",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<!-- When this is enabled, the project will be switched from nuget packages to consuming the ElectronNet orchestration directly --> <!-- When this is enabled, the project will be switched from nuget packages to consuming the ElectronNet orchestration directly -->
<ElectronNetDevMode>true</ElectronNetDevMode> <ElectronNetDevMode>true</ElectronNetDevMode>
<IsTest>True</IsTest>
</PropertyGroup> </PropertyGroup>
<Import Project="..\ElectronNET\build\ElectronNET.Core.props" Condition="$(ElectronNetDevMode)" /> <Import Project="..\ElectronNET\build\ElectronNET.Core.props" Condition="$(ElectronNetDevMode)" />

View File

@@ -0,0 +1,108 @@
using System.Diagnostics;
namespace ElectronNET.IntegrationTests.Tests;
/// <summary>
/// Tests for the Electron clean targets.
/// Covers GitHub issue #1096: cleaning must not require a restore, so the clean targets
/// must not depend on targets that need the assets file (NETSDK1004).
/// </summary>
public class ElectronCleanTargetsTests
{
private static readonly string CorePropsPath = FindBuildFile("src/ElectronNET/build/ElectronNET.Core.props", "ElectronNET/build/ElectronNET.Core.props");
private static readonly string CoreTargetsPath = FindBuildFile("src/ElectronNET/build/ElectronNET.Core.targets", "ElectronNET/build/ElectronNET.Core.targets");
[Fact]
public async Task ElectronCleanTargets_WithoutRestore_ShouldSucceed()
{
var tempDir = CreateTempProjectDirectory();
try
{
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetMsBuildAsync(tempDir, "Clean");
output.Should().NotContain("NETSDK1004",
$"cleaning must not require the assets file of a previous restore. Full output:\n{output}");
exitCode.Should().Be(0,
$"cleaning an unrestored project must succeed. Full output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
private static string FindBuildFile(string relativeFromRepoRoot, string relativeFromSrc)
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var fromRepoRoot = Path.Combine(dir.FullName, relativeFromRepoRoot);
if (File.Exists(fromRepoRoot))
{
return Path.GetFullPath(fromRepoRoot);
}
var fromSrc = Path.Combine(dir.FullName, relativeFromSrc);
if (File.Exists(fromSrc))
{
return Path.GetFullPath(fromSrc);
}
dir = dir.Parent;
}
throw new FileNotFoundException(
$"Could not locate '{relativeFromRepoRoot}' by walking up from '{AppContext.BaseDirectory}'.");
}
private static string CreateTempProjectDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-clean-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
Directory.CreateDirectory(Path.Combine(tempDir, "Properties"));
return tempDir;
}
private static Task WriteMinimalCsprojAsync(string tempDir)
{
var propsPathEscaped = CorePropsPath.Replace("'", "&apos;");
var targetsPathEscaped = CoreTargetsPath.Replace("'", "&apos;");
return File.WriteAllTextAsync(
Path.Combine(tempDir, "TestApp.csproj"),
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<Import Project="{{propsPathEscaped}}" />
<Import Project="{{targetsPathEscaped}}" />
</Project>
""");
}
private static async Task<(int ExitCode, string Output)> RunDotnetMsBuildAsync(string workingDirectory, string target)
{
// Deliberately without /restore - that is what issue #1096 is about.
var psi = new ProcessStartInfo("dotnet", $"msbuild TestApp.csproj --nologo -v:minimal /t:{target}")
{
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using var process = Process.Start(psi)!;
var stdOut = await process.StandardOutput.ReadToEndAsync();
var stdErr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, stdOut + stdErr);
}
}

View File

@@ -0,0 +1,133 @@
using System.Diagnostics;
namespace ElectronNET.IntegrationTests.Tests;
/// <summary>
/// Tests for ElectronNET.Core.targets icon copy behavior.
/// Covers GitHub issue #1047: modern macOS .icon app-icon packages are folders
/// and must be copied recursively, not treated as a single file.
/// </summary>
public class ElectronIconTargetsTests
{
private static readonly string CorePropsPath = FindBuildFile("src/ElectronNET/build/ElectronNET.Core.props", "ElectronNET/build/ElectronNET.Core.props");
private static readonly string CoreTargetsPath = FindBuildFile("src/ElectronNET/build/ElectronNET.Core.targets", "ElectronNET/build/ElectronNET.Core.targets");
[Fact]
public async Task ElectronCoreTargets_ElectronIconDirectory_ShouldBeMappedIntoElectronOutput()
{
var tempDir = CreateTempProjectDirectory();
try
{
var iconPackageDir = Path.Combine(tempDir, "Assets", "MyApp.icon");
Directory.CreateDirectory(Path.Combine(iconPackageDir, "layers"));
await File.WriteAllTextAsync(Path.Combine(iconPackageDir, "manifest.json"), "{}");
await File.WriteAllTextAsync(Path.Combine(iconPackageDir, "layers", "foreground.png"), "not-a-real-png");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetMsBuildAsync(tempDir, "DumpElectronIconCopyItems");
var normalizedOutput = NormalizePathSeparators(output);
exitCode.Should().Be(0,
$"MSBuild target evaluation must succeed. Full output:\n{output}");
normalizedOutput.Should().Contain(
".electron/MyApp.icon/manifest.json",
$"the icon package root file must be mapped into .electron/MyApp.icon. Full output:\n{output}");
normalizedOutput.Should().Contain(
".electron/MyApp.icon/layers/foreground.png",
$"nested files in .icon package must preserve structure in .electron output. Full output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
private static string FindBuildFile(string relativeFromRepoRoot, string relativeFromSrc)
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var fromRepoRoot = Path.Combine(dir.FullName, relativeFromRepoRoot);
if (File.Exists(fromRepoRoot))
{
return Path.GetFullPath(fromRepoRoot);
}
var fromSrc = Path.Combine(dir.FullName, relativeFromSrc);
if (File.Exists(fromSrc))
{
return Path.GetFullPath(fromSrc);
}
dir = dir.Parent;
}
throw new FileNotFoundException(
$"Could not locate '{relativeFromRepoRoot}' by walking up from '{AppContext.BaseDirectory}'.");
}
private static string CreateTempProjectDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-icon-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
Directory.CreateDirectory(Path.Combine(tempDir, "Properties"));
return tempDir;
}
private static Task WriteMinimalCsprojAsync(string tempDir)
{
var propsPathEscaped = CorePropsPath.Replace("'", "&apos;");
var targetsPathEscaped = CoreTargetsPath.Replace("'", "&apos;");
return File.WriteAllTextAsync(
Path.Combine(tempDir, "TestApp.csproj"),
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<Import Project="{{propsPathEscaped}}" />
<PropertyGroup Label="ElectronNetCommon">
<ElectronIcon>Assets/MyApp.icon</ElectronIcon>
</PropertyGroup>
<Import Project="{{targetsPathEscaped}}" />
<Target Name="DumpElectronIconCopyItems"
DependsOnTargets="ElectronResolvePaths;ElectronGetCopyToOutputDirectoryItems">
<Message Importance="High"
Text="ELECTRON_COPY_ITEMS: @(_ElectronFilesToCopyWithTargetPath->'%(TargetPath)')" />
</Target>
</Project>
""");
}
private static async Task<(int ExitCode, string Output)> RunDotnetMsBuildAsync(string workingDirectory, string target)
{
var psi = new ProcessStartInfo("dotnet", $"msbuild TestApp.csproj --nologo -v:minimal /restore /t:{target}")
{
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using var process = Process.Start(psi)!;
var stdOut = await process.StandardOutput.ReadToEndAsync();
var stdErr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, stdOut + stdErr);
}
private static string NormalizePathSeparators(string value)
{
return value.Replace('\\', '/');
}
}

View File

@@ -145,6 +145,111 @@ public class MigrationChecksTargetsTests
} }
} }
[Fact]
public async Task MigrationChecksTargets_BuildWithRootPackageLockJson_ShouldNotEmitELECTRON001Warning()
{
// A root package.json is validated by ELECTRON008/ELECTRON009 and therefore allowed.
// Its accompanying package-lock.json must not be reported by ELECTRON001.
var tempDir = CreateTempProjectDirectory();
try
{
await File.WriteAllTextAsync(
Path.Combine(tempDir, "package.json"),
"""{ "devDependencies": { "vite": "^5.0.0" } }""");
await File.WriteAllTextAsync(
Path.Combine(tempDir, "package-lock.json"),
"""{ "lockfileVersion": 3 }""");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0, $"Full build output:\n{output}");
output.Should().NotContain(
"ELECTRON001",
$"a root package-lock.json belongs to the allowed root package.json. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task MigrationChecksTargets_BuildWithIncompleteHostHookFolder_ShouldEmitELECTRON010AndELECTRON011Warnings()
{
// Host hook TypeScript sources are silently ignored when package.json/tsconfig.json
// are missing, so the migration checks must point that out.
var tempDir = CreateTempProjectDirectory();
try
{
var hookDir = Path.Combine(tempDir, "ElectronHostHook");
Directory.CreateDirectory(hookDir);
await File.WriteAllTextAsync(Path.Combine(hookDir, "index.ts"), "export class HookService {}");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0, $"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON010",
$"the ElectronHostHook folder has TypeScript files but no package.json. " +
$"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON011",
$"the ElectronHostHook folder has TypeScript files but no tsconfig.json. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task MigrationChecksTargets_BuildWithConsoleProfileContainingProjectGuid_ShouldEmitELECTRON007Warning()
{
// ProjectGuid is an ASP.NET publish profile property and must be reported for
// non-web projects, just like WebPublishMethod.
var tempDir = CreateTempProjectDirectory();
try
{
var profilesDir = Path.Combine(tempDir, "Properties", "PublishProfiles");
Directory.CreateDirectory(profilesDir);
await File.WriteAllTextAsync(
Path.Combine(profilesDir, "FolderProfile.pubxml"),
"""
<Project>
<PropertyGroup>
<ProjectGuid>00000000-0000-0000-0000-000000000000</ProjectGuid>
</PropertyGroup>
</Project>
""");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0, $"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON007",
$"a console project must not use an ASP.NET publish profile. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Helpers // Helpers
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View File

@@ -108,13 +108,19 @@ namespace ElectronNET.IntegrationTests.Tests
await Task.Delay(500.ms()); await Task.Delay(500.ms());
var ok = await window.WebContents.GetZoomLevelAsync(); var ok = await window.WebContents.GetZoomLevelAsync();
ok.Should().Be(0); ok.Should().Be(0.0);
window.WebContents.SetZoomLevel(2); window.WebContents.SetZoomLevel(2);
await Task.Delay(500.ms()); await Task.Delay(500.ms());
ok = await window.WebContents.GetZoomLevelAsync(); ok = await window.WebContents.GetZoomLevelAsync();
ok.Should().Be(2); ok.Should().Be(2.0);
window.WebContents.SetZoomLevel(0.5);
await Task.Delay(500.ms());
ok = await window.WebContents.GetZoomLevelAsync();
ok.Should().BeApproximately(0.5, 0.001);
} }
finally finally
{ {
@@ -193,5 +199,94 @@ namespace ElectronNET.IntegrationTests.Tests
} }
} }
[IntegrationFact]
public async Task InsertAndRemoveCSS_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><p>CSS Test</p></body></html>");
var key = await wc.InsertCSSAsync("body { background-color: rgb(255, 0, 0); }");
key.Should().NotBeNullOrEmpty();
var color = await wc.ExecuteJavaScriptAsync<string>("getComputedStyle(document.body).backgroundColor");
color.Should().Be("rgb(255, 0, 0)");
await wc.RemoveInsertedCSSAsync(key);
color = await wc.ExecuteJavaScriptAsync<string>("getComputedStyle(document.body).backgroundColor");
color.Should().NotBe("rgb(255, 0, 0)");
}
[IntegrationFact]
public async Task EditCommands_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><input id='in' autofocus /></body></html>");
await Task.Delay(500.ms());
await wc.InsertTextAsync("Electron.NET");
await Task.Delay(500.ms());
var value = await wc.ExecuteJavaScriptAsync<string>("document.getElementById('in').value");
value.Should().Be("Electron.NET");
wc.SelectAll();
wc.Delete();
await Task.Delay(500.ms());
value = await wc.ExecuteJavaScriptAsync<string>("document.getElementById('in').value");
value.Should().BeEmpty();
}
[IntegrationFact]
public async Task FindInPage_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><p>needle in a haystack</p></body></html>");
await Task.Delay(500.ms());
var tcs = new TaskCompletionSource<FoundInPageResult>();
void OnFound(FoundInPageResult result) => tcs.TrySetResult(result);
wc.OnFoundInPage += OnFound;
try
{
var requestId = await wc.FindInPageAsync("needle");
requestId.Should().BeGreaterThan(0);
var completed = await Task.WhenAny(tcs.Task, Task.Delay(5.seconds()));
completed.Should().Be(tcs.Task);
var result = await tcs.Task;
result.RequestId.Should().Be(requestId);
result.Matches.Should().BeGreaterThan(0);
}
finally
{
wc.OnFoundInPage -= OnFound;
wc.StopFindInPage(StopFindInPageAction.ClearSelection);
}
}
[IntegrationFact]
public async Task Reload_and_loading_state_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><h1>Reload Test</h1></body></html>");
await Task.Delay(500.ms());
(await wc.IsLoadingAsync()).Should().BeFalse();
(await wc.IsLoadingMainFrameAsync()).Should().BeFalse();
(await wc.IsWaitingForResponseAsync()).Should().BeFalse();
wc.Reload();
await Task.Delay(1.seconds());
var title = await wc.ExecuteJavaScriptAsync<string>("document.querySelector('h1').textContent");
title.Should().Be("Reload Test");
}
} }
} }

View File

@@ -0,0 +1,55 @@
namespace ElectronNET.IntegrationTests.Tests;
/// <summary>
/// Regression checks for BrowserWindow lifecycle cleanup in WindowManager.
/// Covers GitHub issue #1008.
/// </summary>
public class WindowManagerLifecycleTests
{
private static readonly string WindowManagerFilePath = FindWindowManagerFile();
[Fact]
public void WindowManager_ShouldUsePersistentBrowserWindowClosedSubscription()
{
File.Exists(WindowManagerFilePath).Should().BeTrue(
$"WindowManager source must exist at '{WindowManagerFilePath}'.");
var content = File.ReadAllText(WindowManagerFilePath);
content.Should().Contain(
"Socket.On<int[]>(\"BrowserWindowClosed\"",
"closed-window cleanup must be wired for every close event, not just the first one.");
content.Should().NotContain(
"Socket.Once<int[]>(\"BrowserWindowClosed\"",
"a one-shot subscription causes stale BrowserWindow references after subsequent closes (issue #1008).");
}
private static string FindWindowManagerFile()
{
const string RelativeFromRepoRoot = "src/ElectronNET.API/API/WindowManager.cs";
const string RelativeFromSrc = "ElectronNET.API/API/WindowManager.cs";
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var fromRepoRoot = Path.Combine(dir.FullName, RelativeFromRepoRoot);
if (File.Exists(fromRepoRoot))
{
return Path.GetFullPath(fromRepoRoot);
}
var fromSrc = Path.Combine(dir.FullName, RelativeFromSrc);
if (File.Exists(fromSrc))
{
return Path.GetFullPath(fromSrc);
}
dir = dir.Parent;
}
throw new FileNotFoundException(
"Could not locate WindowManager.cs by walking up from " +
$"'{AppContext.BaseDirectory}'.");
}
}

View File

@@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "!Config", "!Config", "{02EA
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.AspNet", "ElectronNET.AspNet\ElectronNET.AspNet.csproj", "{DD10D21A-D131-1D9C-33F9-406046E0C5B0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.AspNet", "ElectronNET.AspNet\ElectronNET.AspNet.csproj", "{DD10D21A-D131-1D9C-33F9-406046E0C5B0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.Templates", "ElectronNET.Templates\ElectronNET.Templates.csproj", "{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{1BB6F634-2831-4496-83A6-BC6761DCEC8D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{1BB6F634-2831-4496-83A6-BC6761DCEC8D}"
EndProject EndProject
Global Global
@@ -45,6 +47,10 @@ Global
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.Build.0 = Release|Any CPU {DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.Build.0 = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -54,6 +60,7 @@ Global
{829FC339-4785-4229-ABA5-53ADB544DA00} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D} {829FC339-4785-4229-ABA5-53ADB544DA00} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{8860606D-6847-F22A-5AED-DF4E0984DD24} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D} {8860606D-6847-F22A-5AED-DF4E0984DD24} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{DD10D21A-D131-1D9C-33F9-406046E0C5B0} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D} {DD10D21A-D131-1D9C-33F9-406046E0C5B0} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {81A62E71-9E04-4EFE-AD5C-23165375F8EF} SolutionGuid = {81A62E71-9E04-4EFE-AD5C-23165375F8EF}

View File

@@ -8,7 +8,7 @@
<ResourcePreloader /> <ResourcePreloader />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" /> <link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" /> <link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["electronnet-samples-blazorsignalr.styles.css"]" /> <link rel="stylesheet" href="@Assets["electronnet-samples-authmiddleware.styles.css"]" />
<ImportMap /> <ImportMap />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet /> <HeadOutlet />

View File

@@ -1,6 +1,6 @@
<div class="top-row ps-3 navbar navbar-dark"> <div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="">ElectronNET.Samples.BlazorSignalR</a> <a class="navbar-brand" href="">ElectronNET.Samples.AuthMiddleware</a>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@
<TypeScriptUseNodeJS>true</TypeScriptUseNodeJS> <TypeScriptUseNodeJS>true</TypeScriptUseNodeJS>
<TypeScriptTSConfig>ElectronHostHook/tsconfig.json</TypeScriptTSConfig> <TypeScriptTSConfig>ElectronHostHook/tsconfig.json</TypeScriptTSConfig>
<TypeScriptCompileOnSaveEnabled>true</TypeScriptCompileOnSaveEnabled> <TypeScriptCompileOnSaveEnabled>true</TypeScriptCompileOnSaveEnabled>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>false</TypeScriptCompileBlocked>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -27,8 +27,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.5.0" Condition="'$(ElectronNetDevMode)' != 'true'" /> <PackageReference Include="ElectronNET.Core" Version="0.6.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
<PackageReference Include="ElectronNET.Core.AspNet" Version="0.5.0" Condition="'$(ElectronNetDevMode)' != 'true'" /> <PackageReference Include="ElectronNET.Core.AspNet" Version="0.6.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.9.3" /> <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.9.3" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageOutputPath>..\..\artifacts</PackageOutputPath>
<PackageId>$(PackageNamePrefix).Templates</PackageId>
<Title>$(PackageId)</Title>
<Description>$(DescriptionFirstPart) This package contains the 'dotnet new' project templates.</Description>
<PackageTags>electron aspnetcore blazor dotnet-new templates</PackageTags>
<PackageType>Template</PackageType>
<IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<!-- Template content contains dot-prefixed folders (.template.config) that must be packed -->
<NoDefaultExcludes>true</NoDefaultExcludes>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<IncludeSymbols>false</IncludeSymbols>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<NoWarn>$(NoWarn);NU5128</NoWarn>
<Nullable>disable</Nullable>
<!-- The version pinned in the template project files on disk. It is replaced by the
actual package version while packing, so a template always scaffolds an app that
references the Electron.NET packages it was shipped with. -->
<TemplatePlaceholderVersion>0.6.0</TemplatePlaceholderVersion>
<TemplateProjectFile>templates\blazor\ElectronBlazorApp.csproj</TemplateProjectFile>
<TemplateProjectPackagePath>content\templates\blazor\ElectronBlazorApp.csproj</TemplateProjectPackagePath>
</PropertyGroup>
<ItemGroup>
<Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**;$(TemplateProjectFile)" />
<Compile Remove="**\*" />
</ItemGroup>
<ItemGroup>
<None Include="..\ElectronNET\PackageIcon.png" Pack="true" PackagePath="\" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<Target Name="RewriteTemplateProjectVersion" BeforeTargets="_GetPackageFiles">
<PropertyGroup>
<_TemplateProjectSource>$(MSBuildProjectDirectory)\$(TemplateProjectFile)</_TemplateProjectSource>
<_TemplateProjectTarget>$(IntermediateOutputPath)templates\blazor\ElectronBlazorApp.csproj</_TemplateProjectTarget>
<_TemplateProjectContent>$([System.IO.File]::ReadAllText('$(_TemplateProjectSource)'))</_TemplateProjectContent>
</PropertyGroup>
<Error Condition="!$(_TemplateProjectContent.Contains('Version=&quot;$(TemplatePlaceholderVersion)&quot;'))"
Text="'$(TemplateProjectFile)' contains no PackageReference pinned to $(TemplatePlaceholderVersion), so the package version cannot be injected. Align the template's PackageReference versions with the TemplatePlaceholderVersion property." />
<PropertyGroup>
<_TemplateProjectContent>$(_TemplateProjectContent.Replace('Version="$(TemplatePlaceholderVersion)"', 'Version="$(Version)"'))</_TemplateProjectContent>
</PropertyGroup>
<WriteLinesToFile File="$(_TemplateProjectTarget)"
Lines="$([MSBuild]::Escape($(_TemplateProjectContent)))"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<Content Include="$(_TemplateProjectTarget)" Pack="true" PackagePath="$(TemplateProjectPackagePath)" />
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,21 @@
{
"$schema": "http://json.schemastore.org/dotnetcli.host",
"symbolInfo": {
"Framework": {
"longName": "framework",
"shortName": "f"
},
"ElectronVersion": {
"longName": "electron-version",
"shortName": "e"
},
"Port": {
"longName": "port",
"shortName": "p"
},
"skipRestore": {
"longName": "no-restore",
"shortName": ""
}
}
}

View File

@@ -0,0 +1,75 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Electron.NET",
"classifications": [ "Blazor", "Desktop", "Electron", "Web" ],
"identity": "ElectronNET.Core.Templates.Blazor.CSharp",
"groupIdentity": "ElectronNET.Core.Templates.Blazor",
"name": "Electron.NET Blazor App",
"description": "A Blazor Server app hosted in an Electron shell, wired up with Electron.NET.",
"shortName": "electron-blazor",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "ElectronBlazorApp",
"preferNameDirectory": true,
"defaultName": "ElectronBlazorApp",
"symbols": {
"Framework": {
"type": "parameter",
"description": "The target framework for the project.",
"datatype": "choice",
"choices": [
{
"choice": "net10.0",
"description": "Target net10.0"
},
{
"choice": "net8.0",
"description": "Target net8.0"
}
],
"replaces": "net8.0",
"defaultValue": "net10.0"
},
"ElectronVersion": {
"type": "parameter",
"description": "The version of Electron to build the app against.",
"datatype": "text",
"replaces": "ELECTRON_VERSION",
"defaultValue": "38.2.2"
},
"Port": {
"type": "parameter",
"description": "Port number to use for the ASP.NET server during development.",
"datatype": "integer",
"replaces": "8001",
"defaultValue": "8001"
},
"skipRestore": {
"type": "parameter",
"datatype": "bool",
"description": "If specified, skips the automatic restore of the project on create.",
"defaultValue": "false"
}
},
"primaryOutputs": [
{
"path": "ElectronBlazorApp.csproj"
}
],
"postActions": [
{
"id": "restore",
"condition": "(!skipRestore)",
"description": "Restore NuGet packages required by this project.",
"manualInstructions": [
{
"text": "Run 'dotnet restore'"
}
],
"actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
"continueOnError": true
}
]
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="app.css" />
<ImportMap />
<title>ElectronBlazorApp</title>
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
@inherits LayoutComponentBase
<div class="page">
<NavMenu />
<main>
@Body
</main>
</div>

View File

@@ -0,0 +1,4 @@
<nav class="nav-menu">
<NavLink href="" Match="NavLinkMatch.All">Home</NavLink>
<NavLink href="counter">Counter</NavLink>
</nav>

View File

@@ -0,0 +1,19 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,34 @@
@page "/"
@rendermode InteractiveServer
@using ElectronNET.API
@using ElectronNET.API.Entities
<PageTitle>Home</PageTitle>
<h1>Hello from Electron.NET!</h1>
<p>
This Blazor app is served by ASP.NET Core and displayed in an Electron window.
Everything in the <code>ElectronNET.API</code> namespace is available from your components.
</p>
<button class="btn" @onclick="ShowNotification">Show a native notification</button>
<p role="status">@status</p>
@code {
private string? status;
private void ShowNotification()
{
if (HybridSupport.IsElectronActive)
{
Electron.Notification.Show(new NotificationOptions("ElectronBlazorApp", "Hello from Blazor!"));
status = "Notification sent.";
}
else
{
status = "Not running inside Electron.";
}
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,9 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.JSInterop
@using ElectronBlazorApp
@using ElectronBlazorApp.Components
@using ElectronBlazorApp.Components.Layout

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Electron hosts the app out of process -->
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
<PropertyGroup Label="ElectronNetCommon">
<ElectronPackageId>electron-blazor-app</ElectronPackageId>
<Title>ElectronBlazorApp</Title>
<Description>A Blazor app running in Electron.</Description>
<Version>1.0.0</Version>
<Company>My Company</Company>
<Copyright>Copyright © $(Company)</Copyright>
<ElectronVersion>ELECTRON_VERSION</ElectronVersion>
<ElectronSingleInstance>true</ElectronSingleInstance>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.6.0" />
<PackageReference Include="ElectronNET.Core.AspNet" Version="0.6.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
using ElectronNET.API;
using ElectronNET.API.Entities;
using ElectronNET.AspNet.Middleware;
using ElectronNET.AspNet.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSingleton<IElectronAuthenticationService, ElectronAuthenticationService>();
builder.Services.AddElectron();
// Starts the Electron shell and invokes ElectronAppReady once it is up.
builder.UseElectron(args, ElectronAppReady);
var app = builder.Build();
app.UseMiddleware<ElectronAuthenticationMiddleware>();
app.UseRouting();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
app.UseStaticFiles();
app.UseAntiforgery();
// Fully qualified because ElectronNET.API also defines an 'App' type.
app.MapRazorComponents<ElectronBlazorApp.Components.App>()
.AddInteractiveServerRenderMode();
app.Run();
static async Task ElectronAppReady()
{
var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions
{
IsRunningBlazor = true,
Width = 1152,
Height = 940,
Show = false,
});
window.OnReadyToShow += () => window.Show();
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>false</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>false</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>false</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,22 @@
{
"compression": "maximum",
"linux": {
"target": [ "AppImage" ],
"executableArgs": [ "--no-sandbox" ]
},
"win": {
"target": [
{
"target": "nsis",
"arch": "x64"
}
]
},
"mac": {
"target": [ "dmg" ]
},
"nsis": {
"oneClick": true,
"perMachine": false
}
}

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"ElectronBlazorApp": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:8001/"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,49 @@
:root {
color-scheme: light dark;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
}
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.nav-menu {
display: flex;
gap: 1rem;
padding: 0.75rem 1.5rem;
background-color: #1b1b1f;
}
.nav-menu a {
color: #d7d7d7;
text-decoration: none;
}
.nav-menu a.active {
color: #fff;
font-weight: 600;
}
main {
padding: 1.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
background-color: #1b6ec2;
color: #fff;
cursor: pointer;
}
.btn:hover {
background-color: #1861ac;
}

View File

@@ -29,9 +29,6 @@ namespace ElectronNET.WebApp.Controllers
Electron.IpcMain.On("auto-update", async (args) => Electron.IpcMain.On("auto-update", async (args) =>
{ {
// Electron.NET CLI Command for deploy:
// electronize build /target win /electron-params --publish=always
var currentVersion = await Electron.App.GetVersionAsync(); var currentVersion = await Electron.App.GetVersionAsync();
var updateCheckResult = await Electron.AutoUpdater.CheckForUpdatesAndNotifyAsync(); var updateCheckResult = await Electron.AutoUpdater.CheckForUpdatesAndNotifyAsync();
var availableVersion = updateCheckResult.UpdateInfo.Version; var availableVersion = updateCheckResult.UpdateInfo.Version;

View File

@@ -204,29 +204,43 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/node": { "node_modules/@humanfs/node": {
"version": "0.16.7", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@humanfs/core": "^0.19.1", "@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0" "@humanwhocodes/retry": "^0.4.0"
}, },
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -526,9 +540,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.14", "version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -1123,9 +1137,9 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.3.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -1337,10 +1351,20 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"dev": true, "dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -1787,9 +1811,9 @@
} }
}, },
"node_modules/readdir-glob/node_modules/brace-expansion": { "node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.1.1", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"

View File

@@ -34,7 +34,6 @@
<ElectronSplashScreen>wwwroot\assets\img\about@2x.png</ElectronSplashScreen> <ElectronSplashScreen>wwwroot\assets\img\about@2x.png</ElectronSplashScreen>
<License>MIT</License> <License>MIT</License>
<ElectronSingleInstance>false</ElectronSingleInstance> <ElectronSingleInstance>false</ElectronSingleInstance>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ElectronBuilderVersion>26.0</ElectronBuilderVersion> <ElectronBuilderVersion>26.0</ElectronBuilderVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -76,8 +75,8 @@
<ProjectReference Include="..\ElectronNET.AspNet\ElectronNET.AspNet.csproj" Condition="$(ElectronNetDevMode)" /> <ProjectReference Include="..\ElectronNET.AspNet\ElectronNET.AspNet.csproj" Condition="$(ElectronNetDevMode)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.5.0" Condition="'$(ElectronNetDevMode)' != 'true'" /> <PackageReference Include="ElectronNET.Core" Version="0.6.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
<PackageReference Include="ElectronNET.Core.AspNet" Version="0.5.0" Condition="'$(ElectronNetDevMode)' != 'true'" /> <PackageReference Include="ElectronNET.Core.AspNet" Version="0.6.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.9.3" /> <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.9.3" />
</ItemGroup> </ItemGroup>

View File

@@ -8,7 +8,7 @@
</h1> </h1>
<h3>The <code>HostHook</code> API allows you to execute your own JavaScript/TypeScript code on the host process.</h3> <h3>The <code>HostHook</code> API allows you to execute your own JavaScript/TypeScript code on the host process.</h3>
<p>Create first an ElectronHostHook directory via the Electron.NET CLI, with the following command: <code>electronize add hosthook</code>.</p> <p>Create first an ElectronHostHook directory with a package.json and a JavaScript file.</p>
<p>In this directory you can install any NPM packages and embed your own JavaScript/TypeScript code. It is also possible to respond to events from the host process.</p> <p>In this directory you can install any NPM packages and embed your own JavaScript/TypeScript code. It is also possible to respond to events from the host process.</p>
<p>You find the sample source code in <code>Controllers\HostHookController.cs</code> and in the <code>ElectronHostHook</code> folder.</p> <p>You find the sample source code in <code>Controllers\HostHookController.cs</code> and in the <code>ElectronHostHook</code> folder.</p>
</div> </div>

View File

@@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "!Config", "!Config", "{02EA
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.AspNet", "ElectronNET.AspNet\ElectronNET.AspNet.csproj", "{DD10D21A-D131-1D9C-33F9-406046E0C5B0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.AspNet", "ElectronNET.AspNet\ElectronNET.AspNet.csproj", "{DD10D21A-D131-1D9C-33F9-406046E0C5B0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.Templates", "ElectronNET.Templates\ElectronNET.Templates.csproj", "{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.ConsoleApp", "ElectronNET.ConsoleApp\ElectronNET.ConsoleApp.csproj", "{EE38A326-5DE8-AF09-9EB9-DF0878938783}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.ConsoleApp", "ElectronNET.ConsoleApp\ElectronNET.ConsoleApp.csproj", "{EE38A326-5DE8-AF09-9EB9-DF0878938783}"
ProjectSection(ProjectDependencies) = postProject ProjectSection(ProjectDependencies) = postProject
{1C5FD66E-A1C6-C436-DF7C-3ECE4FEDDFE6} = {1C5FD66E-A1C6-C436-DF7C-3ECE4FEDDFE6} {1C5FD66E-A1C6-C436-DF7C-3ECE4FEDDFE6} = {1C5FD66E-A1C6-C436-DF7C-3ECE4FEDDFE6}
@@ -99,6 +101,10 @@ Global
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.Build.0 = Release|Any CPU {DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.Build.0 = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.Build.0 = Release|Any CPU
{EE38A326-5DE8-AF09-9EB9-DF0878938783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE38A326-5DE8-AF09-9EB9-DF0878938783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE38A326-5DE8-AF09-9EB9-DF0878938783}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE38A326-5DE8-AF09-9EB9-DF0878938783}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE38A326-5DE8-AF09-9EB9-DF0878938783}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE38A326-5DE8-AF09-9EB9-DF0878938783}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -131,6 +137,7 @@ Global
{829FC339-4785-4229-ABA5-53ADB544DA00} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D} {829FC339-4785-4229-ABA5-53ADB544DA00} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{8860606D-6847-F22A-5AED-DF4E0984DD24} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D} {8860606D-6847-F22A-5AED-DF4E0984DD24} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{DD10D21A-D131-1D9C-33F9-406046E0C5B0} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D} {DD10D21A-D131-1D9C-33F9-406046E0C5B0} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{EE38A326-5DE8-AF09-9EB9-DF0878938783} = {EDCBFC49-2AEE-4BAF-9368-4409298C52FC} {EE38A326-5DE8-AF09-9EB9-DF0878938783} = {EDCBFC49-2AEE-4BAF-9368-4409298C52FC}
{015CB06B-6CAE-209F-E050-21C3ACA5FE9F} = {985D39A7-5216-4945-8167-2FD0CB387BD8} {015CB06B-6CAE-209F-E050-21C3ACA5FE9F} = {985D39A7-5216-4945-8167-2FD0CB387BD8}
{06CAADC7-DE5B-47B4-AB2A-E9501459A2D1} = {D36CDFFD-3438-42E4-A7FF-88BA19AC4964} {06CAADC7-DE5B-47B4-AB2A-E9501459A2D1} = {D36CDFFD-3438-42E4-A7FF-88BA19AC4964}

View File

@@ -3,7 +3,7 @@
<Import Project="..\common.props" /> <Import Project="..\common.props" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks> <TargetFrameworks>$(ElectronNetTargetFrameworks)</TargetFrameworks>
<PackageOutputPath>..\..\artifacts</PackageOutputPath> <PackageOutputPath>..\..\artifacts</PackageOutputPath>
<PackageId>$(PackageNamePrefix)</PackageId> <PackageId>$(PackageNamePrefix)</PackageId>
<Title>$(PackageId)</Title> <Title>$(PackageId)</Title>

View File

@@ -4,14 +4,21 @@
<PropertyGroup Label="ElectronNetCommon"> <PropertyGroup Label="ElectronNetCommon">
<ElectronVersion>30.4.0</ElectronVersion> <ElectronVersion>30.4.0</ElectronVersion>
<ElectronBuilderVersion>26.0</ElectronBuilderVersion> <ElectronBuilderVersion>26.0</ElectronBuilderVersion>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier> <!-- ElectronRuntimeIdentifier may be set explicitly here; it is otherwise resolved from
RuntimeIdentifier (or the host) in ElectronResolveRuntimeIdentifier, which runs late
enough to also see RIDs coming from the project file or a publish profile. -->
<ElectronSingleInstance>true</ElectronSingleInstance> <ElectronSingleInstance>true</ElectronSingleInstance>
<ElectronSplashScreen></ElectronSplashScreen> <ElectronSplashScreen></ElectronSplashScreen>
<ElectronIcon></ElectronIcon> <ElectronIcon></ElectronIcon>
<PackageIcon></PackageIcon> <PackageIcon></PackageIcon>
<ElectronPackageId>$(MSBuildProjectName.Replace(".", "-").ToLower())</ElectronPackageId> <ElectronPackageId Condition="'$(ElectronPackageId)' == ''">$(MSBuildProjectName.Replace(".", "-").ToLower())</ElectronPackageId>
<ElectronBuilderJson>electron-builder.json</ElectronBuilderJson> <ElectronBuilderJson>electron-builder.json</ElectronBuilderJson>
<Title>$(MSBuildProjectName)</Title> <Title Condition="'$(Title)' == ''">$(MSBuildProjectName)</Title>
<!-- Location of the Electron binary relative to the .NET application directory of a packaged app.
The default matches the standard layout, where the .NET app is placed in 'resources/bin'. -->
<ElectronRootDir Condition="'$(ElectronRootDir)' == ''">../..</ElectronRootDir>
<!-- Set to true to suppress the npm/npx invocations of the Electron build and publish targets. -->
<ElectronSkipExecCommands Condition="'$(ElectronSkipExecCommands)' == ''">false</ElectronSkipExecCommands>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(ElectronExecutable)' == ''"> <PropertyGroup Condition="'$(ElectronExecutableName)' == ''">
<WinPrefix>win</WinPrefix> <WinPrefix>win</WinPrefix>
<ElectronExecutable Condition="'$(RuntimeIdentifier.StartsWith($(WinPrefix)))' == 'true'">$(Title)</ElectronExecutable> <ElectronExecutableName Condition="'$(RuntimeIdentifier.StartsWith($(WinPrefix)))' == 'true'">$(Title)</ElectronExecutableName>
<ElectronExecutable Condition="'$(RuntimeIdentifier.StartsWith($(WinPrefix)))' != 'true'">$(ElectronPackageId)</ElectronExecutable> <ElectronExecutableName Condition="'$(RuntimeIdentifier.StartsWith($(WinPrefix)))' != 'true'">$(ElectronPackageId)</ElectronExecutableName>
</PropertyGroup>
<PropertyGroup>
<!-- The binary Electron.NET launches. It only differs from the packaged executable in advanced
scenarios, e.g. when a build hook adds a launcher stub to the package. -->
<ElectronExecutable Condition="'$(ElectronExecutable)' == ''">$(ElectronExecutableName)</ElectronExecutable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -19,6 +25,7 @@
<ItemGroup> <ItemGroup>
<AssemblyMetadata Include="ElectronExecutable" Value="$(ElectronExecutable)" /> <AssemblyMetadata Include="ElectronExecutable" Value="$(ElectronExecutable)" />
<AssemblyMetadata Include="ElectronVersion" Value="$(ElectronVersion)" /> <AssemblyMetadata Include="ElectronVersion" Value="$(ElectronVersion)" />
<AssemblyMetadata Include="ElectronRootDir" Value="$(ElectronRootDir)" />
<AssemblyMetadata Include="RuntimeIdentifier" Value="$(RuntimeIdentifier)" /> <AssemblyMetadata Include="RuntimeIdentifier" Value="$(RuntimeIdentifier)" />
<AssemblyMetadata Include="ElectronSingleInstance" Value="$(ElectronSingleInstance)" /> <AssemblyMetadata Include="ElectronSingleInstance" Value="$(ElectronSingleInstance)" />
<AssemblyMetadata Include="Title" Value="$(Title)" /> <AssemblyMetadata Include="Title" Value="$(Title)" />

View File

@@ -52,7 +52,7 @@
<!-- Targets --> <!-- Targets -->
<Target Name="ElectronBeforeClean" AfterTargets="BeforeClean" DependsOnTargets="ElectronResolvePaths"> <Target Name="ElectronBeforeClean" AfterTargets="BeforeClean" DependsOnTargets="ElectronSetPaths">
<ItemGroup> <ItemGroup>
<Clean Include="$(ElectronOutDir)**" /> <Clean Include="$(ElectronOutDir)**" />
</ItemGroup> </ItemGroup>
@@ -60,7 +60,7 @@
<!--<Message Text="ElectronBeforeClean - Clean Files: @(Clean)" Importance="High" />--> <!--<Message Text="ElectronBeforeClean - Clean Files: @(Clean)" Importance="High" />-->
</Target> </Target>
<Target Name="ElectronClean" AfterTargets="CoreClean" DependsOnTargets="ElectronResolvePaths"> <Target Name="ElectronClean" AfterTargets="CoreClean" DependsOnTargets="ElectronSetPaths">
<RemoveDir Directories="$(ElectronOutDir)" /> <RemoveDir Directories="$(ElectronOutDir)" />
@@ -95,8 +95,24 @@
</Target> </Target>
<!-- Resolved at execution time, so that a RuntimeIdentifier coming from the project file,
a publish profile or the CLI wins over the host RID. -->
<Target Name="ElectronResolveRuntimeIdentifier">
<PropertyGroup>
<ElectronRuntimeIdentifier Condition="'$(ElectronRuntimeIdentifier)' == '' AND '$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</ElectronRuntimeIdentifier>
<!-- When using the dotnet CLI (Core MSBuild), infer the current RID as a default -->
<ElectronRuntimeIdentifier Condition="'$(ElectronRuntimeIdentifier)' == '' AND '$(MSBuildRuntimeType)' == 'Core'">$([System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier)</ElectronRuntimeIdentifier>
<!-- Otherwise, Full MSBuild runs only on Windows, so infer a Windows RID from OSArchitecture -->
<ElectronRuntimeIdentifier Condition="'$(ElectronRuntimeIdentifier)' == '' AND '$(MSBuildRuntimeType)' == 'Full'">win-$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower())</ElectronRuntimeIdentifier>
</PropertyGroup>
<Message Text="ElectronRuntimeIdentifier: $(ElectronRuntimeIdentifier)" Importance="High" />
</Target>
<!-- This is also run at design time, each time when a property of the project is changed --> <!-- This is also run at design time, each time when a property of the project is changed -->
<Target Name="ElectronCreatePackageJson" DependsOnTargets="ResolveProjectReferences;ElectronSetPaths"> <Target Name="ElectronCreatePackageJson" DependsOnTargets="ResolveProjectReferences;ElectronSetPaths;ElectronResolveRuntimeIdentifier">
<Message Text="Creating package.json from template..." Importance="High" /> <Message Text="Creating package.json from template..." Importance="High" />
<Message Text="OutDir: $(OutDir)" Importance="High" /> <Message Text="OutDir: $(OutDir)" Importance="High" />
@@ -121,7 +137,7 @@
<!-- Electron Builder uses title for the installation dir on Linux, but it should not have spaces --> <!-- Electron Builder uses title for the installation dir on Linux, but it should not have spaces -->
<LinuxPrefix>linux</LinuxPrefix> <LinuxPrefix>linux</LinuxPrefix>
<ElectronTitle>$(Title)</ElectronTitle> <ElectronTitle>$(Title)</ElectronTitle>
<ElectronTitle Condition="'$(RuntimeIdentifier.StartsWith($(LinuxPrefix)))' == 'true'">$(Title.Replace(' ', '-'))</ElectronTitle> <ElectronTitle Condition="'$(ElectronRuntimeIdentifier.StartsWith($(LinuxPrefix)))' == 'true'">$(Title.Replace(' ', '-'))</ElectronTitle>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -154,7 +170,9 @@
<PropertyGroup Condition="'$(UsingMicrosoftNETSdkWeb)' != 'true'"> <PropertyGroup Condition="'$(UsingMicrosoftNETSdkWeb)' != 'true'">
<_NonIntermediatePublishDir>$(PublishDir)</_NonIntermediatePublishDir> <_NonIntermediatePublishDir>$(PublishDir)</_NonIntermediatePublishDir>
<PublishUrl>$(PublishDir)</PublishUrl> <PublishUrl>$(PublishDir)</PublishUrl>
<PublishDir>$(IntermediateOutputPath)PubTmp\</PublishDir> <!-- ElectronIntermediatePublishDir allows overriding the intermediate publish directory -->
<PublishDir Condition="'$(ElectronIntermediatePublishDir)' != ''">$(ElectronIntermediatePublishDir)</PublishDir>
<PublishDir Condition="'$(ElectronIntermediatePublishDir)' == ''">$(IntermediateOutputPath)PubTmp\</PublishDir>
</PropertyGroup> </PropertyGroup>
<RemoveDir Directories="$(PublishDir);$(PublishUrl)" Condition="'$(UsingMicrosoftNETSdkWeb)' != 'true'" /> <RemoveDir Directories="$(PublishDir);$(PublishUrl)" Condition="'$(UsingMicrosoftNETSdkWeb)' != 'true'" />
@@ -177,9 +195,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- tsconfig.json is a build-time config, not a runtime file. Excluding it also stops
Microsoft.TypeScript.MSBuild from discovering and compiling the host's tsconfig
(types:["node"]) in the consumer project when hook TypeScript compilation is enabled. -->
<ElectronSourceFiles <ElectronSourceFiles
Include="$(ElectronSourceFilesPath)\**\*.js;$(ElectronSourceFilesPath)\**\*.json;$(ElectronSourceFilesPath)\**\*.html" Include="$(ElectronSourceFilesPath)\**\*.js;$(ElectronSourceFilesPath)\**\*.json;$(ElectronSourceFilesPath)\**\*.html"
Exclude="$(ElectronSourceFilesPath)\**\build-helper.js" /> Exclude="$(ElectronSourceFilesPath)\**\build-helper.js;$(ElectronSourceFilesPath)\**\tsconfig.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -213,6 +234,35 @@
</ElectronCustomHookPackageJson> </ElectronCustomHookPackageJson>
</ItemGroup> </ItemGroup>
<!-- Committed-JS mode (TypeScriptCompileBlocked=true): the hook TypeScript is not compiled
during the build, so the committed ElectronHostHook JavaScript (and source maps) are
packaged as-is. Reclassify them from None into .electron\ElectronHostHook content
(mirroring ElectronSourceFiles) so they reach the build output and, at publish, are swept
into app\ -> app.asar. When compilation is enabled (TypeScriptCompileBlocked!=true) this
is skipped: Microsoft.TypeScript.MSBuild compiles the hook and ElectronAdjustHostHookOutput
routes the output, so reclassifying would duplicate .electron\ElectronHostHook items.
Without one of these, require('electron-host-hook') fails ('Cannot find module .../index.js'). -->
<ItemGroup Condition="'$(ElectronHasCustomHookCode)' == 'true' AND '$(TypeScriptCompileBlocked)' == 'true'">
<None Remove="ElectronHostHook\**\*.js;ElectronHostHook\**\*.js.map" />
<ElectronCustomHookJsFiles Include="ElectronHostHook\**\*.js;ElectronHostHook\**\*.js.map"
Exclude="ElectronHostHook\node_modules\**" />
<ElectronCustomHookJsFiles Update="@(ElectronCustomHookJsFiles)">
<Link>$(ElectronDirName)\ElectronHostHook\%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ElectronCustomHookJsFiles>
</ItemGroup>
<AssignTargetPath Files="@(ElectronCustomHookJsFiles)" RootFolder="$(MSBuildProjectDirectory)"
Condition="'$(ElectronHasCustomHookCode)' == 'true' AND '$(TypeScriptCompileBlocked)' == 'true'">
<Output TaskParameter="AssignedFiles" ItemName="ElectronCustomHookJsFilesWithTargetPath" />
</AssignTargetPath>
<ItemGroup Condition="'$(ElectronHasCustomHookCode)' == 'true' AND '$(TypeScriptCompileBlocked)' == 'true'">
<FilesForPackagingFromProject Include="@(ElectronCustomHookJsFilesWithTargetPath->'%(Identity)')" />
<ContentWithTargetPath Include="@(ElectronCustomHookJsFilesWithTargetPath->'%(Identity)')" />
<Content Include="@(ElectronCustomHookJsFiles->'%(Identity)')" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ElectronIntermediatePackageJson Include="$(ElectronIntermediatePackageJson)" /> <ElectronIntermediatePackageJson Include="$(ElectronIntermediatePackageJson)" />
</ItemGroup> </ItemGroup>
@@ -231,13 +281,45 @@
</Target> </Target>
<!-- When hook TypeScript compilation is enabled (TypeScriptCompileBlocked!=true), restore the
hook's npm dependencies into ElectronHostHook\node_modules before tsc runs so its imports
(e.g. socket.io) resolve. Incremental: only re-runs when the hook package.json changes or
node_modules is missing. Skipped in committed-JS mode and at design time. -->
<Target Name="ElectronRestoreHostHookDependencies"
BeforeTargets="CompileTypeScriptWithTSConfig;CompileTypeScript"
Condition="'@(ElectronCustomHookTsFiles->Count())' &gt; 0 AND Exists('$(MSBuildProjectDirectory)\ElectronHostHook\package.json') AND '$(TypeScriptCompileBlocked)' != 'true' AND '$(DesignTimeBuild)' != 'true'"
Inputs="$(MSBuildProjectDirectory)\ElectronHostHook\package.json"
Outputs="$(MSBuildProjectDirectory)\ElectronHostHook\node_modules\.package-lock.json">
<PropertyGroup>
<IsLinuxWsl>false</IsLinuxWsl>
<IsLinuxWsl Condition="'$(ElectronPlatform)' == 'linux' AND $([MSBuild]::IsOSPlatform('Windows'))">true</IsLinuxWsl>
<_NpmCmd>npm install</_NpmCmd>
<_NpmCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmCmd)'</_NpmCmd>
</PropertyGroup>
<Message Importance="High" Text="Restoring ElectronHostHook npm dependencies for TypeScript compilation..." />
<Exec Command="$(_NpmCmd)"
WorkingDirectory="$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', 'ElectronHostHook'))"
Timeout="600000" StandardOutputImportance="Low" StandardErrorImportance="High" ContinueOnError="false" />
</Target>
<!-- Run before GetTypeScriptOutputForPublishing to adjust the target directory --> <!-- Run before GetTypeScriptOutputForPublishing to adjust the target directory -->
<Target Name="ElectronAdjustHostHookOutput" BeforeTargets="GetTypeScriptOutputForPublishing" DependsOnTargets="CompileTypeScriptWithTSConfig"> <Target Name="ElectronAdjustHostHookOutput" BeforeTargets="GetTypeScriptOutputForPublishing" DependsOnTargets="CompileTypeScriptWithTSConfig">
<ItemGroup> <ItemGroup>
<!-- Route the compiled hook JS to .electron\ElectronHostHook. Do NOT reuse
%(DestinationRelativePath): with no outDir in the hook tsconfig, VsTsc leaves it as
an absolute path, which would produce .electron\<absolute-path>. Derive the target
from the file name instead (hook files are flat). Link is set because
GetTypeScriptOutputForPublishing re-runs AssignTargetPath, which honors Link for the
publish TargetPath. -->
<GeneratedJavascript Update="@(GeneratedJavascript)"> <GeneratedJavascript Update="@(GeneratedJavascript)">
<DestinationRelativePath>$(ElectronDirName)\%(DestinationRelativePath)</DestinationRelativePath> <DestinationRelativePath>$(ElectronDirName)\ElectronHostHook\%(Filename)%(Extension)</DestinationRelativePath>
<TargetPath>$(ElectronDirName)\%(DestinationRelativePath)</TargetPath> <TargetPath>$(ElectronDirName)\ElectronHostHook\%(Filename)%(Extension)</TargetPath>
<Link>$(ElectronDirName)\ElectronHostHook\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</GeneratedJavascript> </GeneratedJavascript>
</ItemGroup> </ItemGroup>
@@ -249,7 +331,10 @@
<ItemGroup> <ItemGroup>
<!--<_ElectronFiles Include="$(ElectronIntermediatePackageJson)" />--> <!--<_ElectronFiles Include="$(ElectronIntermediatePackageJson)" />-->
<_ElectronFiles Include="$(ElectronSplashScreen)" Condition="'$(ElectronSplashScreen)'!=''" /> <_ElectronFiles Include="$(ElectronSplashScreen)" Condition="'$(ElectronSplashScreen)'!=''" />
<_ElectronFiles Include="$(ElectronIcon)" Condition="'$(ElectronIcon)'!=''" /> <_ElectronFiles Include="$(ElectronIcon)"
Condition="'$(ElectronIcon)'!='' AND ( !Exists('$(ElectronIcon)') OR !$([System.IO.Directory]::Exists('$(ElectronIcon)') ) )" />
<_ElectronIconDirectoryFiles Include="$(ElectronIcon)\**\*"
Condition="'$(ElectronIcon)'!='' AND Exists('$(ElectronIcon)') AND $([System.IO.Directory]::Exists('$(ElectronIcon)') )" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -265,11 +350,16 @@
</ItemGroup> </ItemGroup>
<Message Importance="High" Text="_ElectronFilesToCopy: @(_ElectronFilesToCopy)" /> <Message Importance="High" Text="_ElectronFilesToCopy: @(_ElectronFilesToCopy)" />
<Message Importance="High" Text="_ElectronIconDirectoryFiles: @(_ElectronIconDirectoryFiles)" />
<ItemGroup> <ItemGroup>
<_ElectronFilesToCopyWithTargetPath Include="@(_ElectronFilesToCopy)"> <_ElectronFilesToCopyWithTargetPath Include="@(_ElectronFilesToCopy)">
<TargetPath>$(ElectronDirName)\%(FileName)%(Extension)</TargetPath> <TargetPath>$(ElectronDirName)\%(FileName)%(Extension)</TargetPath>
</_ElectronFilesToCopyWithTargetPath> </_ElectronFilesToCopyWithTargetPath>
<_ElectronFilesToCopyWithTargetPath Include="@(_ElectronIconDirectoryFiles)">
<TargetPath>$(ElectronDirName)\$(ElectronIconFileName)\%(RecursiveDir)%(FileName)%(Extension)</TargetPath>
</_ElectronFilesToCopyWithTargetPath>
</ItemGroup> </ItemGroup>
<Message Text="_ElectronFilesToCopyWithTargetPath: @(_ElectronFilesToCopyWithTargetPath)" /> <Message Text="_ElectronFilesToCopyWithTargetPath: @(_ElectronFilesToCopyWithTargetPath)" />
@@ -298,21 +388,21 @@
<RemoveDir Directories="$(ElectronHookTargetModuleDir)" Condition="Exists($(ElectronHookTargetModuleDir))" /> <RemoveDir Directories="$(ElectronHookTargetModuleDir)" Condition="Exists($(ElectronHookTargetModuleDir))" />
</Target> </Target>
<Target Name="ElectronCheckVersionMismatch"> <Target Name="ElectronCheckVersionMismatch" DependsOnTargets="ElectronResolveRuntimeIdentifier">
<PropertyGroup> <PropertyGroup>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'win-x64'">x64</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'win-x64'">x64</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'win-x86'">ia32</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'win-x86'">ia32</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'win-arm64'">arm64</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'win-arm64'">arm64</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'linux-x64'">x64</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'linux-x64'">x64</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'linux-arm'">armv7l</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'linux-arm'">armv7l</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">arm64</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'linux-arm64'">arm64</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'osx-x64'">x64</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'osx-x64'">x64</ElectronArch>
<ElectronArch Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">arm64</ElectronArch> <ElectronArch Condition="'$(ElectronRuntimeIdentifier)' == 'osx-arm64'">arm64</ElectronArch>
<ElectronPlatform Condition="'$(RuntimeIdentifier)' == 'win-x64' OR '$(RuntimeIdentifier)' == 'win-x86' OR '$(RuntimeIdentifier)' == 'win-arm64'">win</ElectronPlatform> <ElectronPlatform Condition="'$(ElectronRuntimeIdentifier)' == 'win-x64' OR '$(ElectronRuntimeIdentifier)' == 'win-x86' OR '$(ElectronRuntimeIdentifier)' == 'win-arm64'">win</ElectronPlatform>
<ElectronPlatform Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm' OR '$(RuntimeIdentifier)' == 'linux-arm64'">linux</ElectronPlatform> <ElectronPlatform Condition="'$(ElectronRuntimeIdentifier)' == 'linux-x64' OR '$(ElectronRuntimeIdentifier)' == 'linux-arm' OR '$(ElectronRuntimeIdentifier)' == 'linux-arm64'">linux</ElectronPlatform>
<ElectronPlatform Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'">mac</ElectronPlatform> <ElectronPlatform Condition="'$(ElectronRuntimeIdentifier)' == 'osx-x64' OR '$(ElectronRuntimeIdentifier)' == 'osx-arm64'">mac</ElectronPlatform>
<!-- npm uses different OS names than Electron --> <!-- npm uses different OS names than Electron -->
<NpmOs Condition="'$(ElectronPlatform)' == 'win'">win32</NpmOs> <NpmOs Condition="'$(ElectronPlatform)' == 'win'">win32</NpmOs>
@@ -321,7 +411,7 @@
<!-- npm CPU is same as ElectronArch except for linux-arm --> <!-- npm CPU is same as ElectronArch except for linux-arm -->
<NpmCpu>$(ElectronArch)</NpmCpu> <NpmCpu>$(ElectronArch)</NpmCpu>
<NpmCpu Condition="'$(RuntimeIdentifier)' == 'linux-arm'">arm</NpmCpu> <NpmCpu Condition="'$(ElectronRuntimeIdentifier)' == 'linux-arm'">arm</NpmCpu>
<_CurrentOSPlatform Condition="$([MSBuild]::IsOSPlatform('Windows'))">win</_CurrentOSPlatform> <_CurrentOSPlatform Condition="$([MSBuild]::IsOSPlatform('Windows'))">win</_CurrentOSPlatform>
<_CurrentOSPlatform Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux</_CurrentOSPlatform> <_CurrentOSPlatform Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux</_CurrentOSPlatform>
@@ -371,13 +461,13 @@
<!--<Message Importance="High" Text="ElectronConfigureApp: CopiedFiles - @(_CopiedFiles)" />--> <!--<Message Importance="High" Text="ElectronConfigureApp: CopiedFiles - @(_CopiedFiles)" />-->
<Message Importance="High" Text="Running command: $(_NpmCmd) in folder: $(ElectronOutputPath)" Condition="@(_CopiedFiles->Count()) > 0" /> <Message Importance="High" Text="Running command: $(_NpmCmd) in folder: $(ElectronOutputPath)" Condition="'$(ElectronSkipExecCommands)' != 'true' AND @(_CopiedFiles->Count()) > 0" />
<Exec Command="$(_NpmCmd)" WorkingDirectory="$(ElectronOutputPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="@(_CopiedFiles->Count()) > 0"> <Exec Command="$(_NpmCmd)" WorkingDirectory="$(ElectronOutputPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="'$(ElectronSkipExecCommands)' != 'true' AND @(_CopiedFiles->Count()) > 0">
<Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/> <Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/>
</Exec> </Exec>
<Message Importance="High" Text="Electron setup failed!" Condition="'$(ExecExitCode)' != '0'" /> <Message Importance="High" Text="Electron setup failed!" Condition="'$(ExecExitCode)' != '' AND '$(ExecExitCode)' != '0'" />
<PropertyGroup> <PropertyGroup>
<_NpmCmd>node node_modules/electron/install.js</_NpmCmd> <_NpmCmd>node node_modules/electron/install.js</_NpmCmd>
@@ -386,13 +476,13 @@
<_NpmCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmCmd)'</_NpmCmd> <_NpmCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmCmd)'</_NpmCmd>
</PropertyGroup> </PropertyGroup>
<Message Importance="High" Text="Running command: $(_NpmCmd) in folder: $(ElectronOutputPath)" Condition="$(ElectronMajor) &gt;= 42 AND @(_CopiedFiles->Count()) > 0" /> <Message Importance="High" Text="Running command: $(_NpmCmd) in folder: $(ElectronOutputPath)" Condition="'$(ElectronSkipExecCommands)' != 'true' AND $(ElectronMajor) &gt;= 42 AND @(_CopiedFiles->Count()) > 0" />
<Exec Command="$(_NpmCmd)" WorkingDirectory="$(ElectronOutputPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="$(ElectronMajor) &gt;= 42 AND @(_CopiedFiles->Count()) > 0"> <Exec Command="$(_NpmCmd)" WorkingDirectory="$(ElectronOutputPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="'$(ElectronSkipExecCommands)' != 'true' AND $(ElectronMajor) &gt;= 42 AND @(_CopiedFiles->Count()) > 0">
<Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/> <Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/>
</Exec> </Exec>
<Message Importance="High" Text="Electron installation failed!" Condition="'$(ExecExitCode)' != '0'" /> <Message Importance="High" Text="Electron installation failed!" Condition="'$(ExecExitCode)' != '' AND '$(ExecExitCode)' != '0'" />
<!-- Fix up incorrect symlinks created by npm on Windows when targeting macOS --> <!-- Fix up incorrect symlinks created by npm on Windows when targeting macOS -->
<PropertyGroup> <PropertyGroup>
@@ -449,12 +539,16 @@
<PublishDir>$(_OriginalPublishDir)</PublishDir> <PublishDir>$(_OriginalPublishDir)</PublishDir>
<PublishDir Condition="'$(_NonIntermediatePublishDir)' != ''">$(_NonIntermediatePublishDir)</PublishDir> <PublishDir Condition="'$(_NonIntermediatePublishDir)' != ''">$(_NonIntermediatePublishDir)</PublishDir>
<ElectronPublishDir>$(_OriginalPublishDir)</ElectronPublishDir> <ElectronPublishDir>$(_OriginalPublishDir)</ElectronPublishDir>
<!-- The Electron host files (main.js, package.json, runtime node_modules, ...) are packed
into app.asar. They live in a dedicated 'app' subfolder so the .NET app in 'bin' is NOT
swept into the asar by electron-builder's default 'files' glob. See directories.app below. -->
<ElectronAppDir>$(ElectronPublishDir)app\</ElectronAppDir>
</PropertyGroup> </PropertyGroup>
<Error Condition="'$(_IsPlatformMismatch)' == 'true'" <Error Condition="'$(_IsPlatformMismatch)' == 'true'"
Code="ELECTRON100" Code="ELECTRON100"
Text="The target RuntimeIdentifier '$(RuntimeIdentifier)' (platform: $(ElectronPlatform)) does not match the current operating system ($(_CurrentOSPlatform)). Text="The target RuntimeIdentifier '$(ElectronRuntimeIdentifier)' (platform: $(ElectronPlatform)) does not match the current operating system ($(_CurrentOSPlatform)).
Electron applications must be built on the target operating system: Electron applications must be built on the target operating system:
- Windows targets (win-x64, win-x86, win-arm64) must be built on Windows - Windows targets (win-x64, win-x86, win-arm64) must be built on Windows
@@ -489,7 +583,7 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<ItemGroup> <ItemGroup>
<ElectronPublishFilesToMove Update="@(ElectronPublishFilesToMove)" > <ElectronPublishFilesToMove Update="@(ElectronPublishFilesToMove)" >
<MoveTargetPath>$(ElectronPublishDir)%(RecursiveDir)%(FileName)%(Extension)</MoveTargetPath> <MoveTargetPath>$(ElectronAppDir)%(RecursiveDir)%(FileName)%(Extension)</MoveTargetPath>
</ElectronPublishFilesToMove> </ElectronPublishFilesToMove>
</ItemGroup> </ItemGroup>
@@ -503,20 +597,62 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<ElectronPublishUrlFullPath>$([System.IO.Path]::GetFullPath('$(PublishUrl.TrimEnd(&quot;\&quot;))'))</ElectronPublishUrlFullPath> <ElectronPublishUrlFullPath>$([System.IO.Path]::GetFullPath('$(PublishUrl.TrimEnd(&quot;\&quot;))'))</ElectronPublishUrlFullPath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<ElectronAppDirFullPath>$([System.IO.Path]::GetFullPath('$(ElectronAppDir)'))</ElectronAppDirFullPath>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<IsLinuxWsl>false</IsLinuxWsl> <IsLinuxWsl>false</IsLinuxWsl>
<IsLinuxWsl Condition="'$(ElectronPlatform)' == 'linux' AND $([MSBuild]::IsOSPlatform('Windows'))">true</IsLinuxWsl> <IsLinuxWsl Condition="'$(ElectronPlatform)' == 'linux' AND $([MSBuild]::IsOSPlatform('Windows'))">true</IsLinuxWsl>
<_NpmCmd>npm install electron-builder@$(ElectronBuilderVersion) --save-dev </_NpmCmd>
<_NpmCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmCmd)'</_NpmCmd>
</PropertyGroup> </PropertyGroup>
<Message Importance="High" Text="ElectronPublishApp: ElectronPublishDirFullPath - $(ElectronPublishDirFullPath)" /> <Message Importance="High" Text="ElectronPublishApp: ElectronPublishDirFullPath - $(ElectronPublishDirFullPath)" />
<Message Importance="High" Text="ElectronPublishApp: ElectronAppDirFullPath - $(ElectronAppDirFullPath)" />
<Exec Command="$(_NpmCmd)" WorkingDirectory="$(ElectronPublishDirFullPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="@(CopiedFiles->Count()) > 0"> <!-- Install the app's runtime (prod) dependencies into app\node_modules so they end up
inside app.asar. devDependencies (electron, eslint, typescript) are not needed here:
TypeScript is already compiled at build time and electron-builder fetches Electron itself. -->
<PropertyGroup>
<_NpmAppDepsCmd>npm install --omit=dev</_NpmAppDepsCmd>
<_NpmAppDepsCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmAppDepsCmd)'</_NpmAppDepsCmd>
</PropertyGroup>
<Exec Command="$(_NpmAppDepsCmd)" WorkingDirectory="$(ElectronAppDirFullPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="'$(ElectronSkipExecCommands)' != 'true' AND @(CopiedFiles->Count()) > 0">
<Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/> <Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/>
</Exec> </Exec>
<Message Importance="High" Text="Electron setup failed!" Condition="'$(ExecExitCode)' != '0'" /> <Message Importance="High" Text="Electron app dependency install failed!" Condition="'$(ExecExitCode)' != '' AND '$(ExecExitCode)' != '0'" />
<!-- Install electron-builder (the dev tool) into the publish root, which is the
electron-builder projectDir (cwd). app metadata + runtime deps are read from
app\ via directories.app (standard electron-builder two-package.json layout).
Render the dev package.json first: the two-package.json layout requires a
package.json at the projectDir, and electron-builder requires the 'build'
configuration to live in this development package.json (it is rejected in the
application package.json under app\). The .NET app's metadata/version come from
app\package.json instead. -->
<ItemGroup>
<DevTemplateProperty Include="ElectronPackageId" Value="$(ElectronPackageId)" />
<DevTemplateProperty Include="ElectronExecutableName" Value="$(ElectronExecutableName)" />
<DevTemplateProperty Include="Title" Value="$(Title)" />
<DevTemplateProperty Include="Version" Value="$(Version)" />
</ItemGroup>
<ReplaceTemplateTask TemplateFile="$(MSBuildThisFileDirectory)package.dev.template.json"
OutputFile="$([System.IO.Path]::Combine('$(ElectronPublishDirFullPath)', 'package.json'))"
TemplateProperties="@(DevTemplateProperty)"
Condition="@(CopiedFiles->Count()) > 0" />
<PropertyGroup>
<_NpmCmd>npm install electron-builder@$(ElectronBuilderVersion)</_NpmCmd>
<_NpmCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmCmd)'</_NpmCmd>
</PropertyGroup>
<Exec Command="$(_NpmCmd)" WorkingDirectory="$(ElectronPublishDirFullPath)" Timeout="600000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="'$(ElectronSkipExecCommands)' != 'true' AND @(CopiedFiles->Count()) > 0">
<Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/>
</Exec>
<Message Importance="High" Text="Electron setup failed!" Condition="'$(ExecExitCode)' != '' AND '$(ExecExitCode)' != '0'" />
<!-- Transform ElectronPublishUrlFullPath to a wslpath if IsLinuxWsl is true --> <!-- Transform ElectronPublishUrlFullPath to a wslpath if IsLinuxWsl is true -->
<Exec Command="wsl wslpath '$(ElectronPublishUrlFullPath)'" ConsoleToMsBuild="true" Condition="'$(IsLinuxWsl)' == 'true'"> <Exec Command="wsl wslpath '$(ElectronPublishUrlFullPath)'" ConsoleToMsBuild="true" Condition="'$(IsLinuxWsl)' == 'true'">
@@ -529,18 +665,22 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<ElectronPaParams Condition="'$(Version)' != ''">$(ElectronPaParams) -c.buildVersion &quot;$(Version)&quot;</ElectronPaParams> <ElectronPaParams Condition="'$(Version)' != ''">$(ElectronPaParams) -c.buildVersion &quot;$(Version)&quot;</ElectronPaParams>
<ElectronPaParams Condition="'$(Copyright)' != ''">$(ElectronPaParams) -c.copyright &quot;$(Copyright)&quot;</ElectronPaParams> <ElectronPaParams Condition="'$(Copyright)' != ''">$(ElectronPaParams) -c.copyright &quot;$(Copyright)&quot;</ElectronPaParams>
<ElectronPaParams>$(ElectronPaParams) -c.extraResources &quot;bin/**/*&quot;</ElectronPaParams> <ElectronPaParams>$(ElectronPaParams) -c.extraResources &quot;bin/**/*&quot;</ElectronPaParams>
<!-- Build app.asar from the 'app' subfolder only (host JS + runtime node_modules),
keeping the .NET app under 'bin' out of the asar. cwd stays the publish root so
extraResources and user icon paths under bin/ still resolve. -->
<ElectronPaParams>$(ElectronPaParams) -c.directories.app &quot;app&quot;</ElectronPaParams>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<_NpxCmd>npx electron-builder --config=./$(ElectronBuilderJson) --$(ElectronPlatform) --$(ElectronArch) -c.electronVersion=$(ElectronVersion) -c.directories.output &quot;$(ElectronPublishUrlFullPath)&quot; $(ElectronPaParams)</_NpxCmd> <_NpxCmd>npx electron-builder -c.$(ElectronPlatform).defaultArch=$(ElectronArch) --config=./$(ElectronBuilderJson) -c.electronVersion=$(ElectronVersion) --$(ElectronPlatform) --$(ElectronArch) -c.directories.output &quot;$(ElectronPublishUrlFullPath)&quot; $(ElectronPaParams)</_NpxCmd>
<_NpxCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpxCmd)'</_NpxCmd> <_NpxCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpxCmd)'</_NpxCmd>
</PropertyGroup> </PropertyGroup>
<Exec Command="$(_NpxCmd)" WorkingDirectory="$(ElectronPublishDirFullPath)" Timeout="1800000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="@(CopiedFiles->Count()) > 0"> <Exec Command="$(_NpxCmd)" WorkingDirectory="$(ElectronPublishDirFullPath)" Timeout="1800000" StandardOutputImportance="High" StandardErrorImportance="High" ContinueOnError="false" Condition="'$(ElectronSkipExecCommands)' != 'true' AND @(CopiedFiles->Count()) > 0">
<Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/> <Output TaskParameter="ExitCode" PropertyName="ExecExitCode"/>
</Exec> </Exec>
<Message Importance="High" Text="Electron setup failed!" Condition="'$(ExecExitCode)' != '0'" /> <Message Importance="High" Text="Electron setup failed!" Condition="'$(ExecExitCode)' != '' AND '$(ExecExitCode)' != '0'" />
</Target> </Target>

View File

@@ -9,10 +9,22 @@
ElectronCheckNoManifestJson; ElectronCheckNoManifestJson;
ElectronCheckElectronBuilderJson; ElectronCheckElectronBuilderJson;
ElectronCheckNoParentPaths; ElectronCheckNoParentPaths;
ElectronCheckPubxmlFiles ElectronCheckPubxmlFiles;
ElectronCheckHostHookFolder
</ElectronMigrationChecksDependsOn> </ElectronMigrationChecksDependsOn>
</PropertyGroup> </PropertyGroup>
<!--
Directories that are never scanned by the migration checks. Besides the well-known
bin/obj/publish/node_modules folders this also honors custom output paths (e.g. the
.NET 'artifacts' output layout) so that generated files never trigger false alarms.
-->
<PropertyGroup>
<_ElectronCheckExcludes>$(MSBuildProjectDirectory)\bin\**\*;$(MSBuildProjectDirectory)\obj\**\*;$(MSBuildProjectDirectory)\publish\**\*;$(MSBuildProjectDirectory)\node_modules\**\*;$(MSBuildProjectDirectory)\.electron\**\*</_ElectronCheckExcludes>
<_ElectronCheckExcludes Condition="'$(BaseOutputPath)' != ''">$(_ElectronCheckExcludes);$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(BaseOutputPath)'))**\*</_ElectronCheckExcludes>
<_ElectronCheckExcludes Condition="'$(BaseIntermediateOutputPath)' != ''">$(_ElectronCheckExcludes);$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)'))**\*</_ElectronCheckExcludes>
</PropertyGroup>
<!-- Main migration checks target that runs before build --> <!-- Main migration checks target that runs before build -->
<Target Name="ElectronMigrationChecks" <Target Name="ElectronMigrationChecks"
BeforeTargets="BeforeBuild" BeforeTargets="BeforeBuild"
@@ -32,16 +44,13 @@
<_InvalidPackageJson Include="$(MSBuildProjectDirectory)\**\package.json" <_InvalidPackageJson Include="$(MSBuildProjectDirectory)\**\package.json"
Exclude="$(MSBuildProjectDirectory)\package.json; Exclude="$(MSBuildProjectDirectory)\package.json;
$(MSBuildProjectDirectory)\ElectronHostHook\**\package.json; $(MSBuildProjectDirectory)\ElectronHostHook\**\package.json;
$(MSBuildProjectDirectory)\bin\**\package.json; $(_ElectronCheckExcludes)" />
$(MSBuildProjectDirectory)\obj\**\package.json; <!-- The root package-lock.json belongs to the root package.json, which is validated
$(MSBuildProjectDirectory)\publish\**\package.json; by ELECTRON008/ELECTRON009 instead of ELECTRON001. -->
$(MSBuildProjectDirectory)\node_modules\**\package.json" />
<_InvalidPackageLockJson Include="$(MSBuildProjectDirectory)\**\package-lock.json" <_InvalidPackageLockJson Include="$(MSBuildProjectDirectory)\**\package-lock.json"
Exclude="$(MSBuildProjectDirectory)\ElectronHostHook\**\package-lock.json; Exclude="$(MSBuildProjectDirectory)\package-lock.json;
$(MSBuildProjectDirectory)\bin\**\package-lock.json; $(MSBuildProjectDirectory)\ElectronHostHook\**\package-lock.json;
$(MSBuildProjectDirectory)\obj\**\package-lock.json; $(_ElectronCheckExcludes)" />
$(MSBuildProjectDirectory)\publish\**\package-lock.json;
$(MSBuildProjectDirectory)\node_modules\**\package-lock.json" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -141,10 +150,7 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<ItemGroup> <ItemGroup>
<_InvalidManifestJson Include="$(MSBuildProjectDirectory)\**\electron.manifest.json;$(MSBuildProjectDirectory)\**\electron-manifest.json" <_InvalidManifestJson Include="$(MSBuildProjectDirectory)\**\electron.manifest.json;$(MSBuildProjectDirectory)\**\electron-manifest.json"
Exclude="$(MSBuildProjectDirectory)\bin\**\*; Exclude="$(_ElectronCheckExcludes)" />
$(MSBuildProjectDirectory)\obj\**\*;
$(MSBuildProjectDirectory)\publish\**\*;
$(MSBuildProjectDirectory)\node_modules\**\*" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -187,10 +193,7 @@ MIGRATION REQUIRED:
<ItemGroup> <ItemGroup>
<_ElectronBuilderJsonWrongLocation Include="$(MSBuildProjectDirectory)\**\electron-builder.json" <_ElectronBuilderJsonWrongLocation Include="$(MSBuildProjectDirectory)\**\electron-builder.json"
Exclude="$(MSBuildProjectDirectory)\Properties\electron-builder.json; Exclude="$(MSBuildProjectDirectory)\Properties\electron-builder.json;
$(MSBuildProjectDirectory)\bin\**\*; $(_ElectronCheckExcludes)" />
$(MSBuildProjectDirectory)\obj\**\*;
$(MSBuildProjectDirectory)\publish\**\*;
$(MSBuildProjectDirectory)\node_modules\**\*" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -296,10 +299,11 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
</_PubxmlFileInfo> </_PubxmlFileInfo>
</ItemGroup> </ItemGroup>
<!-- Check each file for WebPublishMethod presence --> <!-- Check each file for the ASP.NET specific properties (WebPublishMethod / ProjectGuid) -->
<ItemGroup Condition="'$(_HasPubxmlFiles)' == 'true'"> <ItemGroup Condition="'$(_HasPubxmlFiles)' == 'true'">
<_PubxmlFileInfoWithFlags Include="@(_PubxmlFileInfo)" Condition="'%(Identity)' != ''"> <_PubxmlFileInfoWithFlags Include="@(_PubxmlFileInfo)" Condition="'%(Identity)' != ''">
<HasWebPublishMethod>$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileContent)', '&lt;WebPublishMethod&gt;'))</HasWebPublishMethod> <HasWebPublishMethod>$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileContent)', '&lt;WebPublishMethod&gt;'))</HasWebPublishMethod>
<HasProjectGuid>$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileContent)', '&lt;ProjectGuid&gt;'))</HasProjectGuid>
</_PubxmlFileInfoWithFlags> </_PubxmlFileInfoWithFlags>
</ItemGroup> </ItemGroup>
@@ -309,10 +313,10 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
Condition="'$(_IsAspNetProject)' == 'true' AND '%(HasWebPublishMethod)' == 'False'" /> Condition="'$(_IsAspNetProject)' == 'true' AND '%(HasWebPublishMethod)' == 'False'" />
</ItemGroup> </ItemGroup>
<!-- For Console/Non-ASP.NET projects: find pubxml files that HAVE WebPublishMethod --> <!-- For Console/Non-ASP.NET projects: find pubxml files that HAVE ASP.NET specific properties -->
<ItemGroup> <ItemGroup>
<_ConsolePubxmlWithAspNetProperties Include="@(_PubxmlFileInfoWithFlags)" <_ConsolePubxmlWithAspNetProperties Include="@(_PubxmlFileInfoWithFlags)"
Condition="'$(_IsAspNetProject)' != 'true' AND '%(HasWebPublishMethod)' == 'True'" /> Condition="'$(_IsAspNetProject)' != 'true' AND ('%(HasWebPublishMethod)' == 'True' OR '%(HasProjectGuid)' == 'True')" />
</ItemGroup> </ItemGroup>
<!-- Warning for ASP.NET projects with incorrect pubxml files --> <!-- Warning for ASP.NET projects with incorrect pubxml files -->
@@ -336,7 +340,7 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<Warning <Warning
Condition="'@(_ConsolePubxmlWithAspNetProperties)' != ''" Condition="'@(_ConsolePubxmlWithAspNetProperties)' != ''"
Code="ELECTRON007" Code="ELECTRON007"
Text="The publish profile '%(_ConsolePubxmlWithAspNetProperties.Identity)' appears to be configured for ASP.NET publishing (containing the WebPublishMethod property), but this is a console application project. Text="The publish profile '%(_ConsolePubxmlWithAspNetProperties.Identity)' appears to be configured for ASP.NET publishing (containing the WebPublishMethod and/or ProjectGuid property), but this is a console application project.
RECOMMENDED ACTION: RECOMMENDED ACTION:
1. Delete the existing publish profiles 1. Delete the existing publish profiles
@@ -351,4 +355,59 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
</Target> </Target>
<!--
Check 6: The ElectronHostHook folder must be set up correctly
Custom host hook TypeScript code is only compiled and packaged when the folder also
contains a package.json (and a tsconfig.json for the TypeScript compilation). Without
those files the hook code is silently ignored.
-->
<Target Name="ElectronCheckHostHookFolder"
Condition="Exists('$(MSBuildProjectDirectory)\ElectronHostHook')">
<ItemGroup>
<_HostHookTsFiles Include="$(MSBuildProjectDirectory)\ElectronHostHook\*.ts" />
</ItemGroup>
<PropertyGroup>
<_HasHostHookTsFiles>false</_HasHostHookTsFiles>
<_HasHostHookTsFiles Condition="@(_HostHookTsFiles-&gt;Count()) &gt; 0">true</_HasHostHookTsFiles>
</PropertyGroup>
<Warning Condition="'$(_HasHostHookTsFiles)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\ElectronHostHook\package.json')"
Code="ELECTRON010"
Text="The 'ElectronHostHook' folder contains TypeScript files but no package.json.
Folder: $(MSBuildProjectDirectory)\ElectronHostHook
The host hook code is only compiled and packaged when the folder contains a package.json declaring its npm dependencies. Without it the hook is silently ignored at runtime.
HOW TO FIX:
Add a package.json to the 'ElectronHostHook' folder, for example:
{
&quot;name&quot;: &quot;electron-host-hook&quot;,
&quot;version&quot;: &quot;1.0.0&quot;,
&quot;main&quot;: &quot;index.js&quot;,
&quot;dependencies&quot;: { &quot;socket.io&quot;: &quot;^4.8.1&quot; },
&quot;devDependencies&quot;: { &quot;typescript&quot;: &quot;^5.9.3&quot; }
}
For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migration-Checks#6-electronhosthook-folder-validation" />
<Warning Condition="'$(_HasHostHookTsFiles)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\ElectronHostHook\tsconfig.json')"
Code="ELECTRON011"
Text="The 'ElectronHostHook' folder contains TypeScript files but no tsconfig.json.
Folder: $(MSBuildProjectDirectory)\ElectronHostHook
Without a tsconfig.json the hook TypeScript sources are not compiled with the expected settings.
HOW TO FIX:
Add a tsconfig.json to the 'ElectronHostHook' folder.
For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migration-Checks#6-electronhosthook-folder-validation" />
</Target>
</Project> </Project>

View File

@@ -437,6 +437,8 @@
<EnumValue Name="40.10.0" DisplayName="40.10.0" /> <EnumValue Name="40.10.0" DisplayName="40.10.0" />
<EnumValue Name="40.10.1" DisplayName="40.10.1" /> <EnumValue Name="40.10.1" DisplayName="40.10.1" />
<EnumValue Name="40.10.2" DisplayName="40.10.2" /> <EnumValue Name="40.10.2" DisplayName="40.10.2" />
<EnumValue Name="40.10.3" DisplayName="40.10.3" />
<EnumValue Name="40.10.4" DisplayName="40.10.4" />
<EnumValue Name="41.0.0" DisplayName="41.0.0" /> <EnumValue Name="41.0.0" DisplayName="41.0.0" />
<EnumValue Name="41.0.1" DisplayName="41.0.1" /> <EnumValue Name="41.0.1" DisplayName="41.0.1" />
<EnumValue Name="41.0.2" DisplayName="41.0.2" /> <EnumValue Name="41.0.2" DisplayName="41.0.2" />
@@ -456,6 +458,8 @@
<EnumValue Name="41.6.1" DisplayName="41.6.1" /> <EnumValue Name="41.6.1" DisplayName="41.6.1" />
<EnumValue Name="41.7.0" DisplayName="41.7.0" /> <EnumValue Name="41.7.0" DisplayName="41.7.0" />
<EnumValue Name="41.7.1" DisplayName="41.7.1" /> <EnumValue Name="41.7.1" DisplayName="41.7.1" />
<EnumValue Name="41.7.2" DisplayName="41.7.2" />
<EnumValue Name="41.8.0" DisplayName="41.8.0" />
<EnumValue Name="42.0.0" DisplayName="42.0.0" /> <EnumValue Name="42.0.0" DisplayName="42.0.0" />
<EnumValue Name="42.0.1" DisplayName="42.0.1" /> <EnumValue Name="42.0.1" DisplayName="42.0.1" />
<EnumValue Name="42.1.0" DisplayName="42.1.0" /> <EnumValue Name="42.1.0" DisplayName="42.1.0" />
@@ -464,6 +468,8 @@
<EnumValue Name="42.3.1" DisplayName="42.3.1" /> <EnumValue Name="42.3.1" DisplayName="42.3.1" />
<EnumValue Name="42.3.2" DisplayName="42.3.2" /> <EnumValue Name="42.3.2" DisplayName="42.3.2" />
<EnumValue Name="42.3.3" DisplayName="42.3.3" /> <EnumValue Name="42.3.3" DisplayName="42.3.3" />
<EnumValue Name="42.4.0" DisplayName="42.4.0" />
<EnumValue Name="42.4.1" DisplayName="42.4.1" />
</EnumProperty> </EnumProperty>
<EnumProperty Name="ElectronBuilderVersion" <EnumProperty Name="ElectronBuilderVersion"
@@ -547,6 +553,30 @@
Category="General"> Category="General">
</StringProperty> </StringProperty>
<StringProperty Name="ElectronExecutableName"
DisplayName="Executable Name"
Description="Name of the executable inside the package, without extension. Defaults to the title on Windows and to the package id on Linux and macOS"
Category="General">
</StringProperty>
<StringProperty Name="ElectronRootDir"
DisplayName="Electron Root Directory"
Description="Location of the Electron binary relative to the .NET application directory of a packaged app. Default: ../.. (the .NET app lives in resources/bin)"
Category="General">
</StringProperty>
<BoolProperty Name="ElectronSkipExecCommands"
DisplayName="Skip npm/npx Commands"
Description="If enabled, the npm and npx invocations of the Electron build and publish targets are skipped"
Category="General">
</BoolProperty>
<StringProperty Name="ElectronIntermediatePublishDir"
DisplayName="Intermediate Publish Directory"
Description="Overrides the intermediate publish directory used while packaging. Default: $(IntermediateOutputPath)PubTmp\"
Category="General">
</StringProperty>
<StringProperty Name="ElectronSplashScreen" <StringProperty Name="ElectronSplashScreen"
DisplayName="Splash Screen Image" DisplayName="Splash Screen Image"
Description="Choose a PNG image to be shown as splash screen on startup" Description="Choose a PNG image to be shown as splash screen on startup"
@@ -566,7 +596,7 @@
<StringProperty Name="ElectronIcon" <StringProperty Name="ElectronIcon"
DisplayName="App Icon" DisplayName="App Icon"
Description="Choose a ICO file to be used as application icon" Description="Choose an icon path for the app (e.g. .ico, .icns, or modern macOS .icon folder)"
Subtype="File" Subtype="File"
Category="General"> Category="General">
<StringProperty.DataSource> <StringProperty.DataSource>
@@ -575,7 +605,7 @@
<StringProperty.ValueEditors> <StringProperty.ValueEditors>
<ValueEditor EditorType="FilePath"> <ValueEditor EditorType="FilePath">
<ValueEditor.Metadata> <ValueEditor.Metadata>
<NameValuePair Name="FileTypeFilter" Value="Icon files (*.ico)|*.ico|All files (*.*)|*.*" /> <NameValuePair Name="FileTypeFilter" Value="Icon files (*.ico,*.icns)|*.ico;*.icns|All files (*.*)|*.*" />
</ValueEditor.Metadata> </ValueEditor.Metadata>
</ValueEditor> </ValueEditor>
</StringProperty.ValueEditors> </StringProperty.ValueEditors>

View File

@@ -0,0 +1,25 @@
{
"name": "electron-builder-host",
"version": "$(Version)",
"private": true,
"build": {
"appId": "$(ElectronPackageId)",
"win": {
"executableName": "$(ElectronExecutableName)"
},
"mac": {
"executableName": "$(ElectronExecutableName)"
},
"linux": {
"desktop": {
"entry": { "Name": "$(Title)" }
},
"executableName": "$(ElectronExecutableName)"
},
"deb": {
"desktop": {
"entry": { "Name": "$(Title)" }
}
}
}
}

View File

@@ -1,20 +1,6 @@
{ {
"name": "$(ElectronPackageId)", "name": "$(ElectronPackageId)",
"productName": "$(ElectronTitle)", "productName": "$(ElectronTitle)",
"build": {
"appId": "$(ElectronPackageId)",
"linux": {
"desktop": {
"entry": { "Name": "$(Title)" }
},
"executableName": "$(ElectronPackageId)"
},
"deb": {
"desktop": {
"entry": { "Name": "$(Title)" }
}
}
},
"description": "$(Description)", "description": "$(Description)",
"version": "$(Version)", "version": "$(Version)",
"main": "main.js", "main": "main.js",

View File

@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.5.1</Version> <Version>0.6.0</Version>
<PackageNamePrefix>ElectronNET.Core</PackageNamePrefix> <PackageNamePrefix>ElectronNET.Core</PackageNamePrefix>
<Authors>Gregor Biswanger, Florian Rappl, softworkz</Authors> <Authors>Gregor Biswanger, Florian Rappl, softworkz</Authors>
<Product>Electron.NET</Product> <Product>Electron.NET</Product>
@@ -18,6 +18,7 @@
<FileVersion>$(Version)</FileVersion> <FileVersion>$(Version)</FileVersion>
<Version>$(Version)$(VersionPostFix)</Version> <Version>$(Version)$(VersionPostFix)</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
<ElectronNetTargetFrameworks Condition="'$(ElectronNetTargetFrameworks)' == ''">net6.0;net8.0;net10.0</ElectronNetTargetFrameworks>
</PropertyGroup> </PropertyGroup>
</Project> </Project>