@lakutata/template-electron-react 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +12 -0
- package/features/i18n/packages/app/package.json +6 -0
- package/features/i18n/packages/app/src/locales/en.json +5 -0
- package/features/i18n/packages/app/src/locales/zh-CN.json +5 -0
- package/package.json +22 -0
- package/template/Dockerfile.installer +17 -0
- package/template/README.md +73 -0
- package/template/_dockerignore +8 -0
- package/template/_gitignore +13 -0
- package/template/package.json +32 -0
- package/template/packages/app/package.json +83 -0
- package/template/packages/app/src/App.ts +35 -0
- package/template/packages/app/src/components/MainWindow.ts +110 -0
- package/template/packages/app/src/config/Config.ts +75 -0
- package/template/packages/app/src/controllers/ExampleController.ts +41 -0
- package/template/packages/app/src/dtos/GreetDTO.ts +7 -0
- package/template/packages/app/src/preload.cts +15 -0
- package/template/packages/app/src/tests/App.spec.ts +109 -0
- package/template/packages/app/tsconfig.json +9 -0
- package/template/packages/resources/logo.png +0 -0
- package/template/packages/resources/package.json +6 -0
- package/template/packages/ui/index.html +13 -0
- package/template/packages/ui/package.json +23 -0
- package/template/packages/ui/src/App.tsx +26 -0
- package/template/packages/ui/src/ipc.ts +125 -0
- package/template/packages/ui/src/main.tsx +11 -0
- package/template/packages/ui/tsconfig.json +15 -0
- package/template/packages/ui/vite.config.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present Lakutata
|
|
4
|
+
|
|
5
|
+
All rights reserved
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @lakutata/template-electron-react
|
|
2
|
+
|
|
3
|
+
The Electron template of the [lakutata](https://github.com/lakutata/lakutata) projects with React: the main process is a
|
|
4
|
+
lakutata application serving the actions of its controllers over IPC to the renderer (React, Vite), isolated from Node.js;
|
|
5
|
+
the application is packaged by `lakutata package installer` (electron-builder), for Linux in Docker (`npm run package:linux`).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @lakutata/cli create --name my-app --template electron-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The files of the project are in `template/`; `lakutata create` fills `{$APP_NAME}`, `{$APP_ID}`, `{$APP_DESC}`,
|
|
12
|
+
`{$APP_AUTHOR}` and `{$APP_LICENSE}`, and renames `_gitignore` and `_dockerignore`.
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lakutata/template-electron-react",
|
|
3
|
+
"version": "3.0.0-beta.0",
|
|
4
|
+
"description": "The Electron template of the lakutata projects with React: the main process is a lakutata application serving the actions of its controllers to the React renderer (Vite) over IPC, packaged by electron-builder.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lakutata",
|
|
7
|
+
"lakutata-template"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"files": [
|
|
11
|
+
"template",
|
|
12
|
+
"features",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"lakutata": {
|
|
17
|
+
"template": "electron-react",
|
|
18
|
+
"features": {
|
|
19
|
+
"i18n": "The messages in several languages (lakutata/com/i18n): src/locales, the language of each request"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# The installers of Linux (npm run package:linux), made on Linux by electron-builder: the packages of the configuration
|
|
2
|
+
# (lakutata.installer of packages/app/package.json) in dist/installer. The ones of another architecture: docker buildx
|
|
3
|
+
# build --platform linux/amd64 ... The packages of fpm (deb, rpm) are made by the fpm of the system (the one electron-builder
|
|
4
|
+
# downloads is the one of x64); add the system libraries your native addons need
|
|
5
|
+
FROM node:22-bookworm AS build
|
|
6
|
+
RUN apt-get update \
|
|
7
|
+
&& apt-get install -y --no-install-recommends build-essential python3 ruby ruby-dev rpm \
|
|
8
|
+
&& gem install fpm --no-document \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
ENV USE_SYSTEM_FPM=true
|
|
11
|
+
WORKDIR /project
|
|
12
|
+
COPY . .
|
|
13
|
+
RUN (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build && npm run package -w app -- --outdir /installer \
|
|
14
|
+
&& mkdir /packages && find /installer -maxdepth 1 -type f ! -name '*.yml' ! -name '*.blockmap' -exec cp {} /packages \;
|
|
15
|
+
|
|
16
|
+
FROM scratch
|
|
17
|
+
COPY --from=build /packages /
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# {$APP_NAME}
|
|
2
|
+
|
|
3
|
+
{$APP_DESC}
|
|
4
|
+
|
|
5
|
+
A [lakutata](https://github.com/lakutata/lakutata) Electron application. The project is a workspace:
|
|
6
|
+
|
|
7
|
+
- `packages/app`: the main process, a lakutata application. Its controllers serve their `@ServiceAction` actions to the
|
|
8
|
+
renderer over IPC (`electronIPCServiceEntrypoint`); `MainWindow` is its window;
|
|
9
|
+
- `packages/ui`: the renderer, React built by Vite. It is isolated from Node.js and from Electron: `window.ipc`
|
|
10
|
+
(`packages/ui/src/ipc.ts`, through the preload script `packages/app/src/preload.cts`) invokes the actions of the main
|
|
11
|
+
process, `window.ipc.invoke({cmd: 'greet', name: 'you'})`, and listens to the events it pushes,
|
|
12
|
+
`window.ipc.on('greeted', listener)` (`once`, `off`);
|
|
13
|
+
- `packages/resources`: the resources of the application, its icon (`logo.png`: the icons of its installers and of its
|
|
14
|
+
window).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run dev # the renderer served by Vite (hot reload) in the window of the application, with its DevTools
|
|
18
|
+
npm test # the tests of the packages (the actions of the main process, without Electron)
|
|
19
|
+
npm run build # build the packages, in the order of their dependencies (ui, then app)
|
|
20
|
+
npm start # start the built application
|
|
21
|
+
npm run package:host # the installers of this platform: dist/installer (deb, dmg, nsis…)
|
|
22
|
+
npm run package:linux # the ones of Linux, in Docker (Dockerfile.installer): dist/installer
|
|
23
|
+
npm run version:patch # give the packages their next version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A new action: a method of a controller of `packages/app/src/controllers` decorated by `@ServiceAction({cmd: 'name'}, DTO)`,
|
|
27
|
+
invoked by the renderer with `window.ipc.invoke({cmd: 'name', ...input})`: its input is validated by its DTO, its result is
|
|
28
|
+
given back, its errors are thrown in the renderer (with their `errno`). Each page is a client of the main process: its id
|
|
29
|
+
is the `clientId` of the context of its requests, which the entrypoint pushes events to, `this.entrypoint.notify(clientId,
|
|
30
|
+
'event', ...args)` (the id may be kept to push them later), or to every client, `this.entrypoint.broadcast('event',
|
|
31
|
+
...args)`.
|
|
32
|
+
|
|
33
|
+
In development (not packaged), the actions are also served over HTTP on the port `DEV_PORT` (9000 by default), for the
|
|
34
|
+
tools such as Postman or curl: `POST /` with `{"cmd": "greet", "name": "you"}` answers the response of the action; the id
|
|
35
|
+
of the client in the `id` header. `GET /events?id=<client>` streams the events pushed to the client (Server-Sent Events):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
curl -N 'http://localhost:9000/events?id=me' &
|
|
39
|
+
curl -X POST http://localhost:9000/ -H 'id: me' -H 'Content-Type: application/json' -d '{"cmd": "greet", "name": "you"}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The packaging
|
|
43
|
+
|
|
44
|
+
`npm run package:host` builds the packages and makes the installers of the platform it runs on with `lakutata package installer`
|
|
45
|
+
(electron-builder, configured by the `lakutata.installer` of `packages/app/package.json`): the main process bundled with
|
|
46
|
+
its dependencies in the archive of the application, its native addons, the preload script and the built renderer (its
|
|
47
|
+
`lakutata.resources`) next to it, in the resources of Electron. The installers of Windows are made on Windows, the ones of
|
|
48
|
+
macOS on macOS, the ones of Linux on Linux: `npm run package:linux` makes them in Docker on any platform
|
|
49
|
+
(`Dockerfile.installer`, whose system libraries are the ones the native addons need: add yours; `docker buildx build
|
|
50
|
+
--platform linux/amd64` makes the ones of another architecture).
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"installer": {
|
|
54
|
+
"runtime": "electron",
|
|
55
|
+
"linux": {"targets": ["deb"], "scripts": {"postinstall": "build/linux/postinstall.sh"}},
|
|
56
|
+
"mac": {"targets": ["dmg"]},
|
|
57
|
+
"win": {"targets": ["nsis", "portable"], "scripts": {"postinstall": "build/win/postinstall.ps1"}},
|
|
58
|
+
"electron": {"linux": {"category": "Utility"}}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`electron` is the configuration of electron-builder (its icons, its file associations, its signature, its updates…),
|
|
63
|
+
merged over the one made from `lakutata.installer`. The scripts of the installers (`scripts`, run by the deb and rpm
|
|
64
|
+
packages, the pkg package of macOS and the installer of NSIS) are the ones of `lakutata package installer` (its documentation in
|
|
65
|
+
`@lakutata/cli`).
|
|
66
|
+
|
|
67
|
+
The deb packages need the email of their maintainer: the `author` of `packages/app/package.json` is `"Name <email>"` (and
|
|
68
|
+
its `homepage` is the one of the packages).
|
|
69
|
+
|
|
70
|
+
The native addons built for Node.js (not with N-API) are rebuilt for Electron before packaging: `npx @electron/rebuild`
|
|
71
|
+
(and `npm rebuild` to run the tests with Node.js again).
|
|
72
|
+
|
|
73
|
+
Electron downloads its binary the first time it runs (`npx install-electron` downloads it ahead, for a CI caching it).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{$APP_ID}",
|
|
3
|
+
"productName": "{$APP_NAME}",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "{$APP_DESC}",
|
|
6
|
+
"author": "{$APP_AUTHOR}",
|
|
7
|
+
"license": "{$APP_LICENSE}",
|
|
8
|
+
"private": true,
|
|
9
|
+
"workspaces": [
|
|
10
|
+
"packages/*"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "lakutata run rebuild",
|
|
14
|
+
"test": "lakutata run test",
|
|
15
|
+
"dev": "lakutata run dev --parallel",
|
|
16
|
+
"start": "npm start -w app",
|
|
17
|
+
"package:host": "npm run build && npm run package -w app -- --outdir ../../dist/installer",
|
|
18
|
+
"package:linux": "docker buildx build --file Dockerfile.installer --output type=local,dest=dist/installer .",
|
|
19
|
+
"version:patch": "lakutata version patch --yes"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@lakutata/builder": "^{$LAKUTATA_VERSION}",
|
|
23
|
+
"@lakutata/cli": "^{$LAKUTATA_VERSION}",
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"electron": "^44.5.1",
|
|
26
|
+
"electron-builder": "^26.15.3",
|
|
27
|
+
"typescript": "^6.0.3"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "app",
|
|
3
|
+
"productName": "{$APP_NAME}",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "{$APP_DESC}",
|
|
6
|
+
"author": "{$APP_AUTHOR}",
|
|
7
|
+
"license": "{$APP_LICENSE}",
|
|
8
|
+
"private": true,
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "build/App.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"clean": "lakutata clean",
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"rebuild": "npm run clean && npm run build",
|
|
15
|
+
"dev": "npm run rebuild && electron build/App.js --renderer=http://localhost:5173",
|
|
16
|
+
"start": "electron build/App.js",
|
|
17
|
+
"test": "node --test \"build/tests/**/*.spec.js\"",
|
|
18
|
+
"package": "lakutata package installer"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"lakutata": "^{$LAKUTATA_VERSION}",
|
|
22
|
+
"ui": "^0.0.1",
|
|
23
|
+
"resources": "^0.0.1"
|
|
24
|
+
},
|
|
25
|
+
"lakutata": {
|
|
26
|
+
"icon": "../resources/logo.png",
|
|
27
|
+
"resources": [
|
|
28
|
+
{
|
|
29
|
+
"from": "../ui/dist",
|
|
30
|
+
"to": "renderer"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"from": "build/preload.cjs",
|
|
34
|
+
"to": "preload.cjs"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"from": "../resources/logo.png",
|
|
38
|
+
"to": "logo.png"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"installer": {
|
|
42
|
+
"runtime": "electron",
|
|
43
|
+
"name": "{$APP_ID}",
|
|
44
|
+
"appId": "{$APP_ID}",
|
|
45
|
+
"linux": {
|
|
46
|
+
"targets": [
|
|
47
|
+
"deb"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"mac": {
|
|
51
|
+
"targets": [
|
|
52
|
+
"dmg"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"win": {
|
|
56
|
+
"targets": [
|
|
57
|
+
"nsis",
|
|
58
|
+
"portable"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"electron": {
|
|
62
|
+
"linux": {
|
|
63
|
+
"category": "Utility",
|
|
64
|
+
"syncDesktopName": true
|
|
65
|
+
},
|
|
66
|
+
"deb": {
|
|
67
|
+
"depends": [
|
|
68
|
+
"libgtk-3-0",
|
|
69
|
+
"libnotify4",
|
|
70
|
+
"libnss3",
|
|
71
|
+
"libxss1",
|
|
72
|
+
"libxtst6",
|
|
73
|
+
"xdg-utils",
|
|
74
|
+
"libatspi2.0-0",
|
|
75
|
+
"libuuid1",
|
|
76
|
+
"libsecret-1-0",
|
|
77
|
+
"libasound2"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {Application, resourcesPath, type ILogger} from 'lakutata'
|
|
2
|
+
import electron from 'electron'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {fileURLToPath} from 'node:url'
|
|
5
|
+
import {installerConfig} from './config/Config.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The directory of the preload script and of the built renderer: the resources of the packaged application
|
|
9
|
+
* (lakutata.resources), the directory of the main process in development (build/, the renderer of the ui package)
|
|
10
|
+
*/
|
|
11
|
+
const directory: string = resourcesPath() ?? path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The URL of the renderer in development (--renderer=<url>, the dev server of Vite)
|
|
15
|
+
*/
|
|
16
|
+
const renderer: string | undefined = process.argv.find((argument: string): boolean => argument.startsWith('--renderer='))?.slice('--renderer='.length)
|
|
17
|
+
|
|
18
|
+
//One instance of the application: a second one gives its window back to the first one
|
|
19
|
+
if (!electron.app.requestSingleInstanceLock()) {
|
|
20
|
+
electron.app.exit(0)
|
|
21
|
+
} else {
|
|
22
|
+
Application
|
|
23
|
+
.run(installerConfig({
|
|
24
|
+
directory: directory,
|
|
25
|
+
renderer: renderer,
|
|
26
|
+
mode: electron.app.isPackaged ? 'production' : 'development',
|
|
27
|
+
//The actions served to the tools in development (Postman, curl), not by the packaged application
|
|
28
|
+
devPort: electron.app.isPackaged ? undefined : Number(process.env.DEV_PORT ?? 9000)
|
|
29
|
+
}))
|
|
30
|
+
.onLaunched((app: Application, log: ILogger): void => log.info('%s started in %s mode', app.appName, app.mode()))
|
|
31
|
+
.onFatalException((error: Error, log: ILogger): void => {
|
|
32
|
+
log.error('The application cannot start: %s', error.message)
|
|
33
|
+
electron.app.exit(1)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {Application, Component, DTO} from 'lakutata'
|
|
2
|
+
import {Configurable, Inject} from 'lakutata/decorator/di'
|
|
3
|
+
import type {BrowserWindow} from 'electron'
|
|
4
|
+
import {existsSync} from 'node:fs'
|
|
5
|
+
import {createRequire} from 'node:module'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The window of the application: the renderer, isolated from Node.js (it invokes the actions of the main process through
|
|
10
|
+
* the preload script only)
|
|
11
|
+
*/
|
|
12
|
+
export class MainWindow extends Component {
|
|
13
|
+
|
|
14
|
+
@Inject(Application)
|
|
15
|
+
protected readonly app: Application
|
|
16
|
+
|
|
17
|
+
@Configurable(DTO.Number().optional().default(1024))
|
|
18
|
+
protected readonly width: number
|
|
19
|
+
|
|
20
|
+
@Configurable(DTO.Number().optional().default(720))
|
|
21
|
+
protected readonly height: number
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The directory of the preload script and of the built renderer (the resources of the packaged application)
|
|
25
|
+
*/
|
|
26
|
+
@Configurable(DTO.String().optional())
|
|
27
|
+
protected readonly directory?: string
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The URL of the renderer in development, the built renderer otherwise
|
|
31
|
+
*/
|
|
32
|
+
@Configurable(DTO.String().optional())
|
|
33
|
+
protected readonly renderer?: string
|
|
34
|
+
|
|
35
|
+
#window: BrowserWindow | undefined
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The index.html of the built renderer: in the resources of the packaged application, in the ui package otherwise
|
|
39
|
+
*/
|
|
40
|
+
protected rendererFile(): string {
|
|
41
|
+
const packaged: string = path.join(this.directory ?? '', 'renderer', 'index.html')
|
|
42
|
+
if (existsSync(packaged)) return packaged
|
|
43
|
+
return path.join(path.dirname(createRequire(import.meta.url).resolve('ui/package.json')), 'dist', 'index.html')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The icon of the window (Windows, Linux: the windows of macOS show the one of the application): in the resources of
|
|
48
|
+
* the packaged application, in the resources package otherwise
|
|
49
|
+
*/
|
|
50
|
+
protected iconFile(): string {
|
|
51
|
+
const packaged: string = path.join(this.directory ?? '', 'logo.png')
|
|
52
|
+
if (existsSync(packaged)) return packaged
|
|
53
|
+
return path.join(path.dirname(createRequire(import.meta.url).resolve('resources/package.json')), 'logo.png')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load the URL of the renderer in development: the dev server of Vite may start after the application
|
|
58
|
+
* @param window
|
|
59
|
+
* @param url
|
|
60
|
+
*/
|
|
61
|
+
protected async loadURL(window: BrowserWindow, url: string): Promise<void> {
|
|
62
|
+
for (let attempt: number = 1; ; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await window.loadURL(url)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (attempt >= 30 || window.isDestroyed()) throw error
|
|
67
|
+
await new Promise((resolve: (value: unknown) => void): unknown => setTimeout(resolve, 500))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected async init(): Promise<void> {
|
|
73
|
+
//Electron is loaded with the window: the tests, run by Node.js, serve the actions without it (the package of
|
|
74
|
+
//Electron downloads its binary when it is loaded)
|
|
75
|
+
const {app, BrowserWindow} = (await import('electron')).default
|
|
76
|
+
await app.whenReady()
|
|
77
|
+
const window: BrowserWindow = new BrowserWindow({
|
|
78
|
+
width: this.width,
|
|
79
|
+
height: this.height,
|
|
80
|
+
title: this.app.appName,
|
|
81
|
+
icon: this.iconFile(),
|
|
82
|
+
show: false,
|
|
83
|
+
webPreferences: {
|
|
84
|
+
preload: path.join(this.directory ?? '', 'preload.cjs'),
|
|
85
|
+
contextIsolation: true,
|
|
86
|
+
nodeIntegration: false,
|
|
87
|
+
sandbox: true
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
this.#window = window
|
|
91
|
+
window.once('ready-to-show', (): void => window.show())
|
|
92
|
+
//The application ends with its window
|
|
93
|
+
window.once('closed', (): void => this.app.exit(0))
|
|
94
|
+
if (this.renderer) {
|
|
95
|
+
await this.loadURL(window, this.renderer)
|
|
96
|
+
window.webContents.openDevTools({mode: 'detach'})
|
|
97
|
+
} else {
|
|
98
|
+
await window.loadFile(this.rendererFile())
|
|
99
|
+
}
|
|
100
|
+
//A second instance of the application gives its window back
|
|
101
|
+
app.on('second-instance', (): void => {
|
|
102
|
+
if (window.isMinimized()) window.restore()
|
|
103
|
+
window.focus()
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected async destroy(): Promise<void> {
|
|
108
|
+
if (this.#window && !this.#window.isDestroyed()) this.#window.destroy()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type {ApplicationOptions} from 'lakutata'
|
|
2
|
+
import {buildEntrypoints, httpServiceEntrypoint} from 'lakutata/com/entrypoint'
|
|
3
|
+
import {electronIPCServiceEntrypoint, type IPCMain} from 'lakutata/adapter/electron'
|
|
4
|
+
import {MainWindow} from '../components/MainWindow.js'
|
|
5
|
+
import {ExampleController} from '../controllers/ExampleController.js'
|
|
6
|
+
//@if i18n
|
|
7
|
+
import {buildI18nOptions, I18nMiddleware} from 'lakutata/com/i18n'
|
|
8
|
+
import en from '../locales/en.json' with {type: 'json'}
|
|
9
|
+
import zhCN from '../locales/zh-CN.json' with {type: 'json'}
|
|
10
|
+
//@end
|
|
11
|
+
|
|
12
|
+
export type ConfigOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* The directory of the main process, where the preload script and the built renderer are
|
|
15
|
+
*/
|
|
16
|
+
directory?: string
|
|
17
|
+
/**
|
|
18
|
+
* The URL of the renderer in development (the dev server of Vite), the built renderer otherwise
|
|
19
|
+
*/
|
|
20
|
+
renderer?: string
|
|
21
|
+
mode?: 'development' | 'production'
|
|
22
|
+
/**
|
|
23
|
+
* Open the window, true by default (the tests serve the actions without window)
|
|
24
|
+
*/
|
|
25
|
+
window?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* The ipcMain serving the actions, the one of Electron by default (the tests give theirs)
|
|
28
|
+
*/
|
|
29
|
+
ipcMain?: IPCMain
|
|
30
|
+
/**
|
|
31
|
+
* The port of the HTTP entrypoint of the development, none by default: the actions invoked by the tools (Postman,
|
|
32
|
+
* curl: POST / with their pattern and their input), the events pushed to them received (GET /events, Server-Sent
|
|
33
|
+
* Events), the id of their client in the header id (or the query id of the events)
|
|
34
|
+
*/
|
|
35
|
+
devPort?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The options of the application
|
|
40
|
+
* @param options
|
|
41
|
+
*/
|
|
42
|
+
export function installerConfig(options: ConfigOptions = {}): ApplicationOptions {
|
|
43
|
+
return {
|
|
44
|
+
id: '{$APP_ID}',
|
|
45
|
+
name: '{$APP_NAME}',
|
|
46
|
+
mode: options.mode ?? 'development',
|
|
47
|
+
components: {
|
|
48
|
+
//@if i18n
|
|
49
|
+
//The messages of the languages (src/locales), bundled with the code; created by the first request
|
|
50
|
+
i18n: buildI18nOptions({messages: {'en': en, 'zh-CN': zhCN}}),
|
|
51
|
+
//@end
|
|
52
|
+
entrypoint: buildEntrypoints({
|
|
53
|
+
controllers: [ExampleController],
|
|
54
|
+
//@if i18n
|
|
55
|
+
//The messages in the language of its environment (LANG)
|
|
56
|
+
middlewares: [I18nMiddleware],
|
|
57
|
+
//@end
|
|
58
|
+
//The renderer invokes the actions over IPC: window.ipc.invoke({cmd: 'greet', name: 'you'})
|
|
59
|
+
service: [
|
|
60
|
+
electronIPCServiceEntrypoint({ipcMain: options.ipcMain}),
|
|
61
|
+
...options.devPort === undefined ? [] : [httpServiceEntrypoint({port: options.devPort})]
|
|
62
|
+
]
|
|
63
|
+
}),
|
|
64
|
+
...(options.window === false ? {} : {
|
|
65
|
+
window: {
|
|
66
|
+
class: MainWindow,
|
|
67
|
+
directory: options.directory,
|
|
68
|
+
renderer: options.renderer
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
//The actions are served before the window loads the renderer
|
|
73
|
+
bootstrap: options.window === false ? ['entrypoint'] : ['entrypoint', 'window']
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {Controller, Entrypoint, type ServiceContext} from 'lakutata/com/entrypoint'
|
|
2
|
+
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
3
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
4
|
+
import {GreetDTO} from '../dtos/GreetDTO.js'
|
|
5
|
+
//@if i18n
|
|
6
|
+
import {Translator} from 'lakutata/com/i18n'
|
|
7
|
+
//@end
|
|
8
|
+
|
|
9
|
+
export class ExampleController extends Controller {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pushes the events to the clients: the windows (window.ipc.on), the tools in development (GET /events)
|
|
13
|
+
*/
|
|
14
|
+
@Inject('entrypoint')
|
|
15
|
+
protected readonly entrypoint: Entrypoint
|
|
16
|
+
//@if i18n
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The messages of the language of the request
|
|
20
|
+
*/
|
|
21
|
+
@Inject(Translator)
|
|
22
|
+
protected readonly i18n: Translator
|
|
23
|
+
//@end
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Invoked by the renderer: window.ipc.invoke({cmd: 'greet', name: 'you'})
|
|
27
|
+
* @param input
|
|
28
|
+
*/
|
|
29
|
+
@ServiceAction({cmd: 'greet'}, GreetDTO)
|
|
30
|
+
public async greet(input: GreetDTO): Promise<{ message: string }> {
|
|
31
|
+
//An event pushed to the client of the request (window.ipc.on('greeted', listener)), whose id may be kept to push
|
|
32
|
+
//it events later; this.entrypoint.broadcast pushes one to every client
|
|
33
|
+
const clientId: string | undefined = (this.context as ServiceContext).clientId
|
|
34
|
+
if (clientId) this.entrypoint.notify(clientId, 'greeted', input.name)
|
|
35
|
+
//@if i18n
|
|
36
|
+
return {message: this.i18n.t('greeting.hello', {name: input.name})}
|
|
37
|
+
//@else
|
|
38
|
+
return {message: `Hello, ${input.name}!`}
|
|
39
|
+
//@end
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {contextBridge, ipcRenderer} from 'electron'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The preload script of the window: the transport of window.ipc (packages/ui/src/ipc.ts), the renderer invokes the actions
|
|
5
|
+
* of the main process and receives the events it pushes, nothing else of Electron or of Node.js
|
|
6
|
+
*/
|
|
7
|
+
contextBridge.exposeInMainWorld('ipcBridge', {
|
|
8
|
+
//The page is a client of the main process: its id is the clientId of the contexts of its requests
|
|
9
|
+
invoke: (data: Record<string, unknown>, clientId: string): Promise<unknown> => ipcRenderer.invoke('lakutata:service', data, clientId),
|
|
10
|
+
//The events of the main process (Entrypoint.notify, broadcast), once the page is subscribed to them
|
|
11
|
+
subscribe: (clientId: string, listener: (event: string, args: unknown[]) => void): void => {
|
|
12
|
+
ipcRenderer.on('lakutata:service:event', (_event: unknown, name: string, args: unknown[]): void => listener(name, args))
|
|
13
|
+
ipcRenderer.send('lakutata:service:subscribe', clientId)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {after, before, describe, it} from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import {createServer, type Server} from 'node:net'
|
|
4
|
+
import {Application} from 'lakutata'
|
|
5
|
+
import type {IPCMain} from 'lakutata/adapter/electron'
|
|
6
|
+
import {installerConfig} from '../config/Config.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The ipcMain of the tests: the actions are invoked as the renderer invokes them, without Electron
|
|
10
|
+
*/
|
|
11
|
+
const handlers: Map<string, (event: unknown, data: unknown) => Promise<unknown>> = new Map()
|
|
12
|
+
const listeners: Map<string, (event: unknown, clientId: string) => void> = new Map()
|
|
13
|
+
const ipcMain: IPCMain = {
|
|
14
|
+
handle: (channel: string, listener: (event: unknown, data: unknown) => Promise<unknown>): unknown => handlers.set(channel, listener),
|
|
15
|
+
removeHandler: (channel: string): unknown => handlers.delete(channel),
|
|
16
|
+
on: (channel: string, listener: (event: unknown, clientId: string) => void): unknown => listeners.set(channel, listener),
|
|
17
|
+
removeAllListeners: (channel: string): unknown => listeners.delete(channel)
|
|
18
|
+
}
|
|
19
|
+
//The web contents of the renderer: the events pushed to it
|
|
20
|
+
const received: unknown[][] = []
|
|
21
|
+
const sender = {once: (): void => undefined, off: (): void => undefined, send: (...args: unknown[]): unknown => received.push(args)}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Invoke an action as the renderer does (window.ipc.invoke)
|
|
25
|
+
* @param data
|
|
26
|
+
*/
|
|
27
|
+
function invoke(data: Record<string, unknown>): Promise<any> {
|
|
28
|
+
return (handlers.get('lakutata:service') as (event: unknown, data: unknown, clientId: string) => Promise<unknown>)({sender: sender}, data, 'page')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A free port, the one of the HTTP entrypoint of the development
|
|
33
|
+
*/
|
|
34
|
+
async function freePort(): Promise<number> {
|
|
35
|
+
const server: Server = createServer()
|
|
36
|
+
await new Promise<void>((resolve: () => void): unknown => server.listen(0, '127.0.0.1', resolve))
|
|
37
|
+
const port: number = (server.address() as { port: number }).port
|
|
38
|
+
await new Promise<void>((resolve: () => void): unknown => server.close((): void => resolve()))
|
|
39
|
+
return port
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('{$APP_NAME}', (): void => {
|
|
43
|
+
|
|
44
|
+
let app: Application
|
|
45
|
+
let devPort: number
|
|
46
|
+
|
|
47
|
+
before(async (): Promise<void> => {
|
|
48
|
+
//@if i18n
|
|
49
|
+
//The actions answer in the language of the environment: English for the tests
|
|
50
|
+
process.env.LC_ALL = 'en_US.UTF-8'
|
|
51
|
+
//@end
|
|
52
|
+
devPort = await freePort()
|
|
53
|
+
app = await new Promise<Application>((resolve: (app: Application) => void): void => {
|
|
54
|
+
Application.run(installerConfig({window: false, ipcMain: ipcMain, devPort: devPort})).onLaunched((launched: Application): void => resolve(launched))
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
after(async (): Promise<void> => {
|
|
59
|
+
await app?.dispose()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('serves the actions to the renderer', async (): Promise<void> => {
|
|
63
|
+
assert.deepStrictEqual(await invoke({cmd: 'greet', name: 'you'}), {success: true, error: null, payload: {message: 'Hello, you!'}})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
//@if i18n
|
|
67
|
+
it('serves the actions in the language of the environment', async (): Promise<void> => {
|
|
68
|
+
process.env.LC_ALL = 'zh_CN.UTF-8'
|
|
69
|
+
try {
|
|
70
|
+
assert.deepStrictEqual(await invoke({cmd: 'greet', name: 'you'}), {success: true, error: null, payload: {message: '你好,you!'}})
|
|
71
|
+
} finally {
|
|
72
|
+
process.env.LC_ALL = 'en_US.UTF-8'
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
//@end
|
|
77
|
+
it('pushes its events to the renderer', async (): Promise<void> => {
|
|
78
|
+
//The page subscribed to the events
|
|
79
|
+
listeners.get('lakutata:service:subscribe')!({sender: sender}, 'page')
|
|
80
|
+
received.length = 0
|
|
81
|
+
await invoke({cmd: 'greet', name: 'you'})
|
|
82
|
+
assert.deepStrictEqual(received, [['lakutata:service:event', 'greeted', ['you']]])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('serves the actions and their events to the tools of the development over HTTP', async (): Promise<void> => {
|
|
86
|
+
//The events of the client (Server-Sent Events): its id first
|
|
87
|
+
const events: Response = await fetch(`http://127.0.0.1:${devPort}/events?id=tool`)
|
|
88
|
+
const reader = events.body!.getReader()
|
|
89
|
+
let received: string = ''
|
|
90
|
+
const until = async (pattern: RegExp): Promise<void> => {
|
|
91
|
+
while (!pattern.test(received)) received += new TextDecoder().decode((await reader.read()).value)
|
|
92
|
+
}
|
|
93
|
+
await until(/event: client/)
|
|
94
|
+
const response: Response = await fetch(`http://127.0.0.1:${devPort}/`, {method: 'POST', headers: {'Content-Type': 'application/json', 'id': 'tool'}, body: JSON.stringify({cmd: 'greet', name: 'you'})})
|
|
95
|
+
assert.deepStrictEqual(await response.json(), {success: true, error: null, payload: {message: 'Hello, you!'}})
|
|
96
|
+
await until(/event: greeted\ndata: \["you"\]/)
|
|
97
|
+
await reader.cancel()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('refuses an invalid input', async (): Promise<void> => {
|
|
101
|
+
const response = await invoke({cmd: 'greet'})
|
|
102
|
+
assert.strictEqual(response.success, false)
|
|
103
|
+
assert.match(response.error.message, /name/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('refuses an unknown action', async (): Promise<void> => {
|
|
107
|
+
assert.strictEqual((await invoke({cmd: 'unknown'})).success, false)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" />
|
|
7
|
+
<title>{$APP_NAME}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"clean": "lakutata clean dist",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"rebuild": "npm run clean && npm run build",
|
|
10
|
+
"dev": "vite --port 5173 --strictPort",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"react": "^19.3.0",
|
|
15
|
+
"react-dom": "^19.3.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.3.0",
|
|
19
|
+
"@types/react-dom": "^19.3.0",
|
|
20
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
21
|
+
"vite": "^8.3.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
export function App() {
|
|
4
|
+
const [message, setMessage] = useState<string>('…')
|
|
5
|
+
const [pushed, setPushed] = useState<string>('')
|
|
6
|
+
|
|
7
|
+
useEffect((): () => void => {
|
|
8
|
+
//An event the main process pushes
|
|
9
|
+
const greeted = (name: string): void => setPushed(`Pushed by the main process: ${name}`)
|
|
10
|
+
window.ipc.on('greeted', greeted)
|
|
11
|
+
window.ipc.invoke<{ message: string }>({cmd: 'greet', name: '{$APP_NAME}'})
|
|
12
|
+
.then((result: { message: string }): void => setMessage(result.message))
|
|
13
|
+
.catch((error: Error): void => setMessage(error.message))
|
|
14
|
+
return (): void => {
|
|
15
|
+
window.ipc.off('greeted', greeted)
|
|
16
|
+
}
|
|
17
|
+
}, [])
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<main>
|
|
21
|
+
<h1>{$APP_NAME}</h1>
|
|
22
|
+
<p>{message}</p>
|
|
23
|
+
<p>{pushed}</p>
|
|
24
|
+
</main>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response of an action of the main process
|
|
3
|
+
*/
|
|
4
|
+
type ServiceResponse<T> = {
|
|
5
|
+
success: boolean
|
|
6
|
+
error: { message?: string, errno?: string } | null
|
|
7
|
+
payload: T
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A listener of an event of the main process, given its arguments
|
|
12
|
+
*/
|
|
13
|
+
type Listener = (...args: any[]) => void
|
|
14
|
+
|
|
15
|
+
declare global {
|
|
16
|
+
interface Window {
|
|
17
|
+
/**
|
|
18
|
+
* The transport of the IPC, given by the preload script of the window of the application
|
|
19
|
+
*/
|
|
20
|
+
ipcBridge?: {
|
|
21
|
+
invoke<T>(data: Record<string, unknown>, clientId: string): Promise<ServiceResponse<T>>
|
|
22
|
+
subscribe(clientId: string, listener: (event: string, args: unknown[]) => void): void
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The IPC of the main process
|
|
26
|
+
*/
|
|
27
|
+
ipc: IPC
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The IPC of the main process (window.ipc): its actions invoked, their results given and their errors thrown (with their
|
|
33
|
+
* errno), its events listened to. Defined in the page: the errors and the listeners given across the preload script lose
|
|
34
|
+
* their properties and their identity
|
|
35
|
+
*/
|
|
36
|
+
export class IPC {
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The id of the page, a client of the main process (the clientId of the contexts of its requests, which the events
|
|
40
|
+
* are pushed to): a new one each time the page is loaded
|
|
41
|
+
*/
|
|
42
|
+
public readonly id: string = crypto.randomUUID()
|
|
43
|
+
|
|
44
|
+
readonly #listeners: Map<string, Listener[]> = new Map()
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
window.ipcBridge?.subscribe(this.id, (event: string, args: unknown[]): void => {
|
|
48
|
+
for (const listener of [...this.#listeners.get(event) ?? []]) listener(...args)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Invoke an action of the main process (a @ServiceAction of its controllers): window.ipc.invoke({cmd: 'greet', name: 'you'})
|
|
54
|
+
* @param data its pattern and its input
|
|
55
|
+
*/
|
|
56
|
+
public async invoke<T = any>(data: Record<string, unknown>): Promise<T> {
|
|
57
|
+
if (!window.ipcBridge) throw new Error('The actions are invoked in the window of the application (npm run dev)')
|
|
58
|
+
const response: ServiceResponse<T> = await window.ipcBridge.invoke<T>(data, this.id)
|
|
59
|
+
if (!response.success) throw Object.assign(new Error(response.error?.message ?? 'The action failed'), response.error)
|
|
60
|
+
return response.payload
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Listen to an event of the main process
|
|
65
|
+
* @param event
|
|
66
|
+
* @param listener
|
|
67
|
+
*/
|
|
68
|
+
public on(event: string, listener: Listener): this {
|
|
69
|
+
this.#listeners.set(event, [...this.#listeners.get(event) ?? [], listener])
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Listen to the next event of the main process
|
|
75
|
+
* @param event
|
|
76
|
+
* @param listener
|
|
77
|
+
*/
|
|
78
|
+
public once(event: string, listener: Listener): this {
|
|
79
|
+
const once: Listener & { listener?: Listener } = (...args: any[]): void => {
|
|
80
|
+
this.off(event, once)
|
|
81
|
+
listener(...args)
|
|
82
|
+
}
|
|
83
|
+
once.listener = listener
|
|
84
|
+
return this.on(event, once)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Stop listening to an event of the main process
|
|
89
|
+
* @param event
|
|
90
|
+
* @param listener
|
|
91
|
+
*/
|
|
92
|
+
public off(event: string, listener: Listener): this {
|
|
93
|
+
this.#listeners.set(event, (this.#listeners.get(event) ?? []).filter((existing: Listener & { listener?: Listener }): boolean => existing !== listener && existing.listener !== listener))
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stop listening to an event of the main process (off)
|
|
99
|
+
* @param event
|
|
100
|
+
* @param listener
|
|
101
|
+
*/
|
|
102
|
+
public removeListener(event: string, listener: Listener): this {
|
|
103
|
+
return this.off(event, listener)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stop listening to an event of the main process, or to every event
|
|
108
|
+
* @param event
|
|
109
|
+
*/
|
|
110
|
+
public removeAllListeners(event?: string): this {
|
|
111
|
+
if (event === undefined) this.#listeners.clear()
|
|
112
|
+
else this.#listeners.delete(event)
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Kept for the renderers of the version 2 (the listeners of an event are not limited)
|
|
118
|
+
* @param _count
|
|
119
|
+
*/
|
|
120
|
+
public setMaxListeners(_count: number): this {
|
|
121
|
+
return this
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
window.ipc = new IPC()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//window.ipc, before the components using it
|
|
2
|
+
import './ipc'
|
|
3
|
+
import {StrictMode} from 'react'
|
|
4
|
+
import {createRoot} from 'react-dom/client'
|
|
5
|
+
import {App} from './App'
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<App/>
|
|
10
|
+
</StrictMode>
|
|
11
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"types": ["vite/client"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src", "vite.config.ts"]
|
|
15
|
+
}
|