switch-framework-electron 0.2.9
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 +21 -0
- package/README.md +64 -0
- package/child.js +35 -0
- package/index.js +25 -0
- package/lib/bootstrap.js +186 -0
- package/lib/constants.js +8 -0
- package/lib/ipc.js +90 -0
- package/lib/messages.js +5 -0
- package/lib/paths.js +30 -0
- package/lib/servers.js +92 -0
- package/lib/session.js +11 -0
- package/package.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Switcherfaiz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# switch-framework-electron
|
|
2
|
+
|
|
3
|
+
Electron helpers for [Switch Framework](https://github.com/Switcherfaiz/switch-framework) desktop apps — child server forking, dynamic ports, IPC, splash bootstrap, and window controls.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install switch-framework-electron switch-framework switch-framework-backend
|
|
9
|
+
npm install electron --save-dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Scaffold a full app with the CLI:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx create-switch-framework-app my-app --app-type electron
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
**`electron/servers.js`** — declare child processes:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
module.exports = [
|
|
24
|
+
{ name: 'app', entry: 'server.js' },
|
|
25
|
+
{ name: 'worker', entry: 'worker-server.js' },
|
|
26
|
+
];
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**`electron/main.js`** — bootstrap:
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const { bootstrapElectronApp } = require('switch-framework-electron');
|
|
34
|
+
const servers = require('./servers.js');
|
|
35
|
+
|
|
36
|
+
bootstrapElectronApp({
|
|
37
|
+
servers,
|
|
38
|
+
preloadPath: path.join(__dirname, 'preload.js'),
|
|
39
|
+
splashHtmlPath: path.join(__dirname, 'splash.html'),
|
|
40
|
+
appRoot: path.join(__dirname, '..'),
|
|
41
|
+
cwd: path.join(__dirname, '..'),
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each **`entry`** file is a normal Node script that starts an HTTP server (via `switch-framework-backend` or raw `http.createServer`). The package assigns a free localhost port and reports it over IPC.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
| Export | Description |
|
|
50
|
+
|--------|-------------|
|
|
51
|
+
| `bootstrapElectronApp(options)` | Splash → fork servers → open main window |
|
|
52
|
+
| `startServers(list, session, opts)` | Fork all entries from `servers.js` |
|
|
53
|
+
| `onServerReady(cb)` / `whenServerReady()` | Called when every server reported a port |
|
|
54
|
+
| `registerWindowIpc(win)` | Minimize / maximize / close for frameless windows |
|
|
55
|
+
| `createLocalSession()` | Random token for local auth cookie |
|
|
56
|
+
| `childEntry` | Internal fork script path |
|
|
57
|
+
|
|
58
|
+
## Docs
|
|
59
|
+
|
|
60
|
+
[Desktop Server](https://github.com/Switcherfaiz/switch-framework-docs) · [Multiple child servers](https://github.com/Switcherfaiz/switch-framework-docs) · [Package reference](https://github.com/Switcherfaiz/switch-framework-docs)
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT © Faiz Ahmad Ally (Switcherfaiz)
|
package/child.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { SERVER_READY_MESSAGE } = require('./lib/messages.js');
|
|
6
|
+
const {
|
|
7
|
+
SERVER_CHILD_FLAG,
|
|
8
|
+
APP_ROOT_ENV,
|
|
9
|
+
} = require('./lib/constants.js');
|
|
10
|
+
|
|
11
|
+
if (process.env[SERVER_CHILD_FLAG] !== '1') {
|
|
12
|
+
throw new Error('switch-framework-electron/child.js must run as a forked server child');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const origListen = http.Server.prototype.listen;
|
|
16
|
+
http.Server.prototype.listen = function patchedListen() {
|
|
17
|
+
const port = process.env.PORT === undefined ? 0 : Number(process.env.PORT);
|
|
18
|
+
const host = process.env.SWITCH_BIND_HOST || '127.0.0.1';
|
|
19
|
+
this.once('listening', () => {
|
|
20
|
+
const addr = this.address();
|
|
21
|
+
const info = {
|
|
22
|
+
type: SERVER_READY_MESSAGE,
|
|
23
|
+
name: process.env.SWITCH_SERVER_NAME || 'app',
|
|
24
|
+
port: typeof addr === 'object' && addr ? addr.port : port,
|
|
25
|
+
host: typeof addr === 'object' && addr ? addr.address : host,
|
|
26
|
+
};
|
|
27
|
+
console.log(`[switch-framework-electron:${info.name}] http://${info.host}:${info.port}`);
|
|
28
|
+
if (typeof process.send === 'function') process.send(info);
|
|
29
|
+
});
|
|
30
|
+
return origListen.call(this, Number.isFinite(port) ? port : 0, host);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const appRoot = process.env[APP_ROOT_ENV];
|
|
34
|
+
const entry = process.env.SWITCH_SERVER_ENTRY || 'server.js';
|
|
35
|
+
require(path.join(appRoot, entry));
|
package/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pkg = require('./package.json');
|
|
4
|
+
|
|
5
|
+
const ipc = require('./lib/ipc.js');
|
|
6
|
+
const session = require('./lib/session.js');
|
|
7
|
+
const servers = require('./lib/servers.js');
|
|
8
|
+
const bootstrap = require('./lib/bootstrap.js');
|
|
9
|
+
const paths = require('./lib/paths.js');
|
|
10
|
+
const messages = require('./lib/messages.js');
|
|
11
|
+
const constants = require('./lib/constants.js');
|
|
12
|
+
|
|
13
|
+
const VERSION = pkg.version;
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
VERSION,
|
|
17
|
+
...ipc,
|
|
18
|
+
...session,
|
|
19
|
+
...servers,
|
|
20
|
+
...bootstrap,
|
|
21
|
+
...paths,
|
|
22
|
+
...messages,
|
|
23
|
+
...constants,
|
|
24
|
+
childEntry: require('node:path').join(__dirname, 'child.js'),
|
|
25
|
+
};
|
package/lib/bootstrap.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { app, BrowserWindow } = require('electron');
|
|
5
|
+
const { createLocalSession } = require('./session.js');
|
|
6
|
+
const { onServerReady, registerWindowIpc } = require('./ipc.js');
|
|
7
|
+
const { startServers, stopServerProcess } = require('./servers.js');
|
|
8
|
+
|
|
9
|
+
async function attachLocalAuthCookie(win, host, port, token) {
|
|
10
|
+
if (!token || !win) return;
|
|
11
|
+
const url = `http://${host}:${port}/`;
|
|
12
|
+
try {
|
|
13
|
+
await win.webContents.session.cookies.set({
|
|
14
|
+
url,
|
|
15
|
+
name: 'switch-local-auth',
|
|
16
|
+
value: token,
|
|
17
|
+
httpOnly: true,
|
|
18
|
+
path: '/',
|
|
19
|
+
});
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('[switch-framework-electron] Failed to set local auth cookie:', err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createSplashWindow(splashHtmlPath, bounds = { width: 1200, height: 800 }) {
|
|
26
|
+
const splash = new BrowserWindow({
|
|
27
|
+
...bounds,
|
|
28
|
+
center: true,
|
|
29
|
+
frame: false,
|
|
30
|
+
backgroundColor: '#f5f5f5',
|
|
31
|
+
show: false,
|
|
32
|
+
webPreferences: {
|
|
33
|
+
nodeIntegration: false,
|
|
34
|
+
contextIsolation: true,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
splash.loadFile(splashHtmlPath);
|
|
39
|
+
splash.once('ready-to-show', () => splash.show());
|
|
40
|
+
return splash;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createMainWindow({
|
|
44
|
+
bounds,
|
|
45
|
+
host,
|
|
46
|
+
port,
|
|
47
|
+
token,
|
|
48
|
+
preloadPath,
|
|
49
|
+
backgroundColor = '#f5f5f5',
|
|
50
|
+
frame = false,
|
|
51
|
+
webPreferences = {},
|
|
52
|
+
onMainWindowCreated,
|
|
53
|
+
}) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
process.env.SWITCH_WINDOW_HOST = host;
|
|
56
|
+
process.env.SWITCH_WINDOW_PORT = String(port);
|
|
57
|
+
|
|
58
|
+
const mainWindow = new BrowserWindow({
|
|
59
|
+
...bounds,
|
|
60
|
+
frame,
|
|
61
|
+
backgroundColor,
|
|
62
|
+
show: false,
|
|
63
|
+
webPreferences: {
|
|
64
|
+
preload: preloadPath,
|
|
65
|
+
nodeIntegration: false,
|
|
66
|
+
contextIsolation: true,
|
|
67
|
+
...webPreferences,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (typeof onMainWindowCreated === 'function') {
|
|
72
|
+
onMainWindowCreated(mainWindow);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
registerWindowIpc(mainWindow);
|
|
76
|
+
|
|
77
|
+
mainWindow.on('closed', () => {
|
|
78
|
+
mainWindow._swClosed = true;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const appUrl = `http://${host}:${port}/`;
|
|
82
|
+
|
|
83
|
+
mainWindow.webContents.on('did-fail-load', (_event, _code, description) => {
|
|
84
|
+
console.error('[switch-framework-electron] Page failed to load:', description);
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
if (!mainWindow.isDestroyed()) mainWindow.loadURL(appUrl);
|
|
87
|
+
}, 750);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
mainWindow.once('ready-to-show', () => resolve(mainWindow));
|
|
91
|
+
|
|
92
|
+
attachLocalAuthCookie(mainWindow, host, port, token).then(() => {
|
|
93
|
+
mainWindow.loadURL(appUrl);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bootstrapElectronApp(options = {}) {
|
|
99
|
+
const {
|
|
100
|
+
servers = [{ name: 'app', entry: 'server.js' }],
|
|
101
|
+
preloadPath,
|
|
102
|
+
splashHtmlPath,
|
|
103
|
+
appRoot,
|
|
104
|
+
cwd,
|
|
105
|
+
windowBounds = { width: 1200, height: 800 },
|
|
106
|
+
webPreferences = {},
|
|
107
|
+
onMainWindowCreated,
|
|
108
|
+
onReady,
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
if (!preloadPath) throw new Error('bootstrapElectronApp requires preloadPath');
|
|
112
|
+
if (!splashHtmlPath) throw new Error('bootstrapElectronApp requires splashHtmlPath');
|
|
113
|
+
|
|
114
|
+
let splashWindow = null;
|
|
115
|
+
let mainWindow = null;
|
|
116
|
+
let localSession = null;
|
|
117
|
+
let bootstrapping = false;
|
|
118
|
+
|
|
119
|
+
async function openMainWindow(ports) {
|
|
120
|
+
if (!splashWindow || splashWindow.isDestroyed()) return;
|
|
121
|
+
|
|
122
|
+
const appServer = ports.app || Object.values(ports)[0];
|
|
123
|
+
if (!appServer?.port) throw new Error('App server did not report a port');
|
|
124
|
+
|
|
125
|
+
const host = appServer.host || '127.0.0.1';
|
|
126
|
+
const bounds = splashWindow.getBounds();
|
|
127
|
+
|
|
128
|
+
mainWindow = await createMainWindow({
|
|
129
|
+
bounds,
|
|
130
|
+
host,
|
|
131
|
+
port: appServer.port,
|
|
132
|
+
token: localSession?.token,
|
|
133
|
+
preloadPath,
|
|
134
|
+
webPreferences,
|
|
135
|
+
onMainWindowCreated,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (typeof onReady === 'function') {
|
|
139
|
+
await onReady({ ports, mainWindow, splashWindow });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (splashWindow && !splashWindow.isDestroyed()) splashWindow.close();
|
|
143
|
+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.show();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function bootstrap() {
|
|
147
|
+
if (bootstrapping) return;
|
|
148
|
+
bootstrapping = true;
|
|
149
|
+
|
|
150
|
+
localSession = createLocalSession();
|
|
151
|
+
splashWindow = createSplashWindow(splashHtmlPath, windowBounds);
|
|
152
|
+
startServers(servers, localSession, { appRoot, cwd });
|
|
153
|
+
|
|
154
|
+
onServerReady((ports) => {
|
|
155
|
+
openMainWindow(ports)
|
|
156
|
+
.catch((err) => {
|
|
157
|
+
console.error('[switch-framework-electron] Failed to open main window:', err);
|
|
158
|
+
if (splashWindow && !splashWindow.isDestroyed()) splashWindow.close();
|
|
159
|
+
if (!mainWindow) app.quit();
|
|
160
|
+
})
|
|
161
|
+
.finally(() => {
|
|
162
|
+
bootstrapping = false;
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
app.on('before-quit', () => stopServerProcess());
|
|
168
|
+
|
|
169
|
+
app.whenReady().then(() => {
|
|
170
|
+
bootstrap();
|
|
171
|
+
app.on('activate', () => {
|
|
172
|
+
if (BrowserWindow.getAllWindows().length === 0) bootstrap();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
app.on('window-all-closed', () => {
|
|
177
|
+
if (process.platform !== 'darwin') app.quit();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
bootstrapElectronApp,
|
|
183
|
+
createSplashWindow,
|
|
184
|
+
createMainWindow,
|
|
185
|
+
attachLocalAuthCookie,
|
|
186
|
+
};
|
package/lib/constants.js
ADDED
package/lib/ipc.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ipcMain } = require('electron');
|
|
4
|
+
const { SERVER_READY_MESSAGE } = require('./messages.js');
|
|
5
|
+
|
|
6
|
+
const serverPorts = {};
|
|
7
|
+
const pendingServers = new Set();
|
|
8
|
+
const readyListeners = [];
|
|
9
|
+
let allReady = false;
|
|
10
|
+
|
|
11
|
+
function expectServers(names = []) {
|
|
12
|
+
pendingServers.clear();
|
|
13
|
+
names.forEach((name) => pendingServers.add(name));
|
|
14
|
+
allReady = pendingServers.size === 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function notifyAllReady() {
|
|
18
|
+
allReady = true;
|
|
19
|
+
readyListeners.splice(0).forEach((listener) => listener({ ...serverPorts }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function onServerInfo(info) {
|
|
23
|
+
if (!info || info.type !== SERVER_READY_MESSAGE) return;
|
|
24
|
+
if (info.name) {
|
|
25
|
+
serverPorts[info.name] = { port: info.port, host: info.host };
|
|
26
|
+
pendingServers.delete(info.name);
|
|
27
|
+
}
|
|
28
|
+
if (pendingServers.size === 0) notifyAllReady();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function onServerReady(listener) {
|
|
32
|
+
if (allReady) {
|
|
33
|
+
listener({ ...serverPorts });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
readyListeners.push(listener);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function whenServerReady() {
|
|
40
|
+
return new Promise((resolve) => onServerReady(resolve));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetServerReady() {
|
|
44
|
+
allReady = false;
|
|
45
|
+
Object.keys(serverPorts).forEach((key) => delete serverPorts[key]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function registerServerIpc(serverProcess) {
|
|
49
|
+
serverProcess.on('message', onServerInfo);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerWindowIpc(mainWindow) {
|
|
53
|
+
try { ipcMain.removeHandler('window:is-maximized'); } catch (_) {}
|
|
54
|
+
ipcMain.removeAllListeners('window:minimize');
|
|
55
|
+
ipcMain.removeAllListeners('window:maximize');
|
|
56
|
+
ipcMain.removeAllListeners('window:close');
|
|
57
|
+
|
|
58
|
+
ipcMain.on('window:minimize', () => {
|
|
59
|
+
mainWindow?.minimize();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
ipcMain.on('window:maximize', () => {
|
|
63
|
+
if (!mainWindow) return;
|
|
64
|
+
if (mainWindow.isMaximized()) mainWindow.unmaximize();
|
|
65
|
+
else mainWindow.maximize();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
ipcMain.on('window:close', () => {
|
|
69
|
+
mainWindow?.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
ipcMain.handle('window:is-maximized', () => {
|
|
73
|
+
return mainWindow?.isMaximized?.() ?? false;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getServerPorts() {
|
|
78
|
+
return { ...serverPorts };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
SERVER_READY_MESSAGE,
|
|
83
|
+
expectServers,
|
|
84
|
+
registerServerIpc,
|
|
85
|
+
registerWindowIpc,
|
|
86
|
+
onServerReady,
|
|
87
|
+
whenServerReady,
|
|
88
|
+
resetServerReady,
|
|
89
|
+
getServerPorts,
|
|
90
|
+
};
|
package/lib/messages.js
ADDED
package/lib/paths.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
function getAppRoot(appRootOverride) {
|
|
7
|
+
if (appRootOverride) return appRootOverride;
|
|
8
|
+
const { app } = require('electron');
|
|
9
|
+
return app.isPackaged ? app.getAppPath() : process.cwd();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function configurePackagedPaths() {
|
|
13
|
+
const { app } = require('electron');
|
|
14
|
+
if (!app.isPackaged) return;
|
|
15
|
+
|
|
16
|
+
const esbuildBinary = path.join(
|
|
17
|
+
process.resourcesPath,
|
|
18
|
+
'app.asar.unpacked',
|
|
19
|
+
'node_modules',
|
|
20
|
+
'@esbuild',
|
|
21
|
+
`${process.platform}-${process.arch}`,
|
|
22
|
+
process.platform === 'win32' ? 'esbuild.exe' : 'bin/esbuild',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(esbuildBinary)) {
|
|
26
|
+
process.env.ESBUILD_BINARY_PATH = esbuildBinary;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { getAppRoot, configurePackagedPaths };
|
package/lib/servers.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { fork } = require('node:child_process');
|
|
5
|
+
const {
|
|
6
|
+
SERVER_CHILD_FLAG,
|
|
7
|
+
APP_ROOT_ENV,
|
|
8
|
+
USER_DATA_ENV,
|
|
9
|
+
IS_PACKAGED_ENV,
|
|
10
|
+
} = require('./constants.js');
|
|
11
|
+
const { registerServerIpc, expectServers } = require('./ipc.js');
|
|
12
|
+
const { configurePackagedPaths, getAppRoot } = require('./paths.js');
|
|
13
|
+
|
|
14
|
+
const processes = new Map();
|
|
15
|
+
|
|
16
|
+
function getChildScriptPath() {
|
|
17
|
+
return path.join(__dirname, '..', 'child.js');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stopServerProcess(name) {
|
|
21
|
+
if (name) {
|
|
22
|
+
const child = processes.get(name);
|
|
23
|
+
if (child && !child.killed) child.kill();
|
|
24
|
+
processes.delete(name);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, child] of processes) {
|
|
28
|
+
if (child && !child.killed) child.kill();
|
|
29
|
+
processes.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function startServerProcess(spec = {}, session = {}, options = {}) {
|
|
34
|
+
const name = spec.name || 'app';
|
|
35
|
+
if (processes.has(name)) return processes.get(name);
|
|
36
|
+
|
|
37
|
+
const { app } = require('electron');
|
|
38
|
+
configurePackagedPaths();
|
|
39
|
+
|
|
40
|
+
const childPath = getChildScriptPath();
|
|
41
|
+
const forkCwd = app.isPackaged ? process.resourcesPath : (options.cwd || process.cwd());
|
|
42
|
+
const appRoot = getAppRoot(options.appRoot);
|
|
43
|
+
const userDataPath = app.getPath('userData');
|
|
44
|
+
|
|
45
|
+
const child = fork(childPath, [], {
|
|
46
|
+
cwd: forkCwd,
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
50
|
+
[SERVER_CHILD_FLAG]: '1',
|
|
51
|
+
[APP_ROOT_ENV]: appRoot,
|
|
52
|
+
[USER_DATA_ENV]: userDataPath,
|
|
53
|
+
[IS_PACKAGED_ENV]: app.isPackaged ? '1' : '0',
|
|
54
|
+
SWITCH_SERVER_NAME: name,
|
|
55
|
+
SWITCH_SERVER_ENTRY: spec.entry || 'server.js',
|
|
56
|
+
PORT: '0',
|
|
57
|
+
SWITCH_BIND_HOST: '127.0.0.1',
|
|
58
|
+
SWITCH_LOCAL_AUTH_TOKEN: session.token || '',
|
|
59
|
+
...(spec.env || {}),
|
|
60
|
+
},
|
|
61
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
registerServerIpc(child);
|
|
65
|
+
|
|
66
|
+
child.on('error', (err) => {
|
|
67
|
+
console.error(`[switch-framework-electron:${name}] Process error:`, err);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
child.on('exit', (code, signal) => {
|
|
71
|
+
if (code !== 0 && code !== null) {
|
|
72
|
+
console.error(`[switch-framework-electron:${name}] Process exited:`, { code, signal });
|
|
73
|
+
}
|
|
74
|
+
processes.delete(name);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
processes.set(name, child);
|
|
78
|
+
return child;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function startServers(serverList = [], session = {}, options = {}) {
|
|
82
|
+
const list = Array.isArray(serverList) ? serverList : [serverList];
|
|
83
|
+
expectServers(list.map((item) => item.name || 'app'));
|
|
84
|
+
return list.map((item) => startServerProcess(item, session, options));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
startServerProcess,
|
|
89
|
+
startServers,
|
|
90
|
+
stopServerProcess,
|
|
91
|
+
getChildScriptPath,
|
|
92
|
+
};
|
package/lib/session.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "switch-framework-electron",
|
|
3
|
+
"version": "0.2.9",
|
|
4
|
+
"description": "Electron desktop helpers for the Switch Framework ecosystem — child servers, dynamic ports, IPC, splash bootstrap, and window controls for building Electron apps with switch-framework and switch-framework-backend",
|
|
5
|
+
"author": "Switcherfaiz",
|
|
6
|
+
"github": "https://github.com/Switcherfaiz/switch-framework-electron",
|
|
7
|
+
"main": "./index.js",
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"child.js",
|
|
12
|
+
"lib/**/*.js"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"electron": ">=28"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"switch-framework",
|
|
23
|
+
"electron",
|
|
24
|
+
"desktop"
|
|
25
|
+
]
|
|
26
|
+
}
|