@swiftbrowser/native 0.0.0-stage → 0.10.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.
Files changed (107) hide show
  1. package/README.md +143 -2
  2. package/bin/swiftbrowser-native.mjs +49 -0
  3. package/dist/bridge.d.ts +50 -0
  4. package/dist/bridge.d.ts.map +1 -0
  5. package/dist/bridge.js +10 -0
  6. package/dist/bridge.js.map +1 -0
  7. package/dist/build.d.ts +47 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +156 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/devBuild.d.ts +34 -0
  12. package/dist/devBuild.d.ts.map +1 -0
  13. package/dist/devBuild.js +133 -0
  14. package/dist/devBuild.js.map +1 -0
  15. package/dist/encoding.d.ts +32 -0
  16. package/dist/encoding.d.ts.map +1 -0
  17. package/dist/encoding.js +136 -0
  18. package/dist/encoding.js.map +1 -0
  19. package/dist/fetch.d.ts +117 -0
  20. package/dist/fetch.d.ts.map +1 -0
  21. package/dist/fetch.js +272 -0
  22. package/dist/fetch.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/native.d.ts +19 -0
  28. package/dist/native.d.ts.map +1 -0
  29. package/dist/native.js +17 -0
  30. package/dist/native.js.map +1 -0
  31. package/dist/refresh.d.ts +18 -0
  32. package/dist/refresh.d.ts.map +1 -0
  33. package/dist/refresh.js +18 -0
  34. package/dist/refresh.js.map +1 -0
  35. package/dist/run.d.ts +27 -0
  36. package/dist/run.d.ts.map +1 -0
  37. package/dist/run.js +76 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/runtime.d.ts +30 -0
  40. package/dist/runtime.d.ts.map +1 -0
  41. package/dist/runtime.js +193 -0
  42. package/dist/runtime.js.map +1 -0
  43. package/dist/stubs/react-dom-client.d.ts +8 -0
  44. package/dist/stubs/react-dom-client.d.ts.map +1 -0
  45. package/dist/stubs/react-dom-client.js +11 -0
  46. package/dist/stubs/react-dom-client.js.map +1 -0
  47. package/dist/stubs/react-dom.d.ts +9 -0
  48. package/dist/stubs/react-dom.d.ts.map +1 -0
  49. package/dist/stubs/react-dom.js +13 -0
  50. package/dist/stubs/react-dom.js.map +1 -0
  51. package/dist/url.d.ts +48 -0
  52. package/dist/url.d.ts.map +1 -0
  53. package/dist/url.js +213 -0
  54. package/dist/url.js.map +1 -0
  55. package/dist/vendor.d.ts +11 -0
  56. package/dist/vendor.d.ts.map +1 -0
  57. package/dist/vendor.js +27 -0
  58. package/dist/vendor.js.map +1 -0
  59. package/dist/wasmHost.d.ts +35 -0
  60. package/dist/wasmHost.d.ts.map +1 -0
  61. package/dist/wasmHost.js +120 -0
  62. package/dist/wasmHost.js.map +1 -0
  63. package/package.json +73 -4
  64. package/src/bridge.ts +58 -0
  65. package/src/build.ts +185 -0
  66. package/src/devBuild.ts +152 -0
  67. package/src/encoding.ts +130 -0
  68. package/src/fetch.ts +305 -0
  69. package/src/index.ts +20 -0
  70. package/src/native.ts +35 -0
  71. package/src/refresh.ts +29 -0
  72. package/src/run.ts +98 -0
  73. package/src/runtime.ts +208 -0
  74. package/src/stubs/react-dom-client.ts +17 -0
  75. package/src/stubs/react-dom.ts +15 -0
  76. package/src/types/react-refresh.d.ts +38 -0
  77. package/src/url.ts +202 -0
  78. package/src/vendor.ts +30 -0
  79. package/src/wasmHost.ts +147 -0
  80. package/swift/Package.swift +31 -0
  81. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +228 -0
  82. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +42 -0
  83. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +102 -0
  84. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +25 -0
  85. package/swift/Sources/SwiftBrowserNative/Host/WebHost.swift +138 -0
  86. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +251 -0
  87. package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +53 -0
  88. package/swift/Sources/SwiftBrowserNative/SwiftBrowserNative.swift +6 -0
  89. package/swift/Sources/SwiftBrowserNative/Views/Conversions.swift +361 -0
  90. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +169 -0
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +268 -0
  92. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +175 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +196 -0
  94. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +246 -0
  95. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +108 -0
  96. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +189 -0
  97. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +102 -0
  98. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +190 -0
  99. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +182 -0
  100. package/swift/Sources/SwiftBrowserProtocol/JSONValue.swift +268 -0
  101. package/swift/Sources/SwiftBrowserProtocol/Op.swift +100 -0
  102. package/swift/Sources/SwiftBrowserProtocol/Props.swift +469 -0
  103. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +117 -0
  104. package/swift/Sources/SwiftBrowserProtocol/Style.swift +148 -0
  105. package/swift/Sources/SwiftBrowserProtocol/Values.swift +279 -0
  106. package/swift/Sources/SwiftBrowserTree/ElementNode.swift +65 -0
  107. 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