@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.
@@ -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,11 @@
1
+ 'use strict';
2
+
3
+ const windows = new Map();
4
+
5
+ module.exports = {
6
+ windows,
7
+ bridgeServer: null,
8
+ nextWindowId: 1,
9
+ projectRoot: process.env.ATOM_PROJECT_ROOT || process.cwd(),
10
+ isQuitting: false
11
+ };
@@ -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 };