electrobun 0.0.19-beta.8 → 0.0.19-beta.80
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/BUILD.md +90 -0
- package/bin/electrobun.cjs +165 -0
- package/debug.js +5 -0
- package/dist/api/browser/builtinrpcSchema.ts +19 -0
- package/dist/api/browser/index.ts +409 -0
- package/dist/api/browser/rpc/webview.ts +79 -0
- package/dist/api/browser/stylesAndElements.ts +3 -0
- package/dist/api/browser/webviewtag.ts +534 -0
- package/dist/api/bun/core/ApplicationMenu.ts +66 -0
- package/dist/api/bun/core/BrowserView.ts +349 -0
- package/dist/api/bun/core/BrowserWindow.ts +191 -0
- package/dist/api/bun/core/ContextMenu.ts +67 -0
- package/dist/api/bun/core/Paths.ts +5 -0
- package/dist/api/bun/core/Socket.ts +181 -0
- package/dist/api/bun/core/Tray.ts +107 -0
- package/dist/api/bun/core/Updater.ts +552 -0
- package/dist/api/bun/core/Utils.ts +48 -0
- package/dist/api/bun/events/ApplicationEvents.ts +14 -0
- package/dist/api/bun/events/event.ts +29 -0
- package/dist/api/bun/events/eventEmitter.ts +45 -0
- package/dist/api/bun/events/trayEvents.ts +9 -0
- package/dist/api/bun/events/webviewEvents.ts +16 -0
- package/dist/api/bun/events/windowEvents.ts +12 -0
- package/dist/api/bun/index.ts +45 -0
- package/dist/api/bun/proc/linux.md +43 -0
- package/dist/api/bun/proc/native.ts +1220 -0
- package/dist/api/shared/platform.ts +48 -0
- package/dist/main.js +53 -0
- package/package.json +15 -7
- package/src/cli/index.ts +1034 -210
- package/templates/hello-world/README.md +57 -0
- package/templates/hello-world/bun.lock +63 -0
- package/templates/hello-world/electrobun.config +18 -0
- package/templates/hello-world/package.json +16 -0
- package/templates/hello-world/src/bun/index.ts +15 -0
- package/templates/hello-world/src/mainview/index.css +124 -0
- package/templates/hello-world/src/mainview/index.html +47 -0
- package/templates/hello-world/src/mainview/index.ts +5 -0
- package/bin/electrobun +0 -0
package/BUILD.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Build System
|
|
2
|
+
|
|
3
|
+
This document describes Electrobun's build system and cross-platform compilation approach.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Electrobun uses a custom build system (`build.ts`) that handles:
|
|
8
|
+
- Vendoring dependencies (Bun, Zig, CEF, WebView2)
|
|
9
|
+
- Building native wrappers for each platform
|
|
10
|
+
- Creating distribution packages
|
|
11
|
+
|
|
12
|
+
## Platform-Specific Native Wrappers
|
|
13
|
+
|
|
14
|
+
### macOS
|
|
15
|
+
- Single `libNativeWrapper.dylib` with weak linking to CEF framework
|
|
16
|
+
- Uses `-weak_framework 'Chromium Embedded Framework'` for optional CEF support
|
|
17
|
+
- Gracefully falls back to WebKit when CEF is not bundled
|
|
18
|
+
|
|
19
|
+
### Windows
|
|
20
|
+
- Single `libNativeWrapper.dll` with runtime CEF detection
|
|
21
|
+
- Links both WebView2 and CEF libraries at build time
|
|
22
|
+
- Uses runtime checks to determine which webview engine to use
|
|
23
|
+
|
|
24
|
+
### Linux
|
|
25
|
+
**Dual Binary Approach** - Linux builds create two separate native wrapper binaries:
|
|
26
|
+
|
|
27
|
+
#### `libNativeWrapper.so` (GTK-only)
|
|
28
|
+
- Size: ~1.46MB
|
|
29
|
+
- Dependencies: WebKitGTK, GTK+3, AppIndicator only
|
|
30
|
+
- No CEF dependencies linked
|
|
31
|
+
- Used when `bundleCEF: false` in electrobun.config
|
|
32
|
+
|
|
33
|
+
#### `libNativeWrapper_cef.so` (CEF-enabled)
|
|
34
|
+
- Size: ~3.47MB
|
|
35
|
+
- Dependencies: WebKitGTK, GTK+3, AppIndicator + CEF libraries
|
|
36
|
+
- Full CEF functionality available
|
|
37
|
+
- Used when `bundleCEF: true` in electrobun.config
|
|
38
|
+
|
|
39
|
+
#### Why Dual Binaries?
|
|
40
|
+
|
|
41
|
+
Unlike macOS and Windows, Linux doesn't have reliable weak linking for shared libraries. Hard linking CEF libraries causes `dlopen` failures when CEF isn't bundled. The dual binary approach provides:
|
|
42
|
+
|
|
43
|
+
1. **Small bundle sizes** - Developers can ship lightweight apps without CEF overhead
|
|
44
|
+
2. **Flexibility** - Same codebase supports both system WebKitGTK and CEF rendering
|
|
45
|
+
3. **Reliability** - No runtime linking failures or undefined symbols
|
|
46
|
+
|
|
47
|
+
#### CLI Binary Selection
|
|
48
|
+
|
|
49
|
+
The Electrobun CLI automatically copies the appropriate binary based on the `bundleCEF` setting:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const useCEF = config.build.linux?.bundleCEF;
|
|
53
|
+
const nativeWrapperSource = useCEF
|
|
54
|
+
? PATHS.NATIVE_WRAPPER_LINUX_CEF
|
|
55
|
+
: PATHS.NATIVE_WRAPPER_LINUX;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Both binaries are included in the distributed `electrobun` npm package, ensuring developers can toggle CEF support without recompilation.
|
|
59
|
+
|
|
60
|
+
## Build Commands
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Full build with all platforms
|
|
64
|
+
bun build.ts
|
|
65
|
+
|
|
66
|
+
# Development build with playground
|
|
67
|
+
bun dev:playground
|
|
68
|
+
|
|
69
|
+
# Release build
|
|
70
|
+
bun build.ts --release
|
|
71
|
+
|
|
72
|
+
# CI build
|
|
73
|
+
bun build.ts --ci
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Architecture Support
|
|
77
|
+
|
|
78
|
+
- **macOS**: ARM64 (Apple Silicon), x64 (Intel)
|
|
79
|
+
- **Windows**: x64 only (ARM Windows users run via automatic emulation)
|
|
80
|
+
- **Linux**: x64, ARM64
|
|
81
|
+
|
|
82
|
+
### Windows Architecture Notes
|
|
83
|
+
|
|
84
|
+
Windows builds are created on ARM VMs but target x64 architecture. Both x64 and ARM Windows users use the same x64 binary:
|
|
85
|
+
- **x64 Windows**: Runs natively
|
|
86
|
+
- **ARM Windows**: Runs via automatic Windows emulation layer
|
|
87
|
+
|
|
88
|
+
This approach simplifies distribution while maintaining compatibility across Windows architectures.
|
|
89
|
+
|
|
90
|
+
The build system automatically detects the host architecture and downloads appropriate dependencies.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const { existsSync, mkdirSync, createWriteStream, unlinkSync, chmodSync } = require('fs');
|
|
5
|
+
const { join, dirname } = require('path');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const tar = require('tar');
|
|
8
|
+
|
|
9
|
+
// Detect platform and architecture
|
|
10
|
+
function getPlatform() {
|
|
11
|
+
switch (process.platform) {
|
|
12
|
+
case 'win32': return 'win';
|
|
13
|
+
case 'darwin': return 'darwin';
|
|
14
|
+
case 'linux': return 'linux';
|
|
15
|
+
default: throw new Error(`Unsupported platform: ${process.platform}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getArch() {
|
|
20
|
+
switch (process.arch) {
|
|
21
|
+
case 'arm64': return 'arm64';
|
|
22
|
+
case 'x64': return 'x64';
|
|
23
|
+
default: throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const platform = getPlatform();
|
|
28
|
+
// Always use x64 for Windows since we only build x64 Windows binaries
|
|
29
|
+
const arch = platform === 'win' ? 'x64' : getArch();
|
|
30
|
+
const binExt = platform === 'win' ? '.exe' : '';
|
|
31
|
+
|
|
32
|
+
// Paths
|
|
33
|
+
const electrobunDir = join(__dirname, '..');
|
|
34
|
+
const cacheDir = join(electrobunDir, '.cache');
|
|
35
|
+
const cliBinary = join(cacheDir, `electrobun${binExt}`);
|
|
36
|
+
|
|
37
|
+
async function downloadFile(url, filePath) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
40
|
+
const file = createWriteStream(filePath);
|
|
41
|
+
|
|
42
|
+
https.get(url, (response) => {
|
|
43
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
44
|
+
// Follow redirect
|
|
45
|
+
return downloadFile(response.headers.location, filePath).then(resolve).catch(reject);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (response.statusCode !== 200) {
|
|
49
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
response.pipe(file);
|
|
54
|
+
|
|
55
|
+
file.on('finish', () => {
|
|
56
|
+
file.close();
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
file.on('error', reject);
|
|
61
|
+
}).on('error', reject);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function ensureCliBinary() {
|
|
66
|
+
// Check if CLI binary exists in bin location (where npm expects it)
|
|
67
|
+
const binLocation = join(electrobunDir, 'bin', 'electrobun' + binExt);
|
|
68
|
+
if (existsSync(binLocation)) {
|
|
69
|
+
return binLocation;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if core dependencies already exist in cache
|
|
73
|
+
if (existsSync(cliBinary)) {
|
|
74
|
+
// Copy to bin location if it exists in cache but not in bin
|
|
75
|
+
mkdirSync(dirname(binLocation), { recursive: true });
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
fs.copyFileSync(cliBinary, binLocation);
|
|
78
|
+
if (platform !== 'win') {
|
|
79
|
+
chmodSync(binLocation, '755');
|
|
80
|
+
}
|
|
81
|
+
return binLocation;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('Downloading electrobun CLI for your platform...');
|
|
85
|
+
|
|
86
|
+
// Get the package version to download the matching release
|
|
87
|
+
const packageJson = require(join(electrobunDir, 'package.json'));
|
|
88
|
+
const version = packageJson.version;
|
|
89
|
+
const tag = `v${version}`;
|
|
90
|
+
|
|
91
|
+
const tarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${tag}/electrobun-cli-${platform}-${arch}.tar.gz`;
|
|
92
|
+
const tarballPath = join(cacheDir, `electrobun-${platform}-${arch}.tar.gz`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Download tarball
|
|
96
|
+
await downloadFile(tarballUrl, tarballPath);
|
|
97
|
+
|
|
98
|
+
// Extract CLI binary
|
|
99
|
+
await tar.x({
|
|
100
|
+
file: tarballPath,
|
|
101
|
+
cwd: cacheDir
|
|
102
|
+
// No strip needed - CLI tarball contains just the binary
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Clean up tarball
|
|
106
|
+
unlinkSync(tarballPath);
|
|
107
|
+
|
|
108
|
+
// Check if CLI binary was extracted
|
|
109
|
+
if (!existsSync(cliBinary)) {
|
|
110
|
+
throw new Error(`CLI binary not found at ${cliBinary} after extraction`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Make executable on Unix systems
|
|
114
|
+
if (platform !== 'win') {
|
|
115
|
+
chmodSync(cliBinary, '755');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Copy CLI to bin location so npm scripts can find it
|
|
119
|
+
const binLocation = join(electrobunDir, 'bin', 'electrobun' + binExt);
|
|
120
|
+
mkdirSync(dirname(binLocation), { recursive: true });
|
|
121
|
+
|
|
122
|
+
// Copy the downloaded CLI to replace this script
|
|
123
|
+
const fs = require('fs');
|
|
124
|
+
fs.copyFileSync(cliBinary, binLocation);
|
|
125
|
+
|
|
126
|
+
// Make the bin location executable too
|
|
127
|
+
if (platform !== 'win') {
|
|
128
|
+
chmodSync(binLocation, '755');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('electrobun CLI downloaded successfully!');
|
|
132
|
+
return binLocation;
|
|
133
|
+
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(`Failed to download electrobun CLI: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function main() {
|
|
140
|
+
try {
|
|
141
|
+
const args = process.argv.slice(2);
|
|
142
|
+
const cliPath = await ensureCliBinary();
|
|
143
|
+
|
|
144
|
+
// Replace this process with the actual CLI
|
|
145
|
+
const child = spawn(cliPath, args, {
|
|
146
|
+
stdio: 'inherit',
|
|
147
|
+
cwd: process.cwd()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
child.on('exit', (code) => {
|
|
151
|
+
process.exit(code || 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
child.on('error', (error) => {
|
|
155
|
+
console.error('Failed to start electrobun CLI:', error.message);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error:', error.message);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
main();
|
package/debug.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
console.log('process.argv:', process.argv);
|
|
2
|
+
const indexOfElectrobun = process.argv.findIndex((arg) => arg.includes('electrobun'));
|
|
3
|
+
console.log('indexOfElectrobun:', indexOfElectrobun);
|
|
4
|
+
const commandArg = process.argv[indexOfElectrobun + 1] || 'build';
|
|
5
|
+
console.log('commandArg:', commandArg);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// consider just makeing a shared types file
|
|
2
|
+
|
|
3
|
+
export type BuiltinBunToWebviewSchema = {
|
|
4
|
+
requests: {
|
|
5
|
+
evaluateJavascriptWithResponse: {
|
|
6
|
+
params: { script: string };
|
|
7
|
+
response: any;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type BuiltinWebviewToBunSchema = {
|
|
13
|
+
requests: {
|
|
14
|
+
webviewTagInit: {
|
|
15
|
+
params: {};
|
|
16
|
+
response: any;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type RPCSchema,
|
|
3
|
+
type RPCRequestHandler,
|
|
4
|
+
type RPCOptions,
|
|
5
|
+
type RPCMessageHandlerFn,
|
|
6
|
+
type WildcardRPCMessageHandlerFn,
|
|
7
|
+
type RPCTransport,
|
|
8
|
+
createRPC,
|
|
9
|
+
} from "rpc-anywhere";
|
|
10
|
+
import { ConfigureWebviewTags } from "./webviewtag";
|
|
11
|
+
// todo: should this just be injected as a preload script?
|
|
12
|
+
import { isAppRegionDrag } from "./stylesAndElements";
|
|
13
|
+
import type { BuiltinBunToWebviewSchema, BuiltinWebviewToBunSchema } from "./builtinrpcSchema";
|
|
14
|
+
import type { InternalWebviewHandlers, WebviewTagHandlers } from "./rpc/webview";
|
|
15
|
+
|
|
16
|
+
interface ElectrobunWebviewRPCSChema {
|
|
17
|
+
bun: RPCSchema;
|
|
18
|
+
webview: RPCSchema;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const WEBVIEW_ID = window.__electrobunWebviewId;
|
|
22
|
+
const WINDOW_ID = window.__electrobunWindowId;
|
|
23
|
+
const RPC_SOCKET_PORT = window.__electrobunRpcSocketPort;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Electroview<T> {
|
|
27
|
+
bunSocket?: WebSocket;
|
|
28
|
+
// user's custom rpc browser <-> bun
|
|
29
|
+
rpc?: T;
|
|
30
|
+
rpcHandler?: (msg: any) => void;
|
|
31
|
+
// electrobun rpc browser <-> bun
|
|
32
|
+
internalRpc?: any;
|
|
33
|
+
internalRpcHandler?: (msg: any) => void;
|
|
34
|
+
|
|
35
|
+
constructor(config: { rpc: T }) {
|
|
36
|
+
this.rpc = config.rpc;
|
|
37
|
+
this.init();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init() {
|
|
41
|
+
// todo (yoav): should init webviewTag by default when src is local
|
|
42
|
+
// and have a setting that forces it enabled or disabled
|
|
43
|
+
this.initInternalRpc();
|
|
44
|
+
this.initSocketToBun();
|
|
45
|
+
|
|
46
|
+
ConfigureWebviewTags(true, this.internalRpc, this.rpc);
|
|
47
|
+
|
|
48
|
+
this.initElectrobunListeners();
|
|
49
|
+
|
|
50
|
+
window.__electrobun = {
|
|
51
|
+
receiveMessageFromBun: this.receiveMessageFromBun.bind(this),
|
|
52
|
+
receiveInternalMessageFromBun: this.receiveInternalMessageFromBun.bind(this),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (this.rpc) {
|
|
56
|
+
this.rpc.setTransport(this.createTransport());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
initInternalRpc() {
|
|
61
|
+
this.internalRpc = createRPC<WebviewTagHandlers, InternalWebviewHandlers>({
|
|
62
|
+
transport: this.createInternalTransport(),
|
|
63
|
+
// requestHandler: {
|
|
64
|
+
|
|
65
|
+
// },
|
|
66
|
+
maxRequestTime: 1000,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
initSocketToBun() {
|
|
71
|
+
// todo: upgrade to tls
|
|
72
|
+
const socket = new WebSocket(
|
|
73
|
+
`ws://localhost:${RPC_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
this.bunSocket = socket;
|
|
77
|
+
|
|
78
|
+
socket.addEventListener("open", () => {
|
|
79
|
+
// this.bunSocket?.send("Hello from webview " + WEBVIEW_ID);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
socket.addEventListener("message", async (event) => {
|
|
83
|
+
const message = event.data;
|
|
84
|
+
if (typeof message === "string") {
|
|
85
|
+
try {
|
|
86
|
+
const encryptedPacket = JSON.parse(message);
|
|
87
|
+
|
|
88
|
+
const decrypted = await window.__electrobun_decrypt(
|
|
89
|
+
encryptedPacket.encryptedData,
|
|
90
|
+
encryptedPacket.iv,
|
|
91
|
+
encryptedPacket.tag
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
this.rpcHandler?.(JSON.parse(decrypted));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error("Error parsing bun message:", err);
|
|
97
|
+
}
|
|
98
|
+
} else if (message instanceof Blob) {
|
|
99
|
+
// Handle binary data (e.g., convert Blob to ArrayBuffer if needed)
|
|
100
|
+
} else {
|
|
101
|
+
console.error("UNKNOWN DATA TYPE RECEIVED:", event.data);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
socket.addEventListener("error", (event) => {
|
|
106
|
+
console.error("Socket error:", event);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
socket.addEventListener("close", (event) => {
|
|
110
|
+
// console.log("Socket closed:", event);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// This will be attached to the global object, bun can rpc reply by executingJavascript
|
|
115
|
+
// of that global reference to the function
|
|
116
|
+
receiveInternalMessageFromBun(msg: any) {
|
|
117
|
+
if (this.internalRpcHandler) {
|
|
118
|
+
|
|
119
|
+
this.internalRpcHandler(msg);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// TODO: implement proper rpc-anywhere style rpc here
|
|
124
|
+
// todo: this is duplicated in webviewtag.ts and should be DRYed up
|
|
125
|
+
isProcessingQueue = false;
|
|
126
|
+
sendToInternalQueue = [];
|
|
127
|
+
sendToBunInternal(message: {}) {
|
|
128
|
+
try {
|
|
129
|
+
const strMessage = JSON.stringify(message);
|
|
130
|
+
this.sendToInternalQueue.push(strMessage);
|
|
131
|
+
|
|
132
|
+
this.processQueue();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error('failed to send to bun internal', err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
processQueue() {
|
|
139
|
+
const that = this;
|
|
140
|
+
if (that.isProcessingQueue) {
|
|
141
|
+
|
|
142
|
+
// This timeout is just to schedule a retry "later"
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
that.processQueue();
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (that.sendToInternalQueue.length === 0) {
|
|
150
|
+
// that.isProcessingQueue = false;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
that.isProcessingQueue = true;
|
|
155
|
+
|
|
156
|
+
const batchMessage = JSON.stringify(that.sendToInternalQueue);
|
|
157
|
+
that.sendToInternalQueue = [];
|
|
158
|
+
window.__electrobunInternalBridge?.postMessage(batchMessage);
|
|
159
|
+
|
|
160
|
+
// Note: The postmessage handler is routed via native code to a Bun JSCallback.
|
|
161
|
+
// Currently JSCallbacks are somewhat experimental and were designed for a single invocation
|
|
162
|
+
// But we have tons of resize events in this webview's thread that are sent, maybe to main thread
|
|
163
|
+
// and then the JSCallback is invoked on the Bun worker thread. JSCallbacks have a little virtual memory
|
|
164
|
+
// or something that can segfault when called from a thread while the worker(bun) thread is still executing
|
|
165
|
+
// a previous call. The segfaults were really only triggered with multiple <electrobun-webview>s on a page
|
|
166
|
+
// all trying to resize at the same time.
|
|
167
|
+
//
|
|
168
|
+
// To work around this we batch high frequency postMessage calls here with a timeout. While not deterministic hopefully Bun
|
|
169
|
+
// fixes the underlying FFI/JSCallback issue before we have to invest time in a more deterministic solution.
|
|
170
|
+
//
|
|
171
|
+
// On my m4 max a 1ms delay is not long enough to let it complete and can segfault, a 2ms delay is long enough
|
|
172
|
+
// This may be different on slower hardware but not clear if it would need more or less time so leaving this for now
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
that.isProcessingQueue = false;
|
|
175
|
+
}, 2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
initElectrobunListeners() {
|
|
179
|
+
document.addEventListener("mousedown", (e) => {
|
|
180
|
+
if (isAppRegionDrag(e)) {
|
|
181
|
+
this.internalRpc?.send.startWindowMove({ id: WINDOW_ID });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
document.addEventListener("mouseup", (e) => {
|
|
186
|
+
if (isAppRegionDrag(e)) {
|
|
187
|
+
this.internalRpc?.send.stopWindowMove({ id: WINDOW_ID });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
createTransport() {
|
|
193
|
+
const that = this;
|
|
194
|
+
return {
|
|
195
|
+
send(message) {
|
|
196
|
+
try {
|
|
197
|
+
const messageString = JSON.stringify(message);
|
|
198
|
+
// console.log("sending message bunbridge", messageString);
|
|
199
|
+
that.bunBridge(messageString);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error("bun: failed to serialize message to webview", error);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
registerHandler(handler) {
|
|
205
|
+
that.rpcHandler = handler;
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
createInternalTransport(): RPCTransport {
|
|
211
|
+
const that = this;
|
|
212
|
+
return {
|
|
213
|
+
send(message) {
|
|
214
|
+
message.hostWebviewId = WEBVIEW_ID;
|
|
215
|
+
that.sendToBunInternal(message);
|
|
216
|
+
},
|
|
217
|
+
registerHandler(handler) {
|
|
218
|
+
that.internalRpcHandler = handler;
|
|
219
|
+
// webview tag doesn't handle any messages from bun just yet
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async bunBridge(msg: string) {
|
|
225
|
+
if (this.bunSocket?.readyState === WebSocket.OPEN) {
|
|
226
|
+
try {
|
|
227
|
+
const { encryptedData, iv, tag } = await window.__electrobun_encrypt(
|
|
228
|
+
msg
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const encryptedPacket = {
|
|
232
|
+
encryptedData: encryptedData,
|
|
233
|
+
iv: iv,
|
|
234
|
+
tag: tag,
|
|
235
|
+
};
|
|
236
|
+
const encryptedPacketString = JSON.stringify(encryptedPacket);
|
|
237
|
+
this.bunSocket.send(encryptedPacketString);
|
|
238
|
+
return;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error("Error sending message to bun via socket:", error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// if socket's are unavailable, fallback to postMessage
|
|
245
|
+
|
|
246
|
+
// Note: messageHandlers seem to freeze when sending large messages
|
|
247
|
+
// but xhr to views://rpc can run into CORS issues on non views://
|
|
248
|
+
// loaded content (eg: when writing extensions/preload scripts for
|
|
249
|
+
// remote content).
|
|
250
|
+
|
|
251
|
+
// Since most messages--especially those on remote content, are small
|
|
252
|
+
// we can solve most use cases by having a fallback to xhr for
|
|
253
|
+
// large messages
|
|
254
|
+
|
|
255
|
+
// TEMP: disable the fallback for now. for some reason suddenly can't
|
|
256
|
+
// repro now that other places are chunking messages and laptop restart
|
|
257
|
+
|
|
258
|
+
if (true || msg.length < 8 * 1024) {
|
|
259
|
+
window.__electrobunBunBridge?.postMessage(msg);
|
|
260
|
+
} else {
|
|
261
|
+
var xhr = new XMLHttpRequest();
|
|
262
|
+
|
|
263
|
+
// Note: we're only using synchronouse http on this async
|
|
264
|
+
// call to get around CORS for now
|
|
265
|
+
// Note: DO NOT use postMessage handlers since it
|
|
266
|
+
// freezes the process when sending lots of large messages
|
|
267
|
+
|
|
268
|
+
xhr.open("POST", "views://rpc", false); // sychronous call
|
|
269
|
+
xhr.send(msg);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
receiveMessageFromBun(msg) {
|
|
274
|
+
// NOTE: in the webview messages are passed by executing ElectrobunView.receiveMessageFromBun(object)
|
|
275
|
+
// so they're already parsed into an object here
|
|
276
|
+
if (this.rpcHandler) {
|
|
277
|
+
this.rpcHandler(msg);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// todo (yoav): This is mostly just the reverse of the one in BrowserView.ts on the bun side. Should DRY this up.
|
|
281
|
+
static defineRPC<
|
|
282
|
+
Schema extends ElectrobunWebviewRPCSChema,
|
|
283
|
+
BunSchema extends RPCSchema = Schema["bun"],
|
|
284
|
+
WebviewSchema extends RPCSchema = Schema["webview"]
|
|
285
|
+
>(config: {
|
|
286
|
+
maxRequestTime?: number;
|
|
287
|
+
handlers: {
|
|
288
|
+
requests?: RPCRequestHandler<WebviewSchema["requests"]>;
|
|
289
|
+
messages?: {
|
|
290
|
+
[key in keyof WebviewSchema["messages"]]: RPCMessageHandlerFn<
|
|
291
|
+
WebviewSchema["messages"],
|
|
292
|
+
key
|
|
293
|
+
>;
|
|
294
|
+
} & {
|
|
295
|
+
"*"?: WildcardRPCMessageHandlerFn<WebviewSchema["messages"]>;
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
}) {
|
|
299
|
+
// Note: RPC Anywhere requires defining the requests that a schema handles and the messages that a schema sends.
|
|
300
|
+
// eg: BunSchema {
|
|
301
|
+
// requests: // ... requests bun handles, sent by webview
|
|
302
|
+
// messages: // ... messages bun sends, handled by webview
|
|
303
|
+
// }
|
|
304
|
+
// In some generlized contexts that makes sense,
|
|
305
|
+
// In the Electrobun context it can feel a bit counter-intuitive so we swap this around a bit. In Electrobun, the
|
|
306
|
+
// webview and bun are known endpoints so we simplify schema definitions by combining them.
|
|
307
|
+
// Schema {
|
|
308
|
+
// bun: BunSchema {
|
|
309
|
+
// requests: // ... requests bun sends, handled by webview,
|
|
310
|
+
// messages: // ... messages bun sends, handled by webview
|
|
311
|
+
// },
|
|
312
|
+
// webview: WebviewSchema {
|
|
313
|
+
// requests: // ... requests webview sends, handled by bun,
|
|
314
|
+
// messages: // ... messages webview sends, handled by bun
|
|
315
|
+
// },
|
|
316
|
+
// }
|
|
317
|
+
// electrobun also treats messages as "requests that we don't wait for to complete", and normalizes specifying the
|
|
318
|
+
// handlers for them alongside request handlers.
|
|
319
|
+
|
|
320
|
+
const builtinHandlers: {
|
|
321
|
+
requests: RPCRequestHandler<BuiltinBunToWebviewSchema["requests"]>;
|
|
322
|
+
} = {
|
|
323
|
+
requests: {
|
|
324
|
+
evaluateJavascriptWithResponse: ({ script }) => {
|
|
325
|
+
return new Promise((resolve) => {
|
|
326
|
+
try {
|
|
327
|
+
const resultFunction = new Function(script);
|
|
328
|
+
const result = resultFunction();
|
|
329
|
+
|
|
330
|
+
if (result instanceof Promise) {
|
|
331
|
+
result
|
|
332
|
+
.then((resolvedResult) => {
|
|
333
|
+
resolve(resolvedResult);
|
|
334
|
+
})
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
console.error("bun: async script execution failed", error);
|
|
337
|
+
resolve(String(error));
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
resolve(result);
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("bun: failed to eval script", error);
|
|
344
|
+
resolve(String(error));
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
type mixedWebviewSchema = {
|
|
352
|
+
requests: BunSchema["requests"] & BuiltinWebviewToBunSchema["requests"];
|
|
353
|
+
messages: WebviewSchema["messages"];
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
type mixedBunSchema = {
|
|
357
|
+
requests: WebviewSchema["requests"] &
|
|
358
|
+
BuiltinBunToWebviewSchema["requests"];
|
|
359
|
+
messages: BunSchema["messages"];
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const rpcOptions = {
|
|
363
|
+
maxRequestTime: config.maxRequestTime,
|
|
364
|
+
requestHandler: {
|
|
365
|
+
...config.handlers.requests,
|
|
366
|
+
...builtinHandlers.requests,
|
|
367
|
+
},
|
|
368
|
+
transport: {
|
|
369
|
+
// Note: RPC Anywhere will throw if you try add a message listener if transport.registerHandler is falsey
|
|
370
|
+
registerHandler: () => {},
|
|
371
|
+
},
|
|
372
|
+
} as RPCOptions<mixedBunSchema, mixedWebviewSchema>;
|
|
373
|
+
|
|
374
|
+
const rpc = createRPC<mixedBunSchema, mixedWebviewSchema>(rpcOptions);
|
|
375
|
+
|
|
376
|
+
const messageHandlers = config.handlers.messages;
|
|
377
|
+
if (messageHandlers) {
|
|
378
|
+
// note: this can only be done once there is a transport
|
|
379
|
+
// @ts-ignore - this is due to all the schema mixing we're doing, fine to ignore
|
|
380
|
+
// while types in here are borked, they resolve correctly/bubble up to the defineRPC call site.
|
|
381
|
+
rpc.addMessageListener(
|
|
382
|
+
"*",
|
|
383
|
+
(messageName: keyof WebviewSchema["messages"], payload) => {
|
|
384
|
+
const globalHandler = messageHandlers["*"];
|
|
385
|
+
if (globalHandler) {
|
|
386
|
+
globalHandler(messageName, payload);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const messageHandler = messageHandlers[messageName];
|
|
390
|
+
if (messageHandler) {
|
|
391
|
+
messageHandler(payload);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return rpc;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export { type RPCSchema, createRPC, Electroview };
|
|
402
|
+
|
|
403
|
+
const Electrobun = {
|
|
404
|
+
Electroview,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export default Electrobun;
|
|
408
|
+
|
|
409
|
+
|