@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/src/ipc-main.cjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
|
|
5
|
+
class IpcMain extends EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.handlers = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
handle(channel, listener) {
|
|
12
|
+
validateChannel(channel);
|
|
13
|
+
if (typeof listener !== 'function') {
|
|
14
|
+
throw new TypeError('ipcMain.handle listener must be a function');
|
|
15
|
+
}
|
|
16
|
+
if (this.handlers.has(channel)) {
|
|
17
|
+
throw new Error(`Attempted to register a second handler for '${channel}'`);
|
|
18
|
+
}
|
|
19
|
+
this.handlers.set(channel, listener);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handleOnce(channel, listener) {
|
|
23
|
+
validateChannel(channel);
|
|
24
|
+
const wrapped = async (...args) => {
|
|
25
|
+
this.removeHandler(channel);
|
|
26
|
+
return listener(...args);
|
|
27
|
+
};
|
|
28
|
+
this.handle(channel, wrapped);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
removeHandler(channel) {
|
|
32
|
+
this.handlers.delete(channel);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async _invoke(channel, event, args) {
|
|
36
|
+
const handler = this.handlers.get(channel);
|
|
37
|
+
if (!handler) {
|
|
38
|
+
throw new Error(`No ipcMain handler registered for '${channel}'`);
|
|
39
|
+
}
|
|
40
|
+
return handler(event, ...args);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateChannel(channel) {
|
|
45
|
+
if (typeof channel !== 'string' || channel.length === 0) {
|
|
46
|
+
throw new TypeError('IPC channel must be a non-empty string');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = new IpcMain();
|
package/src/menu.cjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class Menu {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.items = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
append(item) {
|
|
9
|
+
this.items.push(item instanceof MenuItem ? item : new MenuItem(item));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
popup() {
|
|
13
|
+
console.warn('[AtomJS] Native menus are planned but not implemented in this alpha runtime.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static buildFromTemplate(template) {
|
|
17
|
+
const menu = new Menu();
|
|
18
|
+
for (const item of template || []) menu.append(item);
|
|
19
|
+
return menu;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static setApplicationMenu(menu) {
|
|
23
|
+
Menu.applicationMenu = menu;
|
|
24
|
+
console.warn('[AtomJS] Application menus are stored for compatibility but are not native yet.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getApplicationMenu() {
|
|
28
|
+
return Menu.applicationMenu || null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class MenuItem {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
Object.assign(this, options);
|
|
35
|
+
this.label = options.label || '';
|
|
36
|
+
this.enabled = options.enabled !== false;
|
|
37
|
+
this.visible = options.visible !== false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Tray {
|
|
42
|
+
constructor(image) {
|
|
43
|
+
this.image = image;
|
|
44
|
+
throw new Error('Tray is not implemented in AtomJS alpha. It requires a native platform adapter.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { Menu, MenuItem, Tray };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
ObjC.import('Cocoa');
|
|
2
|
+
ObjC.import('WebKit');
|
|
3
|
+
|
|
4
|
+
let atomWindow = null;
|
|
5
|
+
let atomWebView = null;
|
|
6
|
+
let atomWindowDelegate = null;
|
|
7
|
+
let atomNavigationDelegate = null;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function emitHostEvent(payload) {
|
|
11
|
+
try {
|
|
12
|
+
const line = `__ATOMJS_EVENT__${JSON.stringify(payload)}\n`;
|
|
13
|
+
const data = $(line).dataUsingEncoding($.NSUTF8StringEncoding);
|
|
14
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(data);
|
|
15
|
+
} catch (_) {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function webViewUrl(webView) {
|
|
19
|
+
try {
|
|
20
|
+
if (!webView || !webView.URL) return '';
|
|
21
|
+
return ObjC.unwrap(webView.URL.absoluteString) || '';
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createNavigationDelegate() {
|
|
28
|
+
if (!$.AtomJSNavigationDelegate) {
|
|
29
|
+
ObjC.registerSubclass({
|
|
30
|
+
name: 'AtomJSNavigationDelegate',
|
|
31
|
+
protocols: ['WKNavigationDelegate'],
|
|
32
|
+
methods: {
|
|
33
|
+
'webView:didStartProvisionalNavigation:'(webView) {
|
|
34
|
+
emitHostEvent({ type: 'did-start-loading', url: webViewUrl(webView) });
|
|
35
|
+
},
|
|
36
|
+
'webView:didFinishNavigation:'(webView) {
|
|
37
|
+
emitHostEvent({ type: 'did-finish-load', url: webViewUrl(webView) });
|
|
38
|
+
},
|
|
39
|
+
'webView:didFailNavigation:withError:'(webView, _navigation, error) {
|
|
40
|
+
emitHostEvent({
|
|
41
|
+
type: 'did-fail-load',
|
|
42
|
+
url: webViewUrl(webView),
|
|
43
|
+
error: error ? ObjC.unwrap(error.localizedDescription) : 'Navigation failed'
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
'webView:didFailProvisionalNavigation:withError:'(webView, _navigation, error) {
|
|
47
|
+
emitHostEvent({
|
|
48
|
+
type: 'did-fail-load',
|
|
49
|
+
url: webViewUrl(webView),
|
|
50
|
+
error: error ? ObjC.unwrap(error.localizedDescription) : 'Navigation failed'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return $.AtomJSNavigationDelegate.alloc.init;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readUtf8(filePath) {
|
|
60
|
+
const value = $.NSString.stringWithContentsOfFileEncodingError(
|
|
61
|
+
$(String(filePath)),
|
|
62
|
+
$.NSUTF8StringEncoding,
|
|
63
|
+
null
|
|
64
|
+
);
|
|
65
|
+
if (!value) throw new Error(`Unable to read AtomJS window configuration: ${filePath}`);
|
|
66
|
+
return ObjC.unwrap(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createWindowDelegate() {
|
|
70
|
+
if (!$.AtomJSWindowDelegate) {
|
|
71
|
+
ObjC.registerSubclass({
|
|
72
|
+
name: 'AtomJSWindowDelegate',
|
|
73
|
+
protocols: ['NSWindowDelegate'],
|
|
74
|
+
methods: {
|
|
75
|
+
'windowWillClose:'() {
|
|
76
|
+
$.NSApplication.sharedApplication.terminate(null);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return $.AtomJSWindowDelegate.alloc.init;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeWindow(config) {
|
|
85
|
+
const width = Math.max(320, Number(config.width) || 800);
|
|
86
|
+
const height = Math.max(240, Number(config.height) || 600);
|
|
87
|
+
const rect = $.NSMakeRect(0, 0, width, height);
|
|
88
|
+
|
|
89
|
+
let styleMask = $.NSTitledWindowMask |
|
|
90
|
+
$.NSClosableWindowMask |
|
|
91
|
+
$.NSMiniaturizableWindowMask;
|
|
92
|
+
if (config.resizable !== false) styleMask |= $.NSResizableWindowMask;
|
|
93
|
+
|
|
94
|
+
const win = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
|
|
95
|
+
rect,
|
|
96
|
+
styleMask,
|
|
97
|
+
$.NSBackingStoreBuffered,
|
|
98
|
+
false
|
|
99
|
+
);
|
|
100
|
+
win.releasedWhenClosed = false;
|
|
101
|
+
win.title = $(String(config.title || 'AtomJS App'));
|
|
102
|
+
win.center;
|
|
103
|
+
|
|
104
|
+
const contentController = $.WKUserContentController.alloc.init;
|
|
105
|
+
const bridgeSource = String(config.bridgeScript || '');
|
|
106
|
+
if (bridgeSource) {
|
|
107
|
+
const userScript = $.WKUserScript.alloc.initWithSourceInjectionTimeForMainFrameOnly(
|
|
108
|
+
$(bridgeSource),
|
|
109
|
+
$.WKUserScriptInjectionTimeAtDocumentStart,
|
|
110
|
+
true
|
|
111
|
+
);
|
|
112
|
+
contentController.addUserScript(userScript);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const webConfiguration = $.WKWebViewConfiguration.alloc.init;
|
|
116
|
+
webConfiguration.userContentController = contentController;
|
|
117
|
+
|
|
118
|
+
const webView = $.WKWebView.alloc.initWithFrameConfiguration(rect, webConfiguration);
|
|
119
|
+
atomNavigationDelegate = createNavigationDelegate();
|
|
120
|
+
webView.navigationDelegate = atomNavigationDelegate;
|
|
121
|
+
webView.autoresizingMask = $.NSViewWidthSizable | $.NSViewHeightSizable;
|
|
122
|
+
|
|
123
|
+
const url = $.NSURL.URLWithString($(String(config.url)));
|
|
124
|
+
if (!url) throw new Error(`Invalid AtomJS URL: ${config.url}`);
|
|
125
|
+
webView.loadRequest($.NSURLRequest.requestWithURL(url));
|
|
126
|
+
|
|
127
|
+
atomWindowDelegate = createWindowDelegate();
|
|
128
|
+
win.delegate = atomWindowDelegate;
|
|
129
|
+
win.contentView.addSubview(webView);
|
|
130
|
+
|
|
131
|
+
atomWindow = win;
|
|
132
|
+
atomWebView = webView;
|
|
133
|
+
return win;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function run(argv) {
|
|
137
|
+
if (!argv || argv.length < 1) throw new Error('AtomJS macOS host expected a configuration path');
|
|
138
|
+
const config = JSON.parse(readUtf8(argv[0]));
|
|
139
|
+
const app = $.NSApplication.sharedApplication;
|
|
140
|
+
app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
|
|
141
|
+
|
|
142
|
+
const win = makeWindow(config);
|
|
143
|
+
win.makeKeyAndOrderFront(null);
|
|
144
|
+
app.activateIgnoringOtherApps(true);
|
|
145
|
+
app.run;
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const configPath = process.argv[2];
|
|
7
|
+
if (!configPath) {
|
|
8
|
+
console.error('AtomJS window host expected a configuration file');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let config;
|
|
13
|
+
try {
|
|
14
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Unable to read AtomJS window configuration:', error);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
await runMacOSHost(configPath);
|
|
22
|
+
} else {
|
|
23
|
+
fs.rmSync(configPath, { force: true });
|
|
24
|
+
await runNativeBindingHost(config);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runMacOSHost(configurationPath) {
|
|
28
|
+
const runtimeDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const helperPath = path.join(runtimeDirectory, 'macos-window-host.jxa.js');
|
|
30
|
+
const osascript = '/usr/bin/osascript';
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(osascript)) {
|
|
33
|
+
console.error('AtomJS could not find /usr/bin/osascript, required for the pure-JavaScript macOS WKWebView host.');
|
|
34
|
+
fs.rmSync(configurationPath, { force: true });
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const child = spawn(osascript, ['-l', 'JavaScript', helperPath, configurationPath], {
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
env: process.env,
|
|
41
|
+
stdio: ['ignore', 'inherit', 'inherit']
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const forwardSignal = (signal) => {
|
|
45
|
+
if (!child.killed) child.kill(signal);
|
|
46
|
+
};
|
|
47
|
+
process.once('SIGINT', () => forwardSignal('SIGINT'));
|
|
48
|
+
process.once('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
49
|
+
process.once('SIGHUP', () => forwardSignal('SIGHUP'));
|
|
50
|
+
|
|
51
|
+
await new Promise((resolve) => {
|
|
52
|
+
child.once('error', (error) => {
|
|
53
|
+
console.error('AtomJS macOS WKWebView host failed to start:', error && error.stack ? error.stack : error);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
child.once('exit', (code, signal) => {
|
|
58
|
+
if (code !== 0 && signal == null) process.exitCode = code || 1;
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
fs.rmSync(configurationPath, { force: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runNativeBindingHost(runtimeConfig) {
|
|
67
|
+
let Webview;
|
|
68
|
+
let SizeHint;
|
|
69
|
+
try {
|
|
70
|
+
({ Webview, SizeHint } = await import('webview-nodejs'));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('\nAtomJS could not load the system WebView binding.');
|
|
73
|
+
console.error('Windows and Linux currently require the webview-nodejs package.');
|
|
74
|
+
console.error('Run `atom doctor`, install the platform prerequisites, then install dependencies again.');
|
|
75
|
+
console.error(error && error.stack ? error.stack : error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const view = new Webview(Boolean(runtimeConfig.debug));
|
|
81
|
+
view.title(String(runtimeConfig.title || 'AtomJS App'));
|
|
82
|
+
view.size(
|
|
83
|
+
Number(runtimeConfig.width || 800),
|
|
84
|
+
Number(runtimeConfig.height || 600),
|
|
85
|
+
runtimeConfig.resizable === false ? SizeHint.Fixed : SizeHint.None
|
|
86
|
+
);
|
|
87
|
+
view.init(String(runtimeConfig.bridgeScript || ''));
|
|
88
|
+
view.navigate(String(runtimeConfig.url));
|
|
89
|
+
view.show();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('AtomJS native window failed:', error && error.stack ? error.stack : error);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/shell.cjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
const { pathToFileURL } = require('node:url');
|
|
5
|
+
|
|
6
|
+
function openExternal(url) {
|
|
7
|
+
const target = String(url);
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
let command;
|
|
10
|
+
let args;
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
command = 'cmd.exe';
|
|
13
|
+
args = ['/d', '/s', '/c', 'start', '', target.replace(/&/g, '^&')];
|
|
14
|
+
} else if (process.platform === 'darwin') {
|
|
15
|
+
command = 'open';
|
|
16
|
+
args = [target];
|
|
17
|
+
} else {
|
|
18
|
+
command = 'xdg-open';
|
|
19
|
+
args = [target];
|
|
20
|
+
}
|
|
21
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
22
|
+
child.once('error', reject);
|
|
23
|
+
child.once('spawn', () => {
|
|
24
|
+
child.unref();
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function openPath(filePath) {
|
|
31
|
+
return openExternal(pathToFileURL(String(filePath)).href).then(() => '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function showItemInFolder(fullPath) {
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
spawn('explorer.exe', ['/select,', String(fullPath)], { detached: true, stdio: 'ignore' }).unref();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
openPath(require('node:path').dirname(String(fullPath))).catch(() => {});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { openExternal, openPath, showItemInFolder };
|
package/src/state.cjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const state = require('./state.cjs');
|
|
5
|
+
|
|
6
|
+
class WebContents extends EventEmitter {
|
|
7
|
+
constructor(owner) {
|
|
8
|
+
super();
|
|
9
|
+
this.owner = owner;
|
|
10
|
+
this.id = owner.id;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
send(channel, ...args) {
|
|
14
|
+
ensureAlive(this.owner);
|
|
15
|
+
if (typeof channel !== 'string' || channel.length === 0) {
|
|
16
|
+
throw new TypeError('webContents.send channel must be a non-empty string');
|
|
17
|
+
}
|
|
18
|
+
state.bridgeServer.send(this.owner.id, { type: 'event', channel, args });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
executeJavaScript(code, _userGesture = false) {
|
|
22
|
+
ensureAlive(this.owner);
|
|
23
|
+
return state.bridgeServer.executeJavaScript(this.owner.id, String(code));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
reload() {
|
|
27
|
+
ensureAlive(this.owner);
|
|
28
|
+
state.bridgeServer.send(this.owner.id, { type: 'system', command: 'reload' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
loadURL(url) {
|
|
32
|
+
ensureAlive(this.owner);
|
|
33
|
+
state.bridgeServer.send(this.owner.id, { type: 'system', command: 'navigate', url: String(url) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
openDevTools() {
|
|
37
|
+
console.warn('[AtomJS] DevTools must currently be enabled with webPreferences.devTools before the window starts.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
closeDevTools() {}
|
|
41
|
+
|
|
42
|
+
isDevToolsOpened() {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getURL() {
|
|
47
|
+
return this.owner._currentUrl || '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isDestroyed() {
|
|
51
|
+
return this.owner.isDestroyed();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureAlive(owner) {
|
|
56
|
+
if (!owner || owner.isDestroyed()) throw new Error('Object has been destroyed');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { WebContents };
|