@atom-js-org/runtime 0.5.3-alpha.1 → 0.5.3-alpha.2
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 +1 -1
- package/src/bridge-server.cjs +54 -1
- package/src/windows-native-host.cjs +231 -14
package/package.json
CHANGED
package/src/bridge-server.cjs
CHANGED
|
@@ -220,6 +220,7 @@ class BridgeServer {
|
|
|
220
220
|
|
|
221
221
|
const match = url.pathname.match(/^\/__atom\/window\/(\d+)\/(.*)$/);
|
|
222
222
|
if (!match) {
|
|
223
|
+
if (await redirectRootRelativeApplicationAsset(request, response, url)) return;
|
|
223
224
|
response.statusCode = 404;
|
|
224
225
|
response.end('Not found');
|
|
225
226
|
return;
|
|
@@ -271,6 +272,58 @@ class BridgeServer {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
async function redirectRootRelativeApplicationAsset(request, response, url) {
|
|
276
|
+
if (!['GET', 'HEAD'].includes(String(request.method || 'GET').toUpperCase())) return false;
|
|
277
|
+
if (url.pathname.startsWith('/__atom/')) return false;
|
|
278
|
+
|
|
279
|
+
const referer = request.headers && request.headers.referer;
|
|
280
|
+
if (!referer) return false;
|
|
281
|
+
|
|
282
|
+
let refererUrl;
|
|
283
|
+
try {
|
|
284
|
+
refererUrl = new URL(referer, 'http://127.0.0.1');
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const refererMatch = refererUrl.pathname.match(/^\/__atom\/window\/(\d+)\//);
|
|
290
|
+
if (!refererMatch) return false;
|
|
291
|
+
|
|
292
|
+
const windowId = Number(refererMatch[1]);
|
|
293
|
+
const win = state.windows.get(windowId);
|
|
294
|
+
if (!win || !win._contentRoot) return false;
|
|
295
|
+
|
|
296
|
+
let requested;
|
|
297
|
+
try {
|
|
298
|
+
requested = decodeURIComponent(url.pathname.replace(/^\/+/, ''));
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (!requested) return false;
|
|
303
|
+
|
|
304
|
+
const root = path.resolve(win._contentRoot);
|
|
305
|
+
const absolute = path.resolve(root, requested);
|
|
306
|
+
if (absolute !== root && !absolute.startsWith(root + path.sep)) return false;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const stat = await fs.promises.stat(absolute);
|
|
310
|
+
if (!stat.isFile() && !stat.isDirectory()) return false;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const encodedPath = requested
|
|
316
|
+
.split(/[\\/]+/)
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.map((part) => encodeURIComponent(part))
|
|
319
|
+
.join('/');
|
|
320
|
+
response.statusCode = 307;
|
|
321
|
+
response.setHeader('location', `/__atom/window/${windowId}/${encodedPath}${url.search}`);
|
|
322
|
+
response.setHeader('cache-control', 'no-store');
|
|
323
|
+
response.end();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
274
327
|
function createIpcEvent(win) {
|
|
275
328
|
return Object.freeze({
|
|
276
329
|
sender: win.webContents,
|
|
@@ -289,4 +342,4 @@ function serializeError(error) {
|
|
|
289
342
|
};
|
|
290
343
|
}
|
|
291
344
|
|
|
292
|
-
module.exports = { BridgeServer };
|
|
345
|
+
module.exports = { BridgeServer, redirectRootRelativeApplicationAsset };
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
3
7
|
let singleton = null;
|
|
4
8
|
|
|
5
9
|
class WindowsNativeHost {
|
|
6
10
|
constructor() {
|
|
7
11
|
this.binding = null;
|
|
8
12
|
this.application = null;
|
|
13
|
+
this.webContext = null;
|
|
14
|
+
this.webviewDataDirectory = null;
|
|
9
15
|
this.startPromise = null;
|
|
10
16
|
this.windows = new Map();
|
|
11
17
|
this.stopping = false;
|
|
@@ -42,6 +48,23 @@ class WindowsNativeHost {
|
|
|
42
48
|
this.binding = binding;
|
|
43
49
|
this.application = new binding.Application();
|
|
44
50
|
await this.application.whenReady({ interval: 16, ref: true });
|
|
51
|
+
|
|
52
|
+
this.webviewDataDirectory = resolveWritableWebViewDataDirectory();
|
|
53
|
+
try {
|
|
54
|
+
this.webContext = this.application.createWebContext({
|
|
55
|
+
dataDirectory: this.webviewDataDirectory,
|
|
56
|
+
allowsAutomation: false
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const wrapped = new Error([
|
|
60
|
+
'AtomJS could not create a writable WebView2 data directory.',
|
|
61
|
+
`Directory: ${this.webviewDataDirectory}`,
|
|
62
|
+
'Check that the current Windows user can write to LOCALAPPDATA.',
|
|
63
|
+
error && error.message ? error.message : String(error)
|
|
64
|
+
].join('\n'));
|
|
65
|
+
wrapped.cause = error;
|
|
66
|
+
throw wrapped;
|
|
67
|
+
}
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
async createWindow(atomWindow, config) {
|
|
@@ -52,6 +75,7 @@ class WindowsNativeHost {
|
|
|
52
75
|
title: String(config.title || 'AtomJS App'),
|
|
53
76
|
width: positive(config.width, 800),
|
|
54
77
|
height: positive(config.height, 600),
|
|
78
|
+
logical: true,
|
|
55
79
|
resizable: config.resizable !== false,
|
|
56
80
|
visible: false,
|
|
57
81
|
decorations: config.frame !== false,
|
|
@@ -85,10 +109,23 @@ class WindowsNativeHost {
|
|
|
85
109
|
url: String(config.url),
|
|
86
110
|
preload: String(config.bridgeScript || ''),
|
|
87
111
|
enableDevtools: Boolean(config.debug),
|
|
88
|
-
transparent: Boolean(config.transparent)
|
|
112
|
+
transparent: Boolean(config.transparent),
|
|
113
|
+
webContext: this.webContext
|
|
89
114
|
});
|
|
90
115
|
|
|
91
|
-
const record = {
|
|
116
|
+
const record = {
|
|
117
|
+
atomWindow,
|
|
118
|
+
nativeWindow,
|
|
119
|
+
webview,
|
|
120
|
+
dragRegions: [],
|
|
121
|
+
dragViewport: {
|
|
122
|
+
width: positive(config.width, 800),
|
|
123
|
+
height: positive(config.height, 600)
|
|
124
|
+
},
|
|
125
|
+
lastCursorPhysical: null,
|
|
126
|
+
dragState: null,
|
|
127
|
+
lastDragClick: null
|
|
128
|
+
};
|
|
92
129
|
this.windows.set(Number(config.windowId), record);
|
|
93
130
|
this._attachEvents(Number(config.windowId), record);
|
|
94
131
|
|
|
@@ -107,22 +144,82 @@ class WindowsNativeHost {
|
|
|
107
144
|
this.windows.delete(windowId);
|
|
108
145
|
});
|
|
109
146
|
record.nativeWindow.on('focus', () => emit({ type: 'focus' }));
|
|
110
|
-
record.nativeWindow.on('blur', () =>
|
|
111
|
-
|
|
112
|
-
type: '
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
record.nativeWindow.on('blur', () => {
|
|
148
|
+
record.dragState = null;
|
|
149
|
+
emit({ type: 'blur' });
|
|
150
|
+
});
|
|
151
|
+
record.nativeWindow.on('move', (event) => {
|
|
152
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
153
|
+
emit({
|
|
154
|
+
type: 'bounds-changed',
|
|
155
|
+
reason: 'move',
|
|
156
|
+
bounds: { x: Number(event.x) / scale, y: Number(event.y) / scale }
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
record.nativeWindow.on('resize', (event) => {
|
|
160
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
161
|
+
emit({
|
|
162
|
+
type: 'bounds-changed',
|
|
163
|
+
reason: 'resize',
|
|
164
|
+
bounds: { width: Number(event.width) / scale, height: Number(event.height) / scale }
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
record.nativeWindow.on('mouse-move', (event) => {
|
|
168
|
+
const point = physicalPoint(event);
|
|
169
|
+
if (!point) return;
|
|
170
|
+
record.lastCursorPhysical = point;
|
|
171
|
+
if (record.dragState) this._continueWindowDrag(record, point);
|
|
172
|
+
});
|
|
173
|
+
record.nativeWindow.on('mouse-down', (event) => {
|
|
174
|
+
if (Number(event.button) !== 0) return;
|
|
175
|
+
const point = physicalPoint(event);
|
|
176
|
+
if (!point) return;
|
|
177
|
+
record.lastCursorPhysical = point;
|
|
178
|
+
if (!isDraggablePoint(record, point)) return;
|
|
179
|
+
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const previous = record.lastDragClick;
|
|
182
|
+
record.lastDragClick = { time: now, x: point.x, y: point.y };
|
|
183
|
+
if (previous && now - previous.time <= 450 && distanceSquared(previous, point) <= 64) {
|
|
184
|
+
record.dragState = null;
|
|
185
|
+
record.lastDragClick = null;
|
|
186
|
+
try { record.nativeWindow.setMaximized(!record.nativeWindow.isMaximized()); } catch {}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this._beginWindowDrag(record, point);
|
|
191
|
+
});
|
|
192
|
+
record.nativeWindow.on('mouse-up', (event) => {
|
|
193
|
+
if (Number(event.button) === 0) record.dragState = null;
|
|
194
|
+
});
|
|
121
195
|
record.webview.on('page-load-started', (event) => emit({ type: 'did-start-loading', url: event.url || '' }));
|
|
122
196
|
record.webview.on('page-load-finished', (event) => emit({ type: 'did-finish-load', url: event.url || '' }));
|
|
123
197
|
record.webview.on('title-changed', (event) => emit({ type: 'page-title-updated', title: event.title || '' }));
|
|
124
198
|
}
|
|
125
199
|
|
|
200
|
+
_beginWindowDrag(record, point) {
|
|
201
|
+
if (!point) return false;
|
|
202
|
+
record.dragState = { offsetX: point.x, offsetY: point.y };
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_continueWindowDrag(record, point) {
|
|
207
|
+
const state = record.dragState;
|
|
208
|
+
if (!state) return;
|
|
209
|
+
try {
|
|
210
|
+
const current = record.nativeWindow.getPosition(false);
|
|
211
|
+
const globalX = Number(current.x) + point.x;
|
|
212
|
+
const globalY = Number(current.y) + point.y;
|
|
213
|
+
record.nativeWindow.setPosition(
|
|
214
|
+
Math.round(globalX - state.offsetX),
|
|
215
|
+
Math.round(globalY - state.offsetY),
|
|
216
|
+
false
|
|
217
|
+
);
|
|
218
|
+
} catch {
|
|
219
|
+
record.dragState = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
126
223
|
send(message) {
|
|
127
224
|
const record = this.windows.get(Number(message.windowId));
|
|
128
225
|
if (!record) return false;
|
|
@@ -154,6 +251,12 @@ class WindowsNativeHost {
|
|
|
154
251
|
win.setMaximized(false);
|
|
155
252
|
win.show();
|
|
156
253
|
return true;
|
|
254
|
+
case 'set-drag-regions':
|
|
255
|
+
record.dragRegions = normalizeDragRegions(message.regions);
|
|
256
|
+
record.dragViewport = normalizeViewport(message.viewport, win);
|
|
257
|
+
return true;
|
|
258
|
+
case 'start-drag':
|
|
259
|
+
return this._beginWindowDrag(record, record.lastCursorPhysical);
|
|
157
260
|
case 'set-bounds': {
|
|
158
261
|
const bounds = message.bounds || {};
|
|
159
262
|
if (Number.isFinite(Number(bounds.width)) && Number.isFinite(Number(bounds.height))) {
|
|
@@ -176,9 +279,14 @@ class WindowsNativeHost {
|
|
|
176
279
|
try { record.nativeWindow.dispose(); } catch {}
|
|
177
280
|
}
|
|
178
281
|
this.windows.clear();
|
|
282
|
+
if (this.webContext) {
|
|
283
|
+
try { this.webContext.dispose(); } catch {}
|
|
284
|
+
}
|
|
179
285
|
if (this.application) {
|
|
180
286
|
try { this.application.exit(); } catch {}
|
|
181
287
|
}
|
|
288
|
+
this.webContext = null;
|
|
289
|
+
this.webviewDataDirectory = null;
|
|
182
290
|
this.application = null;
|
|
183
291
|
this.startPromise = null;
|
|
184
292
|
this.stopping = false;
|
|
@@ -190,6 +298,108 @@ function positive(value, fallback) {
|
|
|
190
298
|
return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
|
|
191
299
|
}
|
|
192
300
|
|
|
301
|
+
function safeScaleFactor(win) {
|
|
302
|
+
try {
|
|
303
|
+
const scale = Number(win.scaleFactor());
|
|
304
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
305
|
+
} catch {
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function physicalPoint(event) {
|
|
311
|
+
const x = Number(event && event.x);
|
|
312
|
+
const y = Number(event && event.y);
|
|
313
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
314
|
+
return { x, y };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeDragRegions(regions) {
|
|
318
|
+
const normalized = [];
|
|
319
|
+
for (const region of Array.isArray(regions) ? regions.slice(0, 4096) : []) {
|
|
320
|
+
const x = Number(region && region.x);
|
|
321
|
+
const y = Number(region && region.y);
|
|
322
|
+
const width = Number(region && region.width);
|
|
323
|
+
const height = Number(region && region.height);
|
|
324
|
+
if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) continue;
|
|
325
|
+
normalized.push({ x, y, width, height, draggable: region.draggable === true });
|
|
326
|
+
}
|
|
327
|
+
return normalized;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeViewport(viewport, win) {
|
|
331
|
+
let fallback = { width: 1, height: 1 };
|
|
332
|
+
try {
|
|
333
|
+
const size = win.getInnerSize(true);
|
|
334
|
+
fallback = { width: positive(size.width, 1), height: positive(size.height, 1) };
|
|
335
|
+
} catch {}
|
|
336
|
+
const width = Number(viewport && viewport.width);
|
|
337
|
+
const height = Number(viewport && viewport.height);
|
|
338
|
+
return {
|
|
339
|
+
width: Number.isFinite(width) && width > 0 ? width : fallback.width,
|
|
340
|
+
height: Number.isFinite(height) && height > 0 ? height : fallback.height
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isDraggablePoint(record, physical) {
|
|
345
|
+
if (!record.dragRegions.length) return false;
|
|
346
|
+
const scale = safeScaleFactor(record.nativeWindow);
|
|
347
|
+
const logical = { x: physical.x / scale, y: physical.y / scale };
|
|
348
|
+
let inner = { width: record.dragViewport.width, height: record.dragViewport.height };
|
|
349
|
+
try { inner = record.nativeWindow.getInnerSize(true); } catch {}
|
|
350
|
+
const scaleX = record.dragViewport.width > 0 ? Number(inner.width) / record.dragViewport.width : 1;
|
|
351
|
+
const scaleY = record.dragViewport.height > 0 ? Number(inner.height) / record.dragViewport.height : 1;
|
|
352
|
+
let draggable = false;
|
|
353
|
+
|
|
354
|
+
for (const region of record.dragRegions) {
|
|
355
|
+
const inside = logical.x >= region.x * scaleX &&
|
|
356
|
+
logical.x < (region.x + region.width) * scaleX &&
|
|
357
|
+
logical.y >= region.y * scaleY &&
|
|
358
|
+
logical.y < (region.y + region.height) * scaleY;
|
|
359
|
+
if (!inside) continue;
|
|
360
|
+
if (!region.draggable) return false;
|
|
361
|
+
draggable = true;
|
|
362
|
+
}
|
|
363
|
+
return draggable;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function distanceSquared(a, b) {
|
|
367
|
+
const dx = Number(a.x) - Number(b.x);
|
|
368
|
+
const dy = Number(a.y) - Number(b.y);
|
|
369
|
+
return (dx * dx) + (dy * dy);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveWritableWebViewDataDirectory() {
|
|
373
|
+
const identity = sanitizePathSegment(
|
|
374
|
+
process.env.ATOM_APP_ID || process.env.ATOM_APP_NAME || 'AtomJS.App'
|
|
375
|
+
);
|
|
376
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
377
|
+
const candidates = [
|
|
378
|
+
path.join(localAppData, identity, 'AtomJS', 'WebView2'),
|
|
379
|
+
path.join(os.tmpdir(), identity, 'AtomJS', 'WebView2')
|
|
380
|
+
];
|
|
381
|
+
let lastError;
|
|
382
|
+
|
|
383
|
+
for (const candidate of candidates) {
|
|
384
|
+
try {
|
|
385
|
+
fs.mkdirSync(candidate, { recursive: true });
|
|
386
|
+
fs.accessSync(candidate, fs.constants.R_OK | fs.constants.W_OK);
|
|
387
|
+
return candidate;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
lastError = error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
throw new Error(`No writable WebView2 data directory was available: ${lastError ? lastError.message : 'unknown error'}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function sanitizePathSegment(value) {
|
|
397
|
+
return String(value || 'AtomJS.App')
|
|
398
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-')
|
|
399
|
+
.replace(/[. ]+$/g, '')
|
|
400
|
+
.slice(0, 120) || 'AtomJS.App';
|
|
401
|
+
}
|
|
402
|
+
|
|
193
403
|
function sanitizeWindowsClass(value) {
|
|
194
404
|
return String(value || 'AtomJS.App').replace(/[^A-Za-z0-9._-]+/g, '.').slice(0, 120) || 'AtomJS.App';
|
|
195
405
|
}
|
|
@@ -206,4 +416,11 @@ async function stopWindowsNativeHost() {
|
|
|
206
416
|
await current.stop();
|
|
207
417
|
}
|
|
208
418
|
|
|
209
|
-
module.exports = {
|
|
419
|
+
module.exports = {
|
|
420
|
+
WindowsNativeHost,
|
|
421
|
+
getWindowsNativeHost,
|
|
422
|
+
stopWindowsNativeHost,
|
|
423
|
+
isDraggablePoint,
|
|
424
|
+
normalizeDragRegions,
|
|
425
|
+
resolveWritableWebViewDataDirectory
|
|
426
|
+
};
|