@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
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
AtomJS Attribution License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AtomJS contributors
|
|
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 use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
8
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
1. Framework redistributions, forks, and modified framework distributions must
|
|
12
|
+
retain this license, the NOTICE file, and a reasonably visible attribution
|
|
13
|
+
that includes both the name "AtomJS" and the project link:
|
|
14
|
+
https://github.com/Atom-js-org/atom
|
|
15
|
+
|
|
16
|
+
2. The attribution requirement applies to distributions of the AtomJS
|
|
17
|
+
framework itself or a derivative framework. It does not require applications
|
|
18
|
+
created with AtomJS to display AtomJS branding, notices, splash screens, or
|
|
19
|
+
user-interface credit.
|
|
20
|
+
|
|
21
|
+
3. The names and logos of AtomJS may not be used to imply endorsement of a
|
|
22
|
+
modified distribution without prior permission. Forks may accurately state
|
|
23
|
+
that they are based on AtomJS.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @atom-js-org/runtime
|
|
2
|
+
|
|
3
|
+
The lightweight AtomJS runtime. It uses Node.js for the main process and the operating system WebView for rendering.
|
|
4
|
+
|
|
5
|
+
AtomJS does not install or execute the Electron runtime and does not bundle a private Chromium copy. On Windows, the system WebView is Microsoft Edge WebView2; on macOS it is WKWebView; on Linux it is WebKitGTK through the current native binding.
|
|
6
|
+
|
|
7
|
+
Most applications should import the Electron-compatible alias instead:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install electron@npm:@atom-js-org/electron@alpha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { app, BrowserWindow, ipcMain } = require('electron');
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Project: https://github.com/Atom-js-org/atom
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
|
|
5
|
+
export interface BrowserWindowConstructorOptions {
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
title?: string;
|
|
9
|
+
show?: boolean;
|
|
10
|
+
resizable?: boolean;
|
|
11
|
+
center?: boolean;
|
|
12
|
+
frame?: boolean;
|
|
13
|
+
backgroundColor?: string;
|
|
14
|
+
webPreferences?: {
|
|
15
|
+
preload?: string;
|
|
16
|
+
contextIsolation?: boolean;
|
|
17
|
+
nodeIntegration?: boolean;
|
|
18
|
+
devTools?: boolean;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class WebContents extends EventEmitter {
|
|
23
|
+
readonly id: number;
|
|
24
|
+
send(channel: string, ...args: unknown[]): void;
|
|
25
|
+
executeJavaScript(code: string, userGesture?: boolean): Promise<unknown>;
|
|
26
|
+
reload(): void;
|
|
27
|
+
loadURL(url: string): void;
|
|
28
|
+
openDevTools(): void;
|
|
29
|
+
closeDevTools(): void;
|
|
30
|
+
isDevToolsOpened(): boolean;
|
|
31
|
+
getURL(): string;
|
|
32
|
+
isDestroyed(): boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class BrowserWindow extends EventEmitter {
|
|
36
|
+
constructor(options?: BrowserWindowConstructorOptions);
|
|
37
|
+
readonly id: number;
|
|
38
|
+
readonly webContents: WebContents;
|
|
39
|
+
loadFile(filePath: string): Promise<void>;
|
|
40
|
+
loadURL(url: string): Promise<void>;
|
|
41
|
+
show(): void;
|
|
42
|
+
hide(): void;
|
|
43
|
+
isVisible(): boolean;
|
|
44
|
+
focus(): void;
|
|
45
|
+
blur(): void;
|
|
46
|
+
close(): void;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
isDestroyed(): boolean;
|
|
49
|
+
setTitle(title: string): void;
|
|
50
|
+
getTitle(): string;
|
|
51
|
+
setMenu(menu: Menu | null): this;
|
|
52
|
+
getMenu(): Menu | null;
|
|
53
|
+
removeMenu(): void;
|
|
54
|
+
setMenuBarVisibility(visible: boolean): void;
|
|
55
|
+
isMenuBarVisible(): boolean;
|
|
56
|
+
setAutoHideMenuBar(hide: boolean): void;
|
|
57
|
+
isFullScreen(): boolean;
|
|
58
|
+
setFullScreen(flag: boolean): void;
|
|
59
|
+
maximize(): void;
|
|
60
|
+
unmaximize(): void;
|
|
61
|
+
isMaximized(): boolean;
|
|
62
|
+
minimize(): void;
|
|
63
|
+
restore(): void;
|
|
64
|
+
isMinimized(): boolean;
|
|
65
|
+
setSize(width: number, height: number): void;
|
|
66
|
+
getSize(): [number, number];
|
|
67
|
+
setBounds(bounds: { x?: number; y?: number; width?: number; height?: number }): void;
|
|
68
|
+
getBounds(): { x: number; y: number; width: number; height: number };
|
|
69
|
+
reload(): void;
|
|
70
|
+
static getAllWindows(): BrowserWindow[];
|
|
71
|
+
static fromId(id: number): BrowserWindow | null;
|
|
72
|
+
static getFocusedWindow(): BrowserWindow | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const app: EventEmitter & {
|
|
76
|
+
whenReady(): Promise<typeof app>;
|
|
77
|
+
isReady(): boolean;
|
|
78
|
+
quit(): Promise<void>;
|
|
79
|
+
exit(exitCode?: number): never;
|
|
80
|
+
getName(): string;
|
|
81
|
+
setName(name: string): void;
|
|
82
|
+
getVersion(): string;
|
|
83
|
+
getAppPath(): string;
|
|
84
|
+
getPath(name: string): string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const ipcMain: EventEmitter & {
|
|
88
|
+
handle(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): void;
|
|
89
|
+
handleOnce(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): void;
|
|
90
|
+
removeHandler(channel: string): void;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const dialog: {
|
|
94
|
+
showOpenDialog(options?: object): Promise<{ canceled: boolean; filePaths: string[] }>;
|
|
95
|
+
showSaveDialog(options?: object): Promise<{ canceled: boolean; filePath?: string }>;
|
|
96
|
+
showMessageBox(options?: object): Promise<{ response: number; checkboxChecked: boolean }>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const shell: {
|
|
100
|
+
openExternal(url: string): Promise<void>;
|
|
101
|
+
openPath(filePath: string): Promise<string>;
|
|
102
|
+
showItemInFolder(fullPath: string): void;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const clipboard: {
|
|
106
|
+
writeText(text: string): void;
|
|
107
|
+
readText(): string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export class Menu {
|
|
111
|
+
items: MenuItem[];
|
|
112
|
+
append(item: MenuItem | object): void;
|
|
113
|
+
popup(): void;
|
|
114
|
+
static buildFromTemplate(template: object[]): Menu;
|
|
115
|
+
static setApplicationMenu(menu: Menu | null): void;
|
|
116
|
+
static getApplicationMenu(): Menu | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class MenuItem {
|
|
120
|
+
constructor(options?: object);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class Tray {
|
|
124
|
+
constructor(image: unknown);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const nativeTheme: {
|
|
128
|
+
readonly shouldUseDarkColors: boolean;
|
|
129
|
+
themeSource: 'system' | 'light' | 'dark';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
export class Notification extends EventEmitter {
|
|
134
|
+
constructor(options?: { title?: string; body?: string; [key: string]: unknown });
|
|
135
|
+
show(): void;
|
|
136
|
+
close(): void;
|
|
137
|
+
static isSupported(): boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const session: any;
|
|
141
|
+
export const protocol: any;
|
|
142
|
+
export const net: any;
|
|
143
|
+
export const screen: any;
|
|
144
|
+
export const webContents: any;
|
|
145
|
+
export const nativeImage: any;
|
|
146
|
+
export const globalShortcut: any;
|
|
147
|
+
export const powerSaveBlocker: any;
|
|
148
|
+
export const powerMonitor: any;
|
|
149
|
+
export const systemPreferences: any;
|
|
150
|
+
export const safeStorage: any;
|
|
151
|
+
export const desktopCapturer: any;
|
|
152
|
+
export const crashReporter: any;
|
|
153
|
+
export const autoUpdater: any;
|
|
154
|
+
export const contentTracing: any;
|
|
155
|
+
export const netLog: any;
|
|
156
|
+
export const utilityProcess: any;
|
|
157
|
+
export const pushNotifications: any;
|
|
158
|
+
export const inAppPurchase: any;
|
|
159
|
+
export class BrowserView extends EventEmitter {}
|
|
160
|
+
export class WebContentsView extends BrowserView {}
|
|
161
|
+
export class BaseWindow extends BrowserWindow {}
|
|
162
|
+
export class View extends EventEmitter {}
|
|
163
|
+
export class ImageView extends View {}
|
|
164
|
+
export class MessageChannelMain { port1: any; port2: any; }
|
|
165
|
+
export const MessagePortMain: any;
|
|
166
|
+
export const TouchBar: any;
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atom-js-org/runtime",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "Electron-like desktop runtime powered by the operating system WebView.",
|
|
5
|
+
"main": "src/index.cjs",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"require": "./src/index.cjs",
|
|
11
|
+
"default": "./src/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"index.d.ts",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.12"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"ws": "^8.21.1"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Atom-js-org/atom.git",
|
|
30
|
+
"directory": "packages/atomjs"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/Atom-js-org/atom#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/Atom-js-org/atom/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"tag": "alpha"
|
|
39
|
+
},
|
|
40
|
+
"license": "LicenseRef-AtomJS-1.0"
|
|
41
|
+
}
|
package/src/app.cjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const state = require('./state.cjs');
|
|
8
|
+
const { BridgeServer } = require('./bridge-server.cjs');
|
|
9
|
+
|
|
10
|
+
class App extends EventEmitter {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this._readyPromise = null;
|
|
14
|
+
this._name = readPackageName();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
whenReady() {
|
|
18
|
+
if (!this._readyPromise) {
|
|
19
|
+
this._readyPromise = (async () => {
|
|
20
|
+
state.bridgeServer = new BridgeServer();
|
|
21
|
+
await state.bridgeServer.start();
|
|
22
|
+
queueMicrotask(() => this.emit('ready'));
|
|
23
|
+
return this;
|
|
24
|
+
})();
|
|
25
|
+
}
|
|
26
|
+
return this._readyPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isReady() {
|
|
30
|
+
return Boolean(this._readyPromise && state.bridgeServer && state.bridgeServer.port);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async quit() {
|
|
34
|
+
if (state.isQuitting) return;
|
|
35
|
+
state.isQuitting = true;
|
|
36
|
+
const event = { defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
|
|
37
|
+
this.emit('before-quit', event);
|
|
38
|
+
if (event.defaultPrevented) {
|
|
39
|
+
state.isQuitting = false;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const win of [...state.windows.values()]) win.destroy();
|
|
44
|
+
if (state.bridgeServer) await state.bridgeServer.stop();
|
|
45
|
+
this.emit('will-quit');
|
|
46
|
+
this.emit('quit', {}, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exit(exitCode = 0) {
|
|
50
|
+
for (const win of [...state.windows.values()]) win.destroy();
|
|
51
|
+
process.exit(exitCode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getName() {
|
|
55
|
+
return this._name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setName(name) {
|
|
59
|
+
this._name = String(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getVersion() {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(state.projectRoot, 'package.json'), 'utf8'));
|
|
65
|
+
return pkg.version || '0.0.0';
|
|
66
|
+
} catch {
|
|
67
|
+
return '0.0.0';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getAppPath() {
|
|
72
|
+
return state.projectRoot;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getPath(name) {
|
|
76
|
+
const home = os.homedir();
|
|
77
|
+
const appData = process.platform === 'win32'
|
|
78
|
+
? (process.env.APPDATA || path.join(home, 'AppData', 'Roaming'))
|
|
79
|
+
: process.platform === 'darwin'
|
|
80
|
+
? path.join(home, 'Library', 'Application Support')
|
|
81
|
+
: (process.env.XDG_CONFIG_HOME || path.join(home, '.config'));
|
|
82
|
+
|
|
83
|
+
const table = {
|
|
84
|
+
home,
|
|
85
|
+
appData,
|
|
86
|
+
userData: path.join(appData, this.getName()),
|
|
87
|
+
temp: os.tmpdir(),
|
|
88
|
+
desktop: path.join(home, 'Desktop'),
|
|
89
|
+
documents: path.join(home, 'Documents'),
|
|
90
|
+
downloads: path.join(home, 'Downloads'),
|
|
91
|
+
music: path.join(home, 'Music'),
|
|
92
|
+
pictures: path.join(home, 'Pictures'),
|
|
93
|
+
videos: path.join(home, 'Videos'),
|
|
94
|
+
logs: path.join(appData, this.getName(), 'logs')
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (!(name in table)) throw new Error(`Unsupported app path: ${name}`);
|
|
98
|
+
return table[name];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readPackageName() {
|
|
103
|
+
try {
|
|
104
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(state.projectRoot, 'package.json'), 'utf8'));
|
|
105
|
+
return pkg.productName || pkg.name || 'AtomJS App';
|
|
106
|
+
} catch {
|
|
107
|
+
return 'AtomJS App';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = new App();
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function generateBridgeScript({ websocketUrl, preloadCode = '' }) {
|
|
4
|
+
const endpoint = JSON.stringify(websocketUrl);
|
|
5
|
+
const preload = JSON.stringify(preloadCode);
|
|
6
|
+
|
|
7
|
+
return `
|
|
8
|
+
(() => {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const endpoint = ${endpoint};
|
|
12
|
+
const preloadSource = ${preload};
|
|
13
|
+
const channelListeners = new Map();
|
|
14
|
+
const pendingInvocations = new Map();
|
|
15
|
+
const outboundQueue = [];
|
|
16
|
+
let sequence = 0;
|
|
17
|
+
let socket = null;
|
|
18
|
+
|
|
19
|
+
function serializeError(error) {
|
|
20
|
+
return {
|
|
21
|
+
name: error && error.name ? String(error.name) : 'Error',
|
|
22
|
+
message: error && error.message ? String(error.message) : String(error),
|
|
23
|
+
stack: error && error.stack ? String(error.stack) : undefined
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sendNow(message) {
|
|
28
|
+
const payload = JSON.stringify(message);
|
|
29
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
30
|
+
socket.send(payload);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
outboundQueue.push(payload);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function flushQueue() {
|
|
37
|
+
while (outboundQueue.length && socket && socket.readyState === WebSocket.OPEN) {
|
|
38
|
+
socket.send(outboundQueue.shift());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeEvent(channel) {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
channel,
|
|
45
|
+
senderId: 'main',
|
|
46
|
+
reply(replyChannel, ...args) {
|
|
47
|
+
ipcRenderer.send(replyChannel, ...args);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function addListener(channel, listener, once) {
|
|
53
|
+
if (typeof listener !== 'function') {
|
|
54
|
+
throw new TypeError('IPC listener must be a function');
|
|
55
|
+
}
|
|
56
|
+
const bucket = channelListeners.get(channel) || new Set();
|
|
57
|
+
const record = { listener, once: Boolean(once) };
|
|
58
|
+
bucket.add(record);
|
|
59
|
+
channelListeners.set(channel, bucket);
|
|
60
|
+
return record;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function removeListener(channel, listener) {
|
|
64
|
+
const bucket = channelListeners.get(channel);
|
|
65
|
+
if (!bucket) return;
|
|
66
|
+
for (const record of bucket) {
|
|
67
|
+
if (record.listener === listener) bucket.delete(record);
|
|
68
|
+
}
|
|
69
|
+
if (bucket.size === 0) channelListeners.delete(channel);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function emitChannel(channel, args) {
|
|
73
|
+
const bucket = channelListeners.get(channel);
|
|
74
|
+
if (!bucket) return;
|
|
75
|
+
for (const record of [...bucket]) {
|
|
76
|
+
try {
|
|
77
|
+
record.listener(makeEvent(channel), ...args);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('[AtomJS renderer IPC listener error]', error);
|
|
80
|
+
}
|
|
81
|
+
if (record.once) bucket.delete(record);
|
|
82
|
+
}
|
|
83
|
+
if (bucket.size === 0) channelListeners.delete(channel);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ipcRenderer = Object.freeze({
|
|
87
|
+
send(channel, ...args) {
|
|
88
|
+
sendNow({ type: 'send', channel, args });
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
invoke(channel, ...args) {
|
|
92
|
+
const id = 'invoke-' + (++sequence);
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timeout = setTimeout(() => {
|
|
95
|
+
pendingInvocations.delete(id);
|
|
96
|
+
reject(new Error('IPC invocation timed out: ' + channel));
|
|
97
|
+
}, 30000);
|
|
98
|
+
pendingInvocations.set(id, { resolve, reject, timeout });
|
|
99
|
+
sendNow({ type: 'invoke', id, channel, args });
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
on(channel, listener) {
|
|
104
|
+
addListener(channel, listener, false);
|
|
105
|
+
return this;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
once(channel, listener) {
|
|
109
|
+
addListener(channel, listener, true);
|
|
110
|
+
return this;
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
removeListener(channel, listener) {
|
|
114
|
+
removeListener(channel, listener);
|
|
115
|
+
return this;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
removeAllListeners(channel) {
|
|
119
|
+
if (channel === undefined) channelListeners.clear();
|
|
120
|
+
else channelListeners.delete(channel);
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const contextBridge = Object.freeze({
|
|
126
|
+
exposeInMainWorld(key, api) {
|
|
127
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
128
|
+
throw new TypeError('contextBridge key must be a non-empty string');
|
|
129
|
+
}
|
|
130
|
+
Object.defineProperty(globalThis, key, {
|
|
131
|
+
configurable: false,
|
|
132
|
+
enumerable: true,
|
|
133
|
+
writable: false,
|
|
134
|
+
value: api
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const webFrame = Object.freeze({
|
|
140
|
+
getZoomFactor: () => 1,
|
|
141
|
+
setZoomFactor: () => {},
|
|
142
|
+
getZoomLevel: () => 0,
|
|
143
|
+
setZoomLevel: () => {},
|
|
144
|
+
insertCSS: async () => '',
|
|
145
|
+
removeInsertedCSS: async () => {}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const atomModule = Object.freeze({ ipcRenderer, contextBridge, webFrame });
|
|
149
|
+
|
|
150
|
+
function atomRequire(specifier) {
|
|
151
|
+
if (specifier === '@atom-js-org/runtime' || specifier === 'atomjs' || specifier === 'atom' ||
|
|
152
|
+
specifier === 'electron' || specifier === 'electron/renderer' ||
|
|
153
|
+
specifier === 'electron/common') {
|
|
154
|
+
return atomModule;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(
|
|
157
|
+
"AtomJS system-WebView preload supports require('electron') for Electron renderer APIs, " +
|
|
158
|
+
"plus require('@atom-js-org/runtime'), require('atomjs'), and require('atom'). Use ipcRenderer for privileged Node.js work."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleSystemMessage(message) {
|
|
163
|
+
switch (message.command) {
|
|
164
|
+
case 'close':
|
|
165
|
+
window.close();
|
|
166
|
+
break;
|
|
167
|
+
case 'reload':
|
|
168
|
+
location.reload();
|
|
169
|
+
break;
|
|
170
|
+
case 'navigate':
|
|
171
|
+
location.href = String(message.url);
|
|
172
|
+
break;
|
|
173
|
+
case 'set-title':
|
|
174
|
+
document.title = String(message.title || '');
|
|
175
|
+
break;
|
|
176
|
+
case 'execute': {
|
|
177
|
+
try {
|
|
178
|
+
const result = await (0, eval)(String(message.code));
|
|
179
|
+
sendNow({ type: 'execute-result', id: message.id, ok: true, result });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
sendNow({ type: 'execute-result', id: message.id, ok: false, error: serializeError(error) });
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
console.warn('[AtomJS] Unknown system command:', message.command);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function connect() {
|
|
191
|
+
socket = new WebSocket(endpoint);
|
|
192
|
+
|
|
193
|
+
socket.addEventListener('open', () => {
|
|
194
|
+
flushQueue();
|
|
195
|
+
sendNow({ type: 'bridge-open' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
socket.addEventListener('message', async (event) => {
|
|
199
|
+
let message;
|
|
200
|
+
try {
|
|
201
|
+
message = JSON.parse(String(event.data));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('[AtomJS] Invalid bridge message', error);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (message.type === 'invoke-result') {
|
|
208
|
+
const pending = pendingInvocations.get(message.id);
|
|
209
|
+
if (!pending) return;
|
|
210
|
+
pendingInvocations.delete(message.id);
|
|
211
|
+
clearTimeout(pending.timeout);
|
|
212
|
+
if (message.ok) pending.resolve(message.result);
|
|
213
|
+
else {
|
|
214
|
+
const error = new Error(message.error && message.error.message ? message.error.message : 'IPC invocation failed');
|
|
215
|
+
if (message.error && message.error.stack) error.stack = message.error.stack;
|
|
216
|
+
pending.reject(error);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (message.type === 'event') {
|
|
222
|
+
emitChannel(message.channel, Array.isArray(message.args) ? message.args : []);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (message.type === 'system') {
|
|
227
|
+
await handleSystemMessage(message);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
socket.addEventListener('close', () => {
|
|
232
|
+
for (const pending of pendingInvocations.values()) {
|
|
233
|
+
clearTimeout(pending.timeout);
|
|
234
|
+
pending.reject(new Error('AtomJS bridge disconnected'));
|
|
235
|
+
}
|
|
236
|
+
pendingInvocations.clear();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
socket.addEventListener('error', (event) => {
|
|
240
|
+
console.error('[AtomJS] Renderer bridge error', event);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
connect();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const module = { exports: {} };
|
|
248
|
+
const preloadFunction = new Function('require', 'module', 'exports', preloadSource);
|
|
249
|
+
preloadFunction(atomRequire, module, module.exports);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error('[AtomJS preload error]', error);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function signalReady() {
|
|
255
|
+
sendNow({ type: 'renderer-ready', title: document.title, href: location.href });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (document.readyState === 'loading') {
|
|
259
|
+
document.addEventListener('DOMContentLoaded', signalReady, { once: true });
|
|
260
|
+
} else {
|
|
261
|
+
queueMicrotask(signalReady);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Object.defineProperty(globalThis, '__ATOMJS_INTERNAL__', {
|
|
265
|
+
configurable: false,
|
|
266
|
+
enumerable: false,
|
|
267
|
+
writable: false,
|
|
268
|
+
value: Object.freeze({ ipcRenderer, contextBridge, webFrame })
|
|
269
|
+
});
|
|
270
|
+
})();
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = { generateBridgeScript };
|