@atom-js-org/runtime 0.5.3-alpha.0 → 0.5.3-alpha.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/runtime",
3
- "version": "0.5.3-alpha.0",
3
+ "version": "0.5.3-alpha.1",
4
4
  "description": "Electron-like desktop runtime powered by the operating system WebView.",
5
5
  "main": "src/index.cjs",
6
6
  "types": "index.d.ts",
@@ -19,10 +19,11 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=20.12"
22
+ "node": ">=24"
23
23
  },
24
24
  "dependencies": {
25
- "ws": "^8.21.1"
25
+ "ws": "^8.21.1",
26
+ "@webviewjs/webview": "0.4.0"
26
27
  },
27
28
  "repository": {
28
29
  "type": "git",
package/src/app.cjs CHANGED
@@ -7,6 +7,7 @@ const fs = require('node:fs');
7
7
  const state = require('./state.cjs');
8
8
  const { BridgeServer } = require('./bridge-server.cjs');
9
9
  const { stopNativeHost } = require('./native-host.cjs');
10
+ const { stopWindowsNativeHost } = require('./windows-native-host.cjs');
10
11
 
11
12
  class App extends EventEmitter {
12
13
  constructor() {
@@ -43,6 +44,7 @@ class App extends EventEmitter {
43
44
 
44
45
  for (const win of [...state.windows.values()]) win.destroy();
45
46
  await stopNativeHost();
47
+ await stopWindowsNativeHost();
46
48
  if (state.bridgeServer) await state.bridgeServer.stop();
47
49
  this.emit('will-quit');
48
50
  this.emit('quit', {}, 0);
@@ -10,6 +10,7 @@ const app = require('./app.cjs');
10
10
  const { WebContents } = require('./web-contents.cjs');
11
11
  const { generateBridgeScript } = require('./bridge-script.cjs');
12
12
  const { getNativeHost } = require('./native-host.cjs');
13
+ const { getWindowsNativeHost } = require('./windows-native-host.cjs');
13
14
 
14
15
  class BrowserWindow extends EventEmitter {
15
16
  constructor(options = {}) {
@@ -136,6 +137,10 @@ class BrowserWindow extends EventEmitter {
136
137
  this._nativeHost = getNativeHost(app.getName());
137
138
  this._hostAttached = true;
138
139
  await this._nativeHost.createWindow(this, config);
140
+ } else if (process.platform === 'win32') {
141
+ this._nativeHost = getWindowsNativeHost();
142
+ this._hostAttached = true;
143
+ await this._nativeHost.createWindow(this, config);
139
144
  } else {
140
145
  await this._startLegacyHost(config);
141
146
  }
@@ -213,8 +218,7 @@ class BrowserWindow extends EventEmitter {
213
218
 
214
219
  _sendHostCommand(command) {
215
220
  if (this._nativeHost) {
216
- this._nativeHost.send({ ...command, windowId: this.id });
217
- return true;
221
+ return this._nativeHost.send({ ...command, windowId: this.id }) !== false;
218
222
  }
219
223
  return false;
220
224
  }
@@ -29,7 +29,7 @@ try {
29
29
  ({ Webview, SizeHint } = await import('webview-nodejs'));
30
30
  } catch (error) {
31
31
  console.error('\nAtomJS could not load the system WebView binding.');
32
- console.error('Windows and Linux currently require the webview-nodejs package.');
32
+ console.error('Linux currently requires the webview-nodejs package.');
33
33
  console.error('Run `atom doctor`, install the platform prerequisites, then install dependencies again.');
34
34
  console.error(error && error.stack ? error.stack : error);
35
35
  process.exit(1);
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+
3
+ let singleton = null;
4
+
5
+ class WindowsNativeHost {
6
+ constructor() {
7
+ this.binding = null;
8
+ this.application = null;
9
+ this.startPromise = null;
10
+ this.windows = new Map();
11
+ this.stopping = false;
12
+ }
13
+
14
+ async ensureStarted() {
15
+ if (process.platform !== 'win32') {
16
+ throw new Error('The in-process Windows native host can only run on Windows.');
17
+ }
18
+ if (!this.startPromise) {
19
+ this.startPromise = this._start().catch((error) => {
20
+ this.startPromise = null;
21
+ throw error;
22
+ });
23
+ }
24
+ await this.startPromise;
25
+ }
26
+
27
+ async _start() {
28
+ let binding;
29
+ try {
30
+ binding = require('@webviewjs/webview');
31
+ } catch (error) {
32
+ const wrapped = new Error([
33
+ 'AtomJS could not load the prebuilt Windows WebView binding.',
34
+ 'Delete node_modules and package-lock.json, then run npm install again.',
35
+ 'CMake and Visual Studio Build Tools are not required.',
36
+ error && error.message ? error.message : String(error)
37
+ ].join('\n'));
38
+ wrapped.cause = error;
39
+ throw wrapped;
40
+ }
41
+
42
+ this.binding = binding;
43
+ this.application = new binding.Application();
44
+ await this.application.whenReady({ interval: 16, ref: true });
45
+ }
46
+
47
+ async createWindow(atomWindow, config) {
48
+ await this.ensureStarted();
49
+
50
+ const parent = config.parentWindowId ? this.windows.get(Number(config.parentWindowId)) : null;
51
+ const nativeOptions = {
52
+ title: String(config.title || 'AtomJS App'),
53
+ width: positive(config.width, 800),
54
+ height: positive(config.height, 600),
55
+ resizable: config.resizable !== false,
56
+ visible: false,
57
+ decorations: config.frame !== false,
58
+ alwaysOnTop: Boolean(config.alwaysOnTop),
59
+ maximizable: config.maximizable !== false,
60
+ minimizable: config.minimizable !== false,
61
+ focused: config.focusable !== false,
62
+ transparent: Boolean(config.transparent),
63
+ windowsSkipTaskbar: Boolean(config.skipTaskbar),
64
+ windowsClassName: sanitizeWindowsClass(process.env.ATOM_APP_ID || process.env.ATOM_APP_NAME || config.title)
65
+ };
66
+ if (Number.isFinite(Number(config.x))) nativeOptions.x = Math.round(Number(config.x));
67
+ if (Number.isFinite(Number(config.y))) nativeOptions.y = Math.round(Number(config.y));
68
+ if (parent && parent.nativeWindow && typeof parent.nativeWindow.getNativeHandle === 'function') {
69
+ nativeOptions.windowsOwnerWindow = parent.nativeWindow.getNativeHandle();
70
+ }
71
+
72
+ const nativeWindow = this.application.createBrowserWindow(nativeOptions);
73
+ nativeWindow.setClosable(config.closable !== false);
74
+ if (Number(config.minWidth) > 0 || Number(config.minHeight) > 0) {
75
+ nativeWindow.setMinSize(positive(config.minWidth, 1), positive(config.minHeight, 1), true);
76
+ }
77
+ if (Number(config.maxWidth) > 0 || Number(config.maxHeight) > 0) {
78
+ nativeWindow.setMaxSize(positive(config.maxWidth, 100000), positive(config.maxHeight, 100000), true);
79
+ }
80
+ if (config.center !== false && !Number.isFinite(Number(config.x)) && !Number.isFinite(Number(config.y))) {
81
+ nativeWindow.center();
82
+ }
83
+
84
+ const webview = nativeWindow.createWebview({
85
+ url: String(config.url),
86
+ preload: String(config.bridgeScript || ''),
87
+ enableDevtools: Boolean(config.debug),
88
+ transparent: Boolean(config.transparent)
89
+ });
90
+
91
+ const record = { atomWindow, nativeWindow, webview };
92
+ this.windows.set(Number(config.windowId), record);
93
+ this._attachEvents(Number(config.windowId), record);
94
+
95
+ if (config.show === false) nativeWindow.hide();
96
+ else nativeWindow.show();
97
+ }
98
+
99
+ _attachEvents(windowId, record) {
100
+ const emit = (event) => {
101
+ if (!this.windows.has(windowId)) return;
102
+ try { record.atomWindow._handleHostEvent({ ...event, windowId }); } catch {}
103
+ };
104
+
105
+ record.nativeWindow.on('close', () => {
106
+ try { record.atomWindow._handleHostEvent({ type: 'closed', windowId }); } catch {}
107
+ this.windows.delete(windowId);
108
+ });
109
+ record.nativeWindow.on('focus', () => emit({ type: 'focus' }));
110
+ record.nativeWindow.on('blur', () => emit({ type: 'blur' }));
111
+ record.nativeWindow.on('move', (event) => emit({
112
+ type: 'bounds-changed',
113
+ reason: 'move',
114
+ bounds: { x: Number(event.x), y: Number(event.y) }
115
+ }));
116
+ record.nativeWindow.on('resize', (event) => emit({
117
+ type: 'bounds-changed',
118
+ reason: 'resize',
119
+ bounds: { width: Number(event.width), height: Number(event.height) }
120
+ }));
121
+ record.webview.on('page-load-started', (event) => emit({ type: 'did-start-loading', url: event.url || '' }));
122
+ record.webview.on('page-load-finished', (event) => emit({ type: 'did-finish-load', url: event.url || '' }));
123
+ record.webview.on('title-changed', (event) => emit({ type: 'page-title-updated', title: event.title || '' }));
124
+ }
125
+
126
+ send(message) {
127
+ const record = this.windows.get(Number(message.windowId));
128
+ if (!record) return false;
129
+ const win = record.nativeWindow;
130
+ const view = record.webview;
131
+
132
+ switch (message.command) {
133
+ case 'navigate': view.loadUrl(String(message.url)); return true;
134
+ case 'show': win.show(); return true;
135
+ case 'hide': win.hide(); return true;
136
+ case 'focus': win.focus(); return true;
137
+ case 'close': win.close(); return true;
138
+ case 'destroy':
139
+ this.windows.delete(Number(message.windowId));
140
+ try { view.dispose(); } catch {}
141
+ try { win.dispose(); } catch {}
142
+ return true;
143
+ case 'set-title': win.setTitle(String(message.title || '')); return true;
144
+ case 'set-always-on-top': win.setAlwaysOnTop(Boolean(message.value)); return true;
145
+ case 'set-resizable': win.setResizable(Boolean(message.value)); return true;
146
+ case 'fullscreen':
147
+ win.setFullscreen(message.value ? this.binding.FullscreenType.Borderless : null);
148
+ return true;
149
+ case 'maximize': win.setMaximized(true); return true;
150
+ case 'unmaximize': win.setMaximized(false); return true;
151
+ case 'minimize': win.setMinimized(true); return true;
152
+ case 'restore':
153
+ win.setMinimized(false);
154
+ win.setMaximized(false);
155
+ win.show();
156
+ return true;
157
+ case 'set-bounds': {
158
+ const bounds = message.bounds || {};
159
+ if (Number.isFinite(Number(bounds.width)) && Number.isFinite(Number(bounds.height))) {
160
+ win.setSize(Math.round(Number(bounds.width)), Math.round(Number(bounds.height)), true);
161
+ }
162
+ if (Number.isFinite(Number(bounds.x)) && Number.isFinite(Number(bounds.y))) {
163
+ win.setPosition(Math.round(Number(bounds.x)), Math.round(Number(bounds.y)), true);
164
+ }
165
+ return true;
166
+ }
167
+ default: return false;
168
+ }
169
+ }
170
+
171
+ async stop() {
172
+ if (this.stopping) return;
173
+ this.stopping = true;
174
+ for (const record of this.windows.values()) {
175
+ try { record.webview.dispose(); } catch {}
176
+ try { record.nativeWindow.dispose(); } catch {}
177
+ }
178
+ this.windows.clear();
179
+ if (this.application) {
180
+ try { this.application.exit(); } catch {}
181
+ }
182
+ this.application = null;
183
+ this.startPromise = null;
184
+ this.stopping = false;
185
+ }
186
+ }
187
+
188
+ function positive(value, fallback) {
189
+ const number = Number(value);
190
+ return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
191
+ }
192
+
193
+ function sanitizeWindowsClass(value) {
194
+ return String(value || 'AtomJS.App').replace(/[^A-Za-z0-9._-]+/g, '.').slice(0, 120) || 'AtomJS.App';
195
+ }
196
+
197
+ function getWindowsNativeHost() {
198
+ if (!singleton) singleton = new WindowsNativeHost();
199
+ return singleton;
200
+ }
201
+
202
+ async function stopWindowsNativeHost() {
203
+ if (!singleton) return;
204
+ const current = singleton;
205
+ singleton = null;
206
+ await current.stop();
207
+ }
208
+
209
+ module.exports = { WindowsNativeHost, getWindowsNativeHost, stopWindowsNativeHost };