@swiftbrowser/native 0.0.0-stage → 0.10.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/README.md +143 -2
- package/bin/swiftbrowser-native.mjs +49 -0
- package/dist/bridge.d.ts +50 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +10 -0
- package/dist/bridge.js.map +1 -0
- package/dist/build.d.ts +47 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +156 -0
- package/dist/build.js.map +1 -0
- package/dist/devBuild.d.ts +34 -0
- package/dist/devBuild.d.ts.map +1 -0
- package/dist/devBuild.js +133 -0
- package/dist/devBuild.js.map +1 -0
- package/dist/encoding.d.ts +32 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +136 -0
- package/dist/encoding.js.map +1 -0
- package/dist/fetch.d.ts +117 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +272 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +19 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +17 -0
- package/dist/native.js.map +1 -0
- package/dist/refresh.d.ts +18 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +18 -0
- package/dist/refresh.js.map +1 -0
- package/dist/run.d.ts +27 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +76 -0
- package/dist/run.js.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +193 -0
- package/dist/runtime.js.map +1 -0
- package/dist/stubs/react-dom-client.d.ts +8 -0
- package/dist/stubs/react-dom-client.d.ts.map +1 -0
- package/dist/stubs/react-dom-client.js +11 -0
- package/dist/stubs/react-dom-client.js.map +1 -0
- package/dist/stubs/react-dom.d.ts +9 -0
- package/dist/stubs/react-dom.d.ts.map +1 -0
- package/dist/stubs/react-dom.js +13 -0
- package/dist/stubs/react-dom.js.map +1 -0
- package/dist/url.d.ts +48 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +213 -0
- package/dist/url.js.map +1 -0
- package/dist/vendor.d.ts +11 -0
- package/dist/vendor.d.ts.map +1 -0
- package/dist/vendor.js +27 -0
- package/dist/vendor.js.map +1 -0
- package/dist/wasmHost.d.ts +35 -0
- package/dist/wasmHost.d.ts.map +1 -0
- package/dist/wasmHost.js +120 -0
- package/dist/wasmHost.js.map +1 -0
- package/package.json +73 -4
- package/src/bridge.ts +58 -0
- package/src/build.ts +185 -0
- package/src/devBuild.ts +152 -0
- package/src/encoding.ts +130 -0
- package/src/fetch.ts +305 -0
- package/src/index.ts +20 -0
- package/src/native.ts +35 -0
- package/src/refresh.ts +29 -0
- package/src/run.ts +98 -0
- package/src/runtime.ts +208 -0
- package/src/stubs/react-dom-client.ts +17 -0
- package/src/stubs/react-dom.ts +15 -0
- package/src/types/react-refresh.d.ts +38 -0
- package/src/url.ts +202 -0
- package/src/vendor.ts +30 -0
- package/src/wasmHost.ts +147 -0
- package/swift/Package.swift +31 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +228 -0
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +42 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +102 -0
- package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +25 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebHost.swift +138 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +251 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +53 -0
- package/swift/Sources/SwiftBrowserNative/SwiftBrowserNative.swift +6 -0
- package/swift/Sources/SwiftBrowserNative/Views/Conversions.swift +361 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +169 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +268 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +175 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +196 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +246 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +108 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +189 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +102 -0
- package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +190 -0
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +182 -0
- package/swift/Sources/SwiftBrowserProtocol/JSONValue.swift +268 -0
- package/swift/Sources/SwiftBrowserProtocol/Op.swift +100 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +469 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +117 -0
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +148 -0
- package/swift/Sources/SwiftBrowserProtocol/Values.swift +279 -0
- package/swift/Sources/SwiftBrowserTree/ElementNode.swift +65 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +168 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// swift-tools-version: 6.0
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
// SwiftBrowser Native: the render ops of docs/ops-protocol.md drawn by real
|
|
5
|
+
// SwiftUI, for apps whose logic is JavaScript running in JavaScriptCore
|
|
6
|
+
// (docs/native.md). A separate package from the repository root's: that one
|
|
7
|
+
// has a target named `SwiftUI` and is only ever built for Wasm and Linux.
|
|
8
|
+
//
|
|
9
|
+
// Targets, from the bottom up:
|
|
10
|
+
//
|
|
11
|
+
// - SwiftBrowserProtocol: the ops, events and typed props (Foundation only).
|
|
12
|
+
// - SwiftBrowserTree: the live element tree the ops mutate (Observation).
|
|
13
|
+
// Both build and test on Linux, so CI without a Mac covers them.
|
|
14
|
+
// - SwiftBrowserNative: the SwiftUI views for each element kind, the
|
|
15
|
+
// JavaScriptCore host and the app runtime. Apple platforms only; every
|
|
16
|
+
// file is wrapped in `#if canImport(SwiftUI)` so the package still
|
|
17
|
+
// resolves and tests elsewhere.
|
|
18
|
+
let package = Package(
|
|
19
|
+
name: "SwiftBrowserNative",
|
|
20
|
+
platforms: [.iOS(.v17), .macOS(.v14)],
|
|
21
|
+
products: [
|
|
22
|
+
.library(name: "SwiftBrowserNative", targets: ["SwiftBrowserNative"]),
|
|
23
|
+
.library(name: "SwiftBrowserProtocol", targets: ["SwiftBrowserProtocol"]),
|
|
24
|
+
.library(name: "SwiftBrowserTree", targets: ["SwiftBrowserTree"]),
|
|
25
|
+
],
|
|
26
|
+
targets: [
|
|
27
|
+
.target(name: "SwiftBrowserProtocol"),
|
|
28
|
+
.target(name: "SwiftBrowserTree", dependencies: ["SwiftBrowserProtocol"]),
|
|
29
|
+
.target(name: "SwiftBrowserNative", dependencies: ["SwiftBrowserProtocol", "SwiftBrowserTree"]),
|
|
30
|
+
]
|
|
31
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import Foundation
|
|
3
|
+
import os
|
|
4
|
+
import SwiftBrowserProtocol
|
|
5
|
+
import SwiftBrowserTree
|
|
6
|
+
#if canImport(UIKit)
|
|
7
|
+
import UIKit
|
|
8
|
+
#elseif canImport(AppKit)
|
|
9
|
+
import AppKit
|
|
10
|
+
#endif
|
|
11
|
+
#if canImport(FoundationBridge)
|
|
12
|
+
import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
/// The native primitives `@swiftbrowser/native` builds the web globals from
|
|
16
|
+
/// (packages/native/src/runtime.ts), installed on `__sbNative`:
|
|
17
|
+
///
|
|
18
|
+
/// | function | what it does |
|
|
19
|
+
/// |---|---|
|
|
20
|
+
/// | `applyOps(json)` | applies a pass's ops to the tree, synchronously |
|
|
21
|
+
/// | `log(level, message)` | the console |
|
|
22
|
+
/// | `now()` | milliseconds, monotonic |
|
|
23
|
+
/// | `setTimer(id, delayMs, repeats)`, `clearTimer(id)` | timers on the main run loop; fire `__sbTimerFired(id)` |
|
|
24
|
+
/// | `fetch(id, requestJSON)` | `URLSession`; answers `__sbFetchResult(id, resultJSON)` |
|
|
25
|
+
/// | `storageGet/Set/Remove/Keys/Clear` | `localStorage` over `UserDefaults` |
|
|
26
|
+
/// | `randomBytes(count)` | base64 of random bytes (`crypto.getRandomValues`) |
|
|
27
|
+
/// | `openURL(url)` | the system opener |
|
|
28
|
+
/// | `locale()` | the viewer's locale identifier |
|
|
29
|
+
/// | `launchArguments()` | the process's arguments (`-screen list` from a scheme or `simctl launch`) |
|
|
30
|
+
///
|
|
31
|
+
/// Everything JavaScript-facing runs on the main thread; work that is not
|
|
32
|
+
/// (a fetch) hops back before calling in.
|
|
33
|
+
@MainActor
|
|
34
|
+
final class HostBridge {
|
|
35
|
+
private let host: JSHost
|
|
36
|
+
private let tree: OpTree
|
|
37
|
+
private var timers: [Int: Timer] = [:]
|
|
38
|
+
private let logger = Logger(subsystem: "swiftbrowser", category: "js")
|
|
39
|
+
private let session = URLSession(configuration: .default)
|
|
40
|
+
/// Keys of `localStorage` live under this prefix in `UserDefaults`.
|
|
41
|
+
static let storagePrefix = "swiftbrowser.storage."
|
|
42
|
+
|
|
43
|
+
init(host: JSHost, tree: OpTree) {
|
|
44
|
+
self.host = host
|
|
45
|
+
self.tree = tree
|
|
46
|
+
installAll()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Stops every timer; the app that owned the bridge is going away.
|
|
50
|
+
func invalidate() {
|
|
51
|
+
for timer in timers.values { timer.invalidate() }
|
|
52
|
+
timers.removeAll()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func installAll() {
|
|
56
|
+
host.install("applyOps") { [tree] arguments in
|
|
57
|
+
guard let json = arguments.first?.string else { return .string("applyOps: expected a JSON string") }
|
|
58
|
+
do {
|
|
59
|
+
try tree.apply(json: json)
|
|
60
|
+
return nil
|
|
61
|
+
} catch {
|
|
62
|
+
return .string("\(error)")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
host.install("log") { [logger] arguments in
|
|
67
|
+
let level = arguments.first?.string ?? "log"
|
|
68
|
+
let message = arguments.dropFirst().map { $0.string ?? $0.json }.joined(separator: " ")
|
|
69
|
+
switch level {
|
|
70
|
+
case "error": logger.error("\(message, privacy: .public)")
|
|
71
|
+
case "warn": logger.warning("\(message, privacy: .public)")
|
|
72
|
+
case "debug": logger.debug("\(message, privacy: .public)")
|
|
73
|
+
default: logger.info("\(message, privacy: .public)")
|
|
74
|
+
}
|
|
75
|
+
#if DEBUG
|
|
76
|
+
print("[js:\(level)] \(message)")
|
|
77
|
+
#endif
|
|
78
|
+
return nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
host.install("now") { _ in .number(ProcessInfo.processInfo.systemUptime * 1000) }
|
|
82
|
+
|
|
83
|
+
host.install("setTimer") { [weak self] arguments in
|
|
84
|
+
guard let self, let id = arguments[safe: 0]?.int else { return nil }
|
|
85
|
+
let delay = max(0, (arguments[safe: 1]?.double ?? 0) / 1000)
|
|
86
|
+
let repeats = arguments[safe: 2]?.bool ?? false
|
|
87
|
+
self.timers[id]?.invalidate()
|
|
88
|
+
let fire: @Sendable (Timer) -> Void = { [weak self] _ in
|
|
89
|
+
// Scheduled on the main run loop, so it fires on the main thread.
|
|
90
|
+
MainActor.assumeIsolated {
|
|
91
|
+
guard let self else { return }
|
|
92
|
+
if !repeats { self.timers[id] = nil }
|
|
93
|
+
self.callIn("__sbTimerFired", [.number(Double(id))])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
#if canImport(FoundationBridge)
|
|
97
|
+
let timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: repeats, block: fire)
|
|
98
|
+
#else
|
|
99
|
+
let timer = Timer(timeInterval: delay, repeats: repeats, block: fire)
|
|
100
|
+
// `.common` so timers keep firing while a scroll view tracks.
|
|
101
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
102
|
+
#endif
|
|
103
|
+
self.timers[id] = timer
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
host.install("clearTimer") { [weak self] arguments in
|
|
108
|
+
guard let self, let id = arguments[safe: 0]?.int else { return nil }
|
|
109
|
+
self.timers[id]?.invalidate()
|
|
110
|
+
self.timers[id] = nil
|
|
111
|
+
return nil
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
host.install("fetch") { [weak self] arguments in
|
|
115
|
+
guard let self, let id = arguments[safe: 0]?.int else { return nil }
|
|
116
|
+
let request = arguments[safe: 1] ?? .null
|
|
117
|
+
self.fetch(id: id, request: request)
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
host.install("storageGet") { arguments in
|
|
122
|
+
guard let key = arguments.first?.string else { return .null }
|
|
123
|
+
return UserDefaults.standard.string(forKey: HostBridge.storagePrefix + key).map(JSONValue.string) ?? .null
|
|
124
|
+
}
|
|
125
|
+
host.install("storageSet") { arguments in
|
|
126
|
+
guard let key = arguments[safe: 0]?.string else { return nil }
|
|
127
|
+
UserDefaults.standard.set(arguments[safe: 1]?.string ?? "", forKey: HostBridge.storagePrefix + key)
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
host.install("storageRemove") { arguments in
|
|
131
|
+
guard let key = arguments.first?.string else { return nil }
|
|
132
|
+
UserDefaults.standard.removeObject(forKey: HostBridge.storagePrefix + key)
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
host.install("storageKeys") { _ in
|
|
136
|
+
let keys = UserDefaults.standard.dictionaryRepresentation().keys
|
|
137
|
+
.filter { $0.hasPrefix(HostBridge.storagePrefix) }
|
|
138
|
+
.map { String($0.dropFirst(HostBridge.storagePrefix.count)) }
|
|
139
|
+
.sorted()
|
|
140
|
+
return .array(keys.map(JSONValue.string))
|
|
141
|
+
}
|
|
142
|
+
host.install("storageClear") { _ in
|
|
143
|
+
for key in UserDefaults.standard.dictionaryRepresentation().keys where key.hasPrefix(HostBridge.storagePrefix) {
|
|
144
|
+
UserDefaults.standard.removeObject(forKey: key)
|
|
145
|
+
}
|
|
146
|
+
return nil
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
host.install("randomBytes") { arguments in
|
|
150
|
+
let count = max(0, min(arguments.first?.int ?? 0, 65536))
|
|
151
|
+
var generator = SystemRandomNumberGenerator()
|
|
152
|
+
let bytes = (0..<count).map { _ in UInt8.random(in: .min ... .max, using: &generator) }
|
|
153
|
+
return .string(Data(bytes).base64EncodedString())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
host.install("openURL") { arguments in
|
|
157
|
+
guard let string = arguments.first?.string, let url = URL(string: string) else { return .bool(false) }
|
|
158
|
+
#if canImport(UIKit)
|
|
159
|
+
UIApplication.shared.open(url)
|
|
160
|
+
#elseif canImport(AppKit)
|
|
161
|
+
NSWorkspace.shared.open(url)
|
|
162
|
+
#endif
|
|
163
|
+
return .bool(true)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
host.install("locale") { _ in .string(Locale.current.identifier) }
|
|
167
|
+
|
|
168
|
+
host.install("launchArguments") { _ in .array(ProcessInfo.processInfo.arguments.map(JSONValue.string)) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Calls into JavaScript from a timer or a completion, where a throw has nowhere to go but the log.
|
|
172
|
+
private func callIn(_ function: String, _ arguments: [JSONValue]) {
|
|
173
|
+
do {
|
|
174
|
+
try host.call(function, arguments)
|
|
175
|
+
} catch {
|
|
176
|
+
logger.error("\(function): \(String(describing: error), privacy: .public)")
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MARK: fetch
|
|
181
|
+
|
|
182
|
+
/// `{ url, method, headers: {name: value}, body: base64 | null }` in,
|
|
183
|
+
/// `{ status, statusText, headers, url, body: base64 }` or `{ error }` out.
|
|
184
|
+
private func fetch(id: Int, request: JSONValue) {
|
|
185
|
+
guard let urlString = request["url"].string, let url = URL(string: urlString) else {
|
|
186
|
+
callIn("__sbFetchResult", [.number(Double(id)), .object(["error": .string("invalid URL")])])
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
var urlRequest = URLRequest(url: url)
|
|
190
|
+
urlRequest.httpMethod = request["method"].string ?? "GET"
|
|
191
|
+
for (name, value) in request["headers"].object ?? [:] {
|
|
192
|
+
if let value = value.string { urlRequest.setValue(value, forHTTPHeaderField: name) }
|
|
193
|
+
}
|
|
194
|
+
if let body = request["body"].string, let data = Data(base64Encoded: body) {
|
|
195
|
+
urlRequest.httpBody = data
|
|
196
|
+
}
|
|
197
|
+
let task = session.dataTask(with: urlRequest) { [weak self] data, response, error in
|
|
198
|
+
let result: JSONValue
|
|
199
|
+
if let error {
|
|
200
|
+
result = .object(["error": .string(error.localizedDescription)])
|
|
201
|
+
} else {
|
|
202
|
+
let http = response as? HTTPURLResponse
|
|
203
|
+
var headers: JSONObject = [:]
|
|
204
|
+
for (name, value) in http?.allHeaderFields ?? [:] {
|
|
205
|
+
headers[String(describing: name).lowercased()] = .string(String(describing: value))
|
|
206
|
+
}
|
|
207
|
+
result = .object([
|
|
208
|
+
"status": .number(Double(http?.statusCode ?? 200)),
|
|
209
|
+
"statusText": .string(http.map { HTTPURLResponse.localizedString(forStatusCode: $0.statusCode) } ?? ""),
|
|
210
|
+
"headers": .object(headers),
|
|
211
|
+
"url": .string(response?.url?.absoluteString ?? urlString),
|
|
212
|
+
"body": .string((data ?? Data()).base64EncodedString()),
|
|
213
|
+
])
|
|
214
|
+
}
|
|
215
|
+
DispatchQueue.main.async { [weak self] in
|
|
216
|
+
MainActor.assumeIsolated {
|
|
217
|
+
self?.callIn("__sbFetchResult", [.number(Double(id)), result])
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
task.resume()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
extension Array {
|
|
226
|
+
subscript(safe index: Int) -> Element? { indices.contains(index) ? self[index] : nil }
|
|
227
|
+
}
|
|
228
|
+
#endif
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftBrowserProtocol
|
|
4
|
+
|
|
5
|
+
/// A JavaScript engine as the runtime needs it: evaluate a script, give it
|
|
6
|
+
/// native functions, call its functions. Narrow on purpose, so JavaScriptCore
|
|
7
|
+
/// (the implementation here) can be swapped for Hermes or QuickJS behind it
|
|
8
|
+
/// (docs/native.md, "The JavaScript engine").
|
|
9
|
+
///
|
|
10
|
+
/// Everything is synchronous and on the main thread: a `call` returns after
|
|
11
|
+
/// the JavaScript ran, including any native functions it called in turn.
|
|
12
|
+
@MainActor
|
|
13
|
+
public protocol JSHost: AnyObject {
|
|
14
|
+
/// Runs a script at top level. `name` labels it in stack traces.
|
|
15
|
+
func evaluate(_ script: String, name: String) throws
|
|
16
|
+
|
|
17
|
+
/// Installs a native function under `name` on the `__sbNative` object
|
|
18
|
+
/// JavaScript sees. Arguments arrive as JSON values; the result, when not
|
|
19
|
+
/// nil, goes back as one.
|
|
20
|
+
func install(_ name: String, _ body: @escaping ([JSONValue]) -> JSONValue?)
|
|
21
|
+
|
|
22
|
+
/// Calls a global function with the arguments and returns what it returned.
|
|
23
|
+
@discardableResult
|
|
24
|
+
func call(_ function: String, _ arguments: [JSONValue]) throws -> JSONValue
|
|
25
|
+
|
|
26
|
+
/// Whether a global of this name is defined.
|
|
27
|
+
func hasGlobal(_ name: String) -> Bool
|
|
28
|
+
|
|
29
|
+
/// Throws the loaded scripts away: a fresh global object for the next `evaluate`.
|
|
30
|
+
func reset()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public struct JSError: Error, CustomStringConvertible {
|
|
34
|
+
public let message: String
|
|
35
|
+
public let stack: String?
|
|
36
|
+
public init(message: String, stack: String? = nil) {
|
|
37
|
+
self.message = message
|
|
38
|
+
self.stack = stack
|
|
39
|
+
}
|
|
40
|
+
public var description: String { stack.map { "\(message)\n\($0)" } ?? message }
|
|
41
|
+
}
|
|
42
|
+
#endif
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && canImport(JavaScriptCore)
|
|
2
|
+
import Foundation
|
|
3
|
+
import JavaScriptCore
|
|
4
|
+
import SwiftBrowserProtocol
|
|
5
|
+
|
|
6
|
+
/// `JSHost` over JavaScriptCore's `JSContext`. It ships in the OS, costs no
|
|
7
|
+
/// binary size, and its API is synchronous from Swift with no C++ between:
|
|
8
|
+
/// a block installed with `install` runs on the thread that called into
|
|
9
|
+
/// JavaScript, and `call` returns when the JavaScript did.
|
|
10
|
+
///
|
|
11
|
+
/// Promise jobs (microtasks) run when the outermost call into the context
|
|
12
|
+
/// returns, so a `setState` inside a `then` reaches its render pass before
|
|
13
|
+
/// the native side continues.
|
|
14
|
+
@MainActor
|
|
15
|
+
public final class JavaScriptCoreHost: JSHost {
|
|
16
|
+
public private(set) var context: JSContext
|
|
17
|
+
private var native: JSValue
|
|
18
|
+
/// The last uncaught exception, taken by the `call` or `evaluate` it interrupted.
|
|
19
|
+
private var pendingError: JSError?
|
|
20
|
+
/// Called for every uncaught exception, before the call that caused it throws.
|
|
21
|
+
public var onError: ((JSError) -> Void)?
|
|
22
|
+
|
|
23
|
+
public init() {
|
|
24
|
+
(context, native) = JavaScriptCoreHost.makeContext()
|
|
25
|
+
installExceptionHandler()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static func makeContext() -> (JSContext, JSValue) {
|
|
29
|
+
let context = JSContext()!
|
|
30
|
+
context.name = "SwiftBrowser"
|
|
31
|
+
let native = JSValue(newObjectIn: context)!
|
|
32
|
+
context.setObject(native, forKeyedSubscript: "__sbNative" as NSString)
|
|
33
|
+
return (context, native)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func installExceptionHandler() {
|
|
37
|
+
context.exceptionHandler = { [weak self] _, exception in
|
|
38
|
+
guard let self else { return }
|
|
39
|
+
let message = exception?.toString() ?? "unknown error"
|
|
40
|
+
let stack = exception?.objectForKeyedSubscript("stack")?.toString()
|
|
41
|
+
let error = JSError(message: message, stack: stack == "undefined" ? nil : stack)
|
|
42
|
+
self.pendingError = error
|
|
43
|
+
self.onError?(error)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func reset() {
|
|
48
|
+
context.exceptionHandler = nil
|
|
49
|
+
(context, native) = JavaScriptCoreHost.makeContext()
|
|
50
|
+
pendingError = nil
|
|
51
|
+
installExceptionHandler()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public func evaluate(_ script: String, name: String) throws {
|
|
55
|
+
pendingError = nil
|
|
56
|
+
context.evaluateScript(script, withSourceURL: URL(string: "sb://\(name)"))
|
|
57
|
+
if let error = pendingError {
|
|
58
|
+
pendingError = nil
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public func install(_ name: String, _ body: @escaping ([JSONValue]) -> JSONValue?) {
|
|
64
|
+
// A block with no declared parameters reads its arguments through the
|
|
65
|
+
// context, so one shape serves every native function.
|
|
66
|
+
let block: @convention(block) () -> Any? = {
|
|
67
|
+
let arguments = (JSContext.currentArguments() as? [JSValue]) ?? []
|
|
68
|
+
return body(arguments.map(JavaScriptCoreHost.json(from:)))?.anyObject
|
|
69
|
+
}
|
|
70
|
+
native.setObject(unsafeBitCast(block, to: AnyObject.self), forKeyedSubscript: name as NSString)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@discardableResult
|
|
74
|
+
public func call(_ function: String, _ arguments: [JSONValue]) throws -> JSONValue {
|
|
75
|
+
guard let callee = context.objectForKeyedSubscript(function), !callee.isUndefined else {
|
|
76
|
+
throw JSError(message: "\(function) is not defined")
|
|
77
|
+
}
|
|
78
|
+
pendingError = nil
|
|
79
|
+
let result = callee.call(withArguments: arguments.map(\.anyObject))
|
|
80
|
+
if let error = pendingError {
|
|
81
|
+
pendingError = nil
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
return result.map(JavaScriptCoreHost.json(from:)) ?? .null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public func hasGlobal(_ name: String) -> Bool {
|
|
88
|
+
guard let value = context.objectForKeyedSubscript(name) else { return false }
|
|
89
|
+
return !value.isUndefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// A JavaScript value as JSON. Strings stay strings without a copy
|
|
93
|
+
/// through `toObject`, the common case (the ops of a pass, an event).
|
|
94
|
+
static func json(from value: JSValue) -> JSONValue {
|
|
95
|
+
if value.isUndefined || value.isNull { return .null }
|
|
96
|
+
if value.isString { return .string(value.toString()) }
|
|
97
|
+
if value.isBoolean { return .bool(value.toBool()) }
|
|
98
|
+
if value.isNumber { return .number(value.toDouble()) }
|
|
99
|
+
return JSONValue(any: value.toObject())
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
#endif
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#if canImport(FoundationBridge)
|
|
2
|
+
// Built against the SwiftBrowser shim (the round trip, docs/native.md): the
|
|
3
|
+
// shim and Foundation both declare these names, and the CLI's staging settles
|
|
4
|
+
// them for an app with the same aliases (packages/cli/src/appSources.ts).
|
|
5
|
+
// Geometry is the shim's; what the SDK's Foundation lacks on WASI is the bridge's.
|
|
6
|
+
// Public, as `NativeApp.load(resource:extension:bundle:)` takes a `Bundle`.
|
|
7
|
+
import Foundation
|
|
8
|
+
public import FoundationBridge
|
|
9
|
+
public import SwiftUI
|
|
10
|
+
|
|
11
|
+
public typealias CGFloat = SwiftUI.CGFloat
|
|
12
|
+
public typealias CGPoint = SwiftUI.CGPoint
|
|
13
|
+
public typealias CGSize = SwiftUI.CGSize
|
|
14
|
+
public typealias CGRect = SwiftUI.CGRect
|
|
15
|
+
public typealias Timer = FoundationBridge.Timer
|
|
16
|
+
public typealias RunLoop = FoundationBridge.RunLoop
|
|
17
|
+
public typealias UserDefaults = FoundationBridge.UserDefaults
|
|
18
|
+
public typealias Bundle = FoundationBridge.Bundle
|
|
19
|
+
public typealias URLSession = FoundationBridge.URLSession
|
|
20
|
+
public typealias URLSessionConfiguration = FoundationBridge.URLSessionConfiguration
|
|
21
|
+
public typealias URLRequest = FoundationBridge.URLRequest
|
|
22
|
+
public typealias URLResponse = FoundationBridge.URLResponse
|
|
23
|
+
public typealias HTTPURLResponse = FoundationBridge.HTTPURLResponse
|
|
24
|
+
public typealias DispatchQueue = FoundationBridge.DispatchQueue
|
|
25
|
+
#endif
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && arch(wasm32)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftBrowserProtocol
|
|
4
|
+
|
|
5
|
+
// The host's JavaScript as the engine (docs/native.md, "The round trip"):
|
|
6
|
+
// one WebAssembly import, `sb_native.request`, carries every request to the
|
|
7
|
+
// page or Node (packages/native/src/wasmHost.ts answers it), and the export
|
|
8
|
+
// `sb_native_invoke` brings the installed native functions' calls back.
|
|
9
|
+
// Strings cross as UTF-8 in linear memory: Swift's own for a request, and
|
|
10
|
+
// for an answer a buffer the host took from `sb_alloc` holding a 4-byte
|
|
11
|
+
// little-endian length and then the bytes, which Swift frees.
|
|
12
|
+
|
|
13
|
+
@_extern(wasm, module: "sb_native", name: "request")
|
|
14
|
+
@_extern(c)
|
|
15
|
+
private func sb_native_request(
|
|
16
|
+
_ kind: UnsafePointer<UInt8>, _ kindLength: Int32,
|
|
17
|
+
_ a: UnsafePointer<UInt8>, _ aLength: Int32,
|
|
18
|
+
_ b: UnsafePointer<UInt8>, _ bLength: Int32
|
|
19
|
+
) -> Int32
|
|
20
|
+
|
|
21
|
+
/// `JSHost` over the JavaScript engine the module runs in: the browser's, or
|
|
22
|
+
/// Node's under scripts/run-wasi.mjs. `evaluate` runs the script at the
|
|
23
|
+
/// host's top level (an indirect `eval`), so the bundle's globals are the
|
|
24
|
+
/// page's; `reset` drops the globals a bundle defines and starts `__sbNative`
|
|
25
|
+
/// over, which is as fresh as one realm gets.
|
|
26
|
+
@MainActor
|
|
27
|
+
public final class WebHost: JSHost {
|
|
28
|
+
/// The functions `install` registered, called back through `sb_native_invoke`.
|
|
29
|
+
private var functions: [String: ([JSONValue]) -> JSONValue?] = [:]
|
|
30
|
+
/// The host that answers `sb_native_invoke`: one engine, one host.
|
|
31
|
+
nonisolated(unsafe) static var current: WebHost?
|
|
32
|
+
|
|
33
|
+
public init() {
|
|
34
|
+
WebHost.current = self
|
|
35
|
+
_ = request("reset", "", "")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public func evaluate(_ script: String, name: String) throws {
|
|
39
|
+
if let error = request("eval", script, name)["error"].string {
|
|
40
|
+
throw JSError(message: error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public func install(_ name: String, _ body: @escaping ([JSONValue]) -> JSONValue?) {
|
|
45
|
+
functions[name] = body
|
|
46
|
+
_ = request("install", name, "")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@discardableResult
|
|
50
|
+
public func call(_ function: String, _ arguments: [JSONValue]) throws -> JSONValue {
|
|
51
|
+
let answer = request("call", function, JSONValue.array(arguments).json)
|
|
52
|
+
if let error = answer["error"].string { throw JSError(message: error) }
|
|
53
|
+
return answer["ok"]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public func hasGlobal(_ name: String) -> Bool {
|
|
57
|
+
request("has", name, "")["ok"].bool ?? false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public func reset() {
|
|
61
|
+
_ = request("reset", "", "")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Runs an installed function for `sb_native_invoke`.
|
|
65
|
+
func invoke(_ name: String, _ arguments: [JSONValue]) -> JSONValue? {
|
|
66
|
+
guard let function = functions[name] else { return .object(["error": .string("\(name) is not installed")]) }
|
|
67
|
+
return function(arguments)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// The host's answer, `{"ok": value}` or `{"error": message}`, or `.null` for none.
|
|
71
|
+
private func request(_ kind: String, _ a: String, _ b: String) -> JSONValue {
|
|
72
|
+
var kind = kind, a = a, b = b
|
|
73
|
+
let pointer = kind.withUTF8 { kindBytes in
|
|
74
|
+
a.withUTF8 { aBytes in
|
|
75
|
+
b.withUTF8 { bBytes in
|
|
76
|
+
sb_native_request(
|
|
77
|
+
kindBytes.baseAddress ?? UnsafePointer(bitPattern: 1)!, Int32(kindBytes.count),
|
|
78
|
+
aBytes.baseAddress ?? UnsafePointer(bitPattern: 1)!, Int32(aBytes.count),
|
|
79
|
+
bBytes.baseAddress ?? UnsafePointer(bitPattern: 1)!, Int32(bBytes.count)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
guard let text = WebHost.takeBuffer(pointer) else { return .null }
|
|
85
|
+
return (try? JSONValue.parse(text)) ?? .object(["error": .string("the host answered with malformed JSON: \(text)")])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Reads and frees a buffer the host allocated with `sb_alloc`: a 4-byte length, then UTF-8.
|
|
89
|
+
static func takeBuffer(_ pointer: Int32) -> String? {
|
|
90
|
+
guard pointer != 0, let base = UnsafeMutablePointer<UInt8>(bitPattern: Int(pointer)) else { return nil }
|
|
91
|
+
let length = Int(UInt32(base[0]) | UInt32(base[1]) << 8 | UInt32(base[2]) << 16 | UInt32(base[3]) << 24)
|
|
92
|
+
let text = String(decoding: UnsafeBufferPointer(start: base + 4, count: length), as: UTF8.self)
|
|
93
|
+
base.deallocate()
|
|
94
|
+
return text
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A buffer in the same layout for the host to read and free with `sb_native_free`.
|
|
98
|
+
static func makeBuffer(_ text: String) -> Int32 {
|
|
99
|
+
var text = text
|
|
100
|
+
return text.withUTF8 { bytes in
|
|
101
|
+
let base = UnsafeMutablePointer<UInt8>.allocate(capacity: bytes.count + 4)
|
|
102
|
+
let length = UInt32(bytes.count)
|
|
103
|
+
base[0] = UInt8(length & 0xff)
|
|
104
|
+
base[1] = UInt8((length >> 8) & 0xff)
|
|
105
|
+
base[2] = UInt8((length >> 16) & 0xff)
|
|
106
|
+
base[3] = UInt8((length >> 24) & 0xff)
|
|
107
|
+
if let source = bytes.baseAddress { (base + 4).update(from: source, count: bytes.count) }
|
|
108
|
+
return Int32(Int(bitPattern: base))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// JavaScript calling an installed native function: `name` and the JSON
|
|
114
|
+
/// array of arguments in buffers from `sb_alloc` (freed here). Answers 0
|
|
115
|
+
/// for no result, else a buffer (`WebHost.makeBuffer`) holding the result's JSON.
|
|
116
|
+
@_cdecl("sb_native_invoke")
|
|
117
|
+
public func _sb_native_invoke(_ name: Int32, _ nameLength: Int32, _ arguments: Int32, _ argumentsLength: Int32) -> Int32 {
|
|
118
|
+
guard let nameBase = UnsafeMutablePointer<UInt8>(bitPattern: Int(name)),
|
|
119
|
+
let argumentsBase = UnsafeMutablePointer<UInt8>(bitPattern: Int(arguments))
|
|
120
|
+
else { return 0 }
|
|
121
|
+
let functionName = String(decoding: UnsafeBufferPointer(start: nameBase, count: Int(nameLength)), as: UTF8.self)
|
|
122
|
+
let argumentsText = String(decoding: UnsafeBufferPointer(start: argumentsBase, count: Int(argumentsLength)), as: UTF8.self)
|
|
123
|
+
nameBase.deallocate()
|
|
124
|
+
argumentsBase.deallocate()
|
|
125
|
+
return MainActor.assumeIsolated {
|
|
126
|
+
guard let host = WebHost.current else { return 0 }
|
|
127
|
+
let parsed = (try? JSONValue.parse(argumentsText))?.array ?? []
|
|
128
|
+
guard let result = host.invoke(functionName, parsed) else { return 0 }
|
|
129
|
+
return WebHost.makeBuffer(result.json)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Frees a buffer `sb_native_invoke` answered with.
|
|
134
|
+
@_cdecl("sb_native_free")
|
|
135
|
+
public func _sb_native_free(_ pointer: Int32) {
|
|
136
|
+
UnsafeMutablePointer<UInt8>(bitPattern: Int(pointer))?.deallocate()
|
|
137
|
+
}
|
|
138
|
+
#endif
|