@atom-js-org/runtime 0.2.0-alpha.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 +31 -0
- package/README.md +17 -0
- package/index.d.ts +166 -0
- package/package.json +41 -0
- package/src/app.cjs +111 -0
- package/src/bridge-script.cjs +274 -0
- package/src/bridge-server.cjs +283 -0
- package/src/browser-window.cjs +411 -0
- package/src/clipboard.cjs +46 -0
- package/src/dialog.cjs +141 -0
- package/src/electron-apis.cjs +491 -0
- package/src/index.cjs +31 -0
- package/src/ipc-main.cjs +50 -0
- package/src/menu.cjs +48 -0
- package/src/runtime/macos-window-host.jxa.js +147 -0
- package/src/runtime/window-host.mjs +94 -0
- package/src/shell.cjs +42 -0
- package/src/state.cjs +11 -0
- package/src/web-contents.cjs +59 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const { URL } = require('node:url');
|
|
8
|
+
const { WebSocketServer, WebSocket } = require('ws');
|
|
9
|
+
const state = require('./state.cjs');
|
|
10
|
+
const ipcMain = require('./ipc-main.cjs');
|
|
11
|
+
|
|
12
|
+
const MIME_TYPES = new Map([
|
|
13
|
+
['.html', 'text/html; charset=utf-8'],
|
|
14
|
+
['.htm', 'text/html; charset=utf-8'],
|
|
15
|
+
['.js', 'text/javascript; charset=utf-8'],
|
|
16
|
+
['.mjs', 'text/javascript; charset=utf-8'],
|
|
17
|
+
['.cjs', 'text/javascript; charset=utf-8'],
|
|
18
|
+
['.css', 'text/css; charset=utf-8'],
|
|
19
|
+
['.json', 'application/json; charset=utf-8'],
|
|
20
|
+
['.svg', 'image/svg+xml'],
|
|
21
|
+
['.png', 'image/png'],
|
|
22
|
+
['.jpg', 'image/jpeg'],
|
|
23
|
+
['.jpeg', 'image/jpeg'],
|
|
24
|
+
['.gif', 'image/gif'],
|
|
25
|
+
['.webp', 'image/webp'],
|
|
26
|
+
['.ico', 'image/x-icon'],
|
|
27
|
+
['.woff', 'font/woff'],
|
|
28
|
+
['.woff2', 'font/woff2'],
|
|
29
|
+
['.ttf', 'font/ttf'],
|
|
30
|
+
['.wasm', 'application/wasm'],
|
|
31
|
+
['.txt', 'text/plain; charset=utf-8'],
|
|
32
|
+
['.map', 'application/json; charset=utf-8']
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
class BridgeServer {
|
|
36
|
+
constructor() {
|
|
37
|
+
this.server = null;
|
|
38
|
+
this.wss = null;
|
|
39
|
+
this.port = null;
|
|
40
|
+
this.token = crypto.randomBytes(32).toString('hex');
|
|
41
|
+
this.sockets = new Map();
|
|
42
|
+
this.pendingExecutions = new Map();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async start() {
|
|
46
|
+
if (this.server) return;
|
|
47
|
+
|
|
48
|
+
this.server = http.createServer((request, response) => {
|
|
49
|
+
this._handleHttp(request, response).catch((error) => {
|
|
50
|
+
response.statusCode = 500;
|
|
51
|
+
response.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
52
|
+
response.end(`AtomJS bridge error: ${error.message}`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
57
|
+
this.server.on('upgrade', (request, socket, head) => {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(request.url, 'http://127.0.0.1');
|
|
60
|
+
const windowId = Number(url.searchParams.get('windowId'));
|
|
61
|
+
const token = url.searchParams.get('token');
|
|
62
|
+
if (url.pathname !== '/__atom/ws' || token !== this.token || !state.windows.has(windowId)) {
|
|
63
|
+
socket.destroy();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.wss.handleUpgrade(request, socket, head, (websocket) => {
|
|
67
|
+
this.wss.emit('connection', websocket, request, windowId);
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
socket.destroy();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.wss.on('connection', (socket, _request, windowId) => {
|
|
75
|
+
const previous = this.sockets.get(windowId);
|
|
76
|
+
if (previous && previous.readyState === WebSocket.OPEN) previous.close();
|
|
77
|
+
this.sockets.set(windowId, socket);
|
|
78
|
+
|
|
79
|
+
socket.on('message', (raw) => {
|
|
80
|
+
this._handleRendererMessage(windowId, raw).catch((error) => {
|
|
81
|
+
console.error('[AtomJS bridge message error]', error);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
socket.on('close', () => {
|
|
86
|
+
if (this.sockets.get(windowId) === socket) this.sockets.delete(windowId);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await new Promise((resolve, reject) => {
|
|
91
|
+
this.server.once('error', reject);
|
|
92
|
+
this.server.listen(0, '127.0.0.1', () => {
|
|
93
|
+
this.server.off('error', reject);
|
|
94
|
+
const address = this.server.address();
|
|
95
|
+
this.port = address.port;
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async stop() {
|
|
102
|
+
for (const socket of this.sockets.values()) {
|
|
103
|
+
try { socket.close(); } catch {}
|
|
104
|
+
}
|
|
105
|
+
this.sockets.clear();
|
|
106
|
+
if (this.wss) this.wss.close();
|
|
107
|
+
if (this.server) {
|
|
108
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
109
|
+
}
|
|
110
|
+
this.server = null;
|
|
111
|
+
this.wss = null;
|
|
112
|
+
this.port = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
websocketUrl(windowId) {
|
|
116
|
+
if (!this.port) throw new Error('AtomJS bridge server is not running');
|
|
117
|
+
const query = new URLSearchParams({ windowId: String(windowId), token: this.token });
|
|
118
|
+
return `ws://127.0.0.1:${this.port}/__atom/ws?${query}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fileUrl(windowId, relativePath) {
|
|
122
|
+
if (!this.port) throw new Error('AtomJS bridge server is not running');
|
|
123
|
+
const normalized = String(relativePath).split(path.sep).map(encodeURIComponent).join('/');
|
|
124
|
+
return `http://127.0.0.1:${this.port}/__atom/window/${windowId}/${normalized}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
send(windowId, message) {
|
|
128
|
+
const socket = this.sockets.get(windowId);
|
|
129
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
|
130
|
+
socket.send(JSON.stringify(message));
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
executeJavaScript(windowId, code, timeoutMs = 30000) {
|
|
135
|
+
const id = crypto.randomUUID();
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const timeout = setTimeout(() => {
|
|
138
|
+
this.pendingExecutions.delete(id);
|
|
139
|
+
reject(new Error('executeJavaScript timed out'));
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
this.pendingExecutions.set(id, { resolve, reject, timeout });
|
|
142
|
+
if (!this.send(windowId, { type: 'system', command: 'execute', id, code })) {
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
this.pendingExecutions.delete(id);
|
|
145
|
+
reject(new Error('Renderer is not connected'));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async _handleRendererMessage(windowId, raw) {
|
|
151
|
+
const win = state.windows.get(windowId);
|
|
152
|
+
if (!win) return;
|
|
153
|
+
|
|
154
|
+
let message;
|
|
155
|
+
try {
|
|
156
|
+
message = JSON.parse(raw.toString('utf8'));
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (message.type === 'renderer-ready') {
|
|
162
|
+
win._markRendererReady(message);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (message.type === 'send') {
|
|
167
|
+
const event = createIpcEvent(win);
|
|
168
|
+
ipcMain.emit(message.channel, event, ...(Array.isArray(message.args) ? message.args : []));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (message.type === 'invoke') {
|
|
173
|
+
const event = createIpcEvent(win);
|
|
174
|
+
try {
|
|
175
|
+
const result = await ipcMain._invoke(message.channel, event, Array.isArray(message.args) ? message.args : []);
|
|
176
|
+
this.send(windowId, { type: 'invoke-result', id: message.id, ok: true, result });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.send(windowId, {
|
|
179
|
+
type: 'invoke-result',
|
|
180
|
+
id: message.id,
|
|
181
|
+
ok: false,
|
|
182
|
+
error: serializeError(error)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (message.type === 'execute-result') {
|
|
189
|
+
const pending = this.pendingExecutions.get(message.id);
|
|
190
|
+
if (!pending) return;
|
|
191
|
+
this.pendingExecutions.delete(message.id);
|
|
192
|
+
clearTimeout(pending.timeout);
|
|
193
|
+
if (message.ok) pending.resolve(message.result);
|
|
194
|
+
else {
|
|
195
|
+
const error = new Error(message.error && message.error.message ? message.error.message : 'Renderer execution failed');
|
|
196
|
+
if (message.error && message.error.stack) error.stack = message.error.stack;
|
|
197
|
+
pending.reject(error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async _handleHttp(request, response) {
|
|
203
|
+
const url = new URL(request.url, 'http://127.0.0.1');
|
|
204
|
+
|
|
205
|
+
if (url.pathname === '/__atom/health') {
|
|
206
|
+
response.statusCode = 200;
|
|
207
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
208
|
+
response.end(JSON.stringify({ ok: true, runtime: 'atomjs' }));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const match = url.pathname.match(/^\/__atom\/window\/(\d+)\/(.*)$/);
|
|
213
|
+
if (!match) {
|
|
214
|
+
response.statusCode = 404;
|
|
215
|
+
response.end('Not found');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const windowId = Number(match[1]);
|
|
220
|
+
const win = state.windows.get(windowId);
|
|
221
|
+
if (!win || !win._contentRoot) {
|
|
222
|
+
response.statusCode = 404;
|
|
223
|
+
response.end('Unknown window');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const requested = decodeURIComponent(match[2] || 'index.html');
|
|
228
|
+
const root = path.resolve(win._contentRoot);
|
|
229
|
+
let absolute = path.resolve(root, requested);
|
|
230
|
+
|
|
231
|
+
if (absolute !== root && !absolute.startsWith(root + path.sep)) {
|
|
232
|
+
response.statusCode = 403;
|
|
233
|
+
response.end('Forbidden');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let stat;
|
|
238
|
+
try {
|
|
239
|
+
stat = await fs.promises.stat(absolute);
|
|
240
|
+
if (stat.isDirectory()) {
|
|
241
|
+
absolute = path.join(absolute, 'index.html');
|
|
242
|
+
stat = await fs.promises.stat(absolute);
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
response.statusCode = 404;
|
|
246
|
+
response.end('File not found');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!stat.isFile()) {
|
|
251
|
+
response.statusCode = 404;
|
|
252
|
+
response.end('File not found');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
response.statusCode = 200;
|
|
257
|
+
response.setHeader('content-type', MIME_TYPES.get(path.extname(absolute).toLowerCase()) || 'application/octet-stream');
|
|
258
|
+
response.setHeader('content-length', String(stat.size));
|
|
259
|
+
response.setHeader('cache-control', process.env.ATOM_DEV === '1' ? 'no-store' : 'public, max-age=3600');
|
|
260
|
+
response.setHeader('x-content-type-options', 'nosniff');
|
|
261
|
+
fs.createReadStream(absolute).pipe(response);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createIpcEvent(win) {
|
|
266
|
+
return Object.freeze({
|
|
267
|
+
sender: win.webContents,
|
|
268
|
+
senderFrame: null,
|
|
269
|
+
reply(channel, ...args) {
|
|
270
|
+
win.webContents.send(channel, ...args);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function serializeError(error) {
|
|
276
|
+
return {
|
|
277
|
+
name: error && error.name ? String(error.name) : 'Error',
|
|
278
|
+
message: error && error.message ? String(error.message) : String(error),
|
|
279
|
+
stack: error && error.stack ? String(error.stack) : undefined
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { BridgeServer };
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
const state = require('./state.cjs');
|
|
9
|
+
const app = require('./app.cjs');
|
|
10
|
+
const { WebContents } = require('./web-contents.cjs');
|
|
11
|
+
const { generateBridgeScript } = require('./bridge-script.cjs');
|
|
12
|
+
|
|
13
|
+
class BrowserWindow extends EventEmitter {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
super();
|
|
16
|
+
this.id = state.nextWindowId++;
|
|
17
|
+
this.options = normalizeOptions(options);
|
|
18
|
+
this.webContents = new WebContents(this);
|
|
19
|
+
this._child = null;
|
|
20
|
+
this._destroyed = false;
|
|
21
|
+
this._rendererReady = false;
|
|
22
|
+
this._visible = this.options.show;
|
|
23
|
+
this._contentRoot = null;
|
|
24
|
+
this._currentUrl = '';
|
|
25
|
+
this._pendingLoad = null;
|
|
26
|
+
this._menu = null;
|
|
27
|
+
this._menuBarVisible = true;
|
|
28
|
+
this._lastFinishedLoad = null;
|
|
29
|
+
state.windows.set(this.id, this);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loadFile(filePath) {
|
|
33
|
+
const task = this._loadFile(filePath);
|
|
34
|
+
// Electron applications commonly call loadFile() without awaiting it. Keep
|
|
35
|
+
// failures observable to callers while preventing a second, unhandled
|
|
36
|
+
// rejection from terminating modern Node.js processes.
|
|
37
|
+
task.catch(() => {});
|
|
38
|
+
return task;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _loadFile(filePath) {
|
|
42
|
+
const absolute = path.resolve(state.projectRoot, filePath);
|
|
43
|
+
const stat = await fs.promises.stat(absolute);
|
|
44
|
+
if (!stat.isFile()) throw new Error(`BrowserWindow.loadFile expected a file: ${absolute}`);
|
|
45
|
+
this._contentRoot = path.dirname(absolute);
|
|
46
|
+
await app.whenReady();
|
|
47
|
+
const url = state.bridgeServer.fileUrl(this.id, path.basename(absolute));
|
|
48
|
+
return this._load(url);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
loadURL(url) {
|
|
52
|
+
const task = this._loadURL(url);
|
|
53
|
+
task.catch(() => {});
|
|
54
|
+
return task;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async _loadURL(url) {
|
|
58
|
+
await app.whenReady();
|
|
59
|
+
return this._load(String(url));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async _load(url) {
|
|
63
|
+
if (this._destroyed) throw new Error('BrowserWindow has been destroyed');
|
|
64
|
+
this._currentUrl = url;
|
|
65
|
+
|
|
66
|
+
if (this._child) {
|
|
67
|
+
state.bridgeServer.send(this.id, { type: 'system', command: 'navigate', url });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const preloadPath = this.options.webPreferences.preload;
|
|
72
|
+
let preloadCode = '';
|
|
73
|
+
if (preloadPath) {
|
|
74
|
+
preloadCode = await fs.promises.readFile(path.resolve(preloadPath), 'utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const config = {
|
|
78
|
+
title: this.options.title || app.getName(),
|
|
79
|
+
width: this.options.width,
|
|
80
|
+
height: this.options.height,
|
|
81
|
+
resizable: this.options.resizable,
|
|
82
|
+
debug: Boolean(this.options.webPreferences.devTools || process.env.ATOM_DEV === '1'),
|
|
83
|
+
url,
|
|
84
|
+
bridgeScript: generateBridgeScript({
|
|
85
|
+
websocketUrl: state.bridgeServer.websocketUrl(this.id),
|
|
86
|
+
preloadCode
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const configPath = path.join(os.tmpdir(), `atomjs-window-${process.pid}-${this.id}-${Date.now()}.json`);
|
|
91
|
+
await fs.promises.writeFile(configPath, JSON.stringify(config), { mode: 0o600 });
|
|
92
|
+
|
|
93
|
+
const hostPath = path.join(__dirname, 'runtime', 'window-host.mjs');
|
|
94
|
+
const nodeExecutable = process.env.ATOM_NODE_EXECUTABLE || process.execPath;
|
|
95
|
+
this._child = spawn(nodeExecutable, [hostPath, configPath], {
|
|
96
|
+
cwd: state.projectRoot,
|
|
97
|
+
env: process.env,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'inherit']
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
attachHostOutput(this, this._child);
|
|
102
|
+
|
|
103
|
+
this._child.once('error', (error) => {
|
|
104
|
+
this.emit('unresponsive');
|
|
105
|
+
this.emit('error', error);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this._child.once('exit', (code, signal) => {
|
|
109
|
+
this._child = null;
|
|
110
|
+
if (!this._destroyed) {
|
|
111
|
+
const failedBeforeReady = !this._rendererReady && code !== 0 && signal == null;
|
|
112
|
+
if (failedBeforeReady) {
|
|
113
|
+
process.exitCode = code || 1;
|
|
114
|
+
this.webContents.emit('did-fail-load', {}, code || 1, 'AtomJS window host exited before the renderer became ready', this._currentUrl, true);
|
|
115
|
+
}
|
|
116
|
+
this._destroyed = true;
|
|
117
|
+
state.windows.delete(this.id);
|
|
118
|
+
this.emit('closed', { code, signal });
|
|
119
|
+
if (state.windows.size === 0 && !state.isQuitting) app.emit('window-all-closed');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this._pendingLoad = new Promise((resolve, reject) => {
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
cleanup();
|
|
126
|
+
reject(new Error('Renderer did not become ready within 20 seconds'));
|
|
127
|
+
}, 20000);
|
|
128
|
+
const onReady = () => {
|
|
129
|
+
cleanup();
|
|
130
|
+
resolve();
|
|
131
|
+
};
|
|
132
|
+
const onClosed = () => {
|
|
133
|
+
cleanup();
|
|
134
|
+
reject(new Error('Window closed before the page finished loading'));
|
|
135
|
+
};
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
this.off('ready-to-show', onReady);
|
|
139
|
+
this.off('closed', onClosed);
|
|
140
|
+
};
|
|
141
|
+
this.once('ready-to-show', onReady);
|
|
142
|
+
this.once('closed', onClosed);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Keep Electron-style fire-and-forget loadFile() usage from producing an
|
|
146
|
+
// unhandled rejection while still returning the rejecting promise to callers.
|
|
147
|
+
this._pendingLoad.catch(() => {});
|
|
148
|
+
return this._pendingLoad;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_markRendererReady(details) {
|
|
152
|
+
this._notifyDidFinishLoad(details && details.href ? details.href : this._currentUrl, 'bridge');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_handleHostEvent(event) {
|
|
156
|
+
if (!event || typeof event !== 'object') return;
|
|
157
|
+
if (event.type === 'did-start-loading') {
|
|
158
|
+
if (event.url) this._currentUrl = String(event.url);
|
|
159
|
+
this.webContents.emit('did-start-loading');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (event.type === 'did-finish-load') {
|
|
163
|
+
this._notifyDidFinishLoad(event.url || this._currentUrl, 'native');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (event.type === 'did-fail-load') {
|
|
167
|
+
const description = event.error ? String(event.error) : 'Navigation failed';
|
|
168
|
+
this.webContents.emit('did-fail-load', {}, -2, description, event.url || this._currentUrl, true);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (event.type === 'page-title-updated' && event.title) {
|
|
172
|
+
this.options.title = String(event.title);
|
|
173
|
+
this.emit('page-title-updated', { preventDefault() {} }, this.options.title, false);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_notifyDidFinishLoad(url, source) {
|
|
178
|
+
const href = url ? String(url) : this._currentUrl;
|
|
179
|
+
if (href) this._currentUrl = href;
|
|
180
|
+
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
if (this._lastFinishedLoad &&
|
|
183
|
+
this._lastFinishedLoad.url === this._currentUrl &&
|
|
184
|
+
this._lastFinishedLoad.source !== source &&
|
|
185
|
+
now - this._lastFinishedLoad.time < 300) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this._lastFinishedLoad = { url: this._currentUrl, time: now, source };
|
|
189
|
+
|
|
190
|
+
const firstLoad = !this._rendererReady;
|
|
191
|
+
this._rendererReady = true;
|
|
192
|
+
this.webContents.emit('dom-ready');
|
|
193
|
+
this.webContents.emit('did-finish-load');
|
|
194
|
+
this.webContents.emit('did-stop-loading');
|
|
195
|
+
|
|
196
|
+
if (firstLoad) {
|
|
197
|
+
this.emit('ready-to-show');
|
|
198
|
+
if (this.options.show) this.emit('show');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
show() {
|
|
203
|
+
this._visible = true;
|
|
204
|
+
this.emit('show');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
hide() {
|
|
208
|
+
this._visible = false;
|
|
209
|
+
console.warn('[AtomJS] Native hide/show is not yet supported by the current pure-JS window host.');
|
|
210
|
+
this.emit('hide');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
isVisible() {
|
|
214
|
+
return this._visible && !this._destroyed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
focus() {
|
|
218
|
+
console.warn('[AtomJS] Native focus is not yet supported by the current window host.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
blur() {}
|
|
222
|
+
|
|
223
|
+
close() {
|
|
224
|
+
if (this._destroyed) return;
|
|
225
|
+
const event = { defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
|
|
226
|
+
this.emit('close', event);
|
|
227
|
+
if (event.defaultPrevented) return;
|
|
228
|
+
state.bridgeServer && state.bridgeServer.send(this.id, { type: 'system', command: 'close' });
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (!this._destroyed && this._child) this._child.kill();
|
|
231
|
+
}, 800).unref();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
destroy() {
|
|
235
|
+
if (this._destroyed) return;
|
|
236
|
+
this._destroyed = true;
|
|
237
|
+
state.windows.delete(this.id);
|
|
238
|
+
if (this._child) {
|
|
239
|
+
try { this._child.kill(); } catch {}
|
|
240
|
+
this._child = null;
|
|
241
|
+
}
|
|
242
|
+
this.emit('closed');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
isDestroyed() {
|
|
246
|
+
return this._destroyed;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setTitle(title) {
|
|
250
|
+
this.options.title = String(title);
|
|
251
|
+
if (state.bridgeServer) {
|
|
252
|
+
state.bridgeServer.send(this.id, { type: 'system', command: 'set-title', title: this.options.title });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getTitle() {
|
|
257
|
+
return this.options.title || app.getName();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
setMenu(menu) {
|
|
262
|
+
this._menu = menu || null;
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getMenu() {
|
|
267
|
+
return this._menu;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
removeMenu() {
|
|
271
|
+
this._menu = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setMenuBarVisibility(visible) {
|
|
275
|
+
this._menuBarVisible = Boolean(visible);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
isMenuBarVisible() {
|
|
279
|
+
return this._menuBarVisible;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
autoHideMenuBar() {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setAutoHideMenuBar() {}
|
|
287
|
+
|
|
288
|
+
isFullScreen() {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
setFullScreen() {
|
|
293
|
+
console.warn('[AtomJS] Native fullscreen switching is not implemented yet.');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
maximize() {
|
|
297
|
+
console.warn('[AtomJS] Native maximize is not implemented yet.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
unmaximize() {}
|
|
301
|
+
|
|
302
|
+
isMaximized() {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
minimize() {
|
|
307
|
+
console.warn('[AtomJS] Native minimize is not implemented yet.');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
restore() {}
|
|
311
|
+
|
|
312
|
+
isMinimized() {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setSize(width, height) {
|
|
317
|
+
this.options.width = Number(width);
|
|
318
|
+
this.options.height = Number(height);
|
|
319
|
+
console.warn('[AtomJS] Changing the native window size after creation is not yet supported.');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
getSize() {
|
|
323
|
+
return [this.options.width, this.options.height];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setBounds(bounds) {
|
|
327
|
+
if (bounds.width) this.options.width = Number(bounds.width);
|
|
328
|
+
if (bounds.height) this.options.height = Number(bounds.height);
|
|
329
|
+
console.warn('[AtomJS] Changing native bounds after creation is not yet supported.');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getBounds() {
|
|
333
|
+
return { x: 0, y: 0, width: this.options.width, height: this.options.height };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
reload() {
|
|
337
|
+
this.webContents.reload();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static getAllWindows() {
|
|
341
|
+
return [...state.windows.values()].filter((win) => !win.isDestroyed());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static fromId(id) {
|
|
345
|
+
return state.windows.get(Number(id)) || null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static getFocusedWindow() {
|
|
349
|
+
const windows = BrowserWindow.getAllWindows();
|
|
350
|
+
return windows.length ? windows[windows.length - 1] : null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function attachHostOutput(win, child) {
|
|
355
|
+
if (!child.stdout) return;
|
|
356
|
+
let pending = '';
|
|
357
|
+
child.stdout.setEncoding('utf8');
|
|
358
|
+
child.stdout.on('data', (chunk) => {
|
|
359
|
+
pending += chunk;
|
|
360
|
+
let newline;
|
|
361
|
+
while ((newline = pending.indexOf('\n')) !== -1) {
|
|
362
|
+
const line = pending.slice(0, newline).replace(/\r$/, '');
|
|
363
|
+
pending = pending.slice(newline + 1);
|
|
364
|
+
handleHostLine(win, line);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
child.stdout.on('end', () => {
|
|
368
|
+
if (pending) handleHostLine(win, pending);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function handleHostLine(win, line) {
|
|
373
|
+
const prefix = '__ATOMJS_EVENT__';
|
|
374
|
+
if (!line.startsWith(prefix)) {
|
|
375
|
+
if (line) process.stdout.write(line + '\n');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
win._handleHostEvent(JSON.parse(line.slice(prefix.length)));
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.warn('[AtomJS] Invalid window-host event:', error.message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeOptions(options) {
|
|
386
|
+
return {
|
|
387
|
+
width: finiteOr(options.width, 800),
|
|
388
|
+
height: finiteOr(options.height, 600),
|
|
389
|
+
title: options.title ? String(options.title) : '',
|
|
390
|
+
show: options.show !== false,
|
|
391
|
+
resizable: options.resizable !== false,
|
|
392
|
+
center: options.center !== false,
|
|
393
|
+
frame: options.frame !== false,
|
|
394
|
+
backgroundColor: options.backgroundColor || '#ffffff',
|
|
395
|
+
webPreferences: {
|
|
396
|
+
preload: options.webPreferences && options.webPreferences.preload
|
|
397
|
+
? path.resolve(options.webPreferences.preload)
|
|
398
|
+
: null,
|
|
399
|
+
contextIsolation: options.webPreferences ? options.webPreferences.contextIsolation !== false : true,
|
|
400
|
+
nodeIntegration: false,
|
|
401
|
+
devTools: options.webPreferences ? options.webPreferences.devTools !== false : true
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function finiteOr(value, fallback) {
|
|
407
|
+
const number = Number(value);
|
|
408
|
+
return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
module.exports = { BrowserWindow };
|