@swiftbrowser/native 0.17.7 → 0.18.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.
Files changed (100) hide show
  1. package/README.md +99 -11
  2. package/dist/blob.d.ts +60 -0
  3. package/dist/blob.d.ts.map +1 -0
  4. package/dist/blob.js +153 -0
  5. package/dist/blob.js.map +1 -0
  6. package/dist/bridge.d.ts +65 -1
  7. package/dist/bridge.d.ts.map +1 -1
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/fetch.d.ts +11 -3
  10. package/dist/fetch.d.ts.map +1 -1
  11. package/dist/fetch.js +26 -2
  12. package/dist/fetch.js.map +1 -1
  13. package/dist/hostRequests.d.ts +55 -0
  14. package/dist/hostRequests.d.ts.map +1 -0
  15. package/dist/hostRequests.js +196 -0
  16. package/dist/hostRequests.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/matchMedia.d.ts +43 -0
  22. package/dist/matchMedia.d.ts.map +1 -0
  23. package/dist/matchMedia.js +63 -0
  24. package/dist/matchMedia.js.map +1 -0
  25. package/dist/notifications.d.ts +108 -0
  26. package/dist/notifications.d.ts.map +1 -0
  27. package/dist/notifications.js +251 -0
  28. package/dist/notifications.js.map +1 -0
  29. package/dist/run.d.ts.map +1 -1
  30. package/dist/run.js +23 -2
  31. package/dist/run.js.map +1 -1
  32. package/dist/runtime.d.ts +2 -0
  33. package/dist/runtime.d.ts.map +1 -1
  34. package/dist/runtime.js +69 -0
  35. package/dist/runtime.js.map +1 -1
  36. package/dist/secureStorage.d.ts +20 -0
  37. package/dist/secureStorage.d.ts.map +1 -0
  38. package/dist/secureStorage.js +28 -0
  39. package/dist/secureStorage.js.map +1 -0
  40. package/dist/site.d.ts +3 -3
  41. package/dist/site.d.ts.map +1 -1
  42. package/dist/site.js +1 -1
  43. package/dist/site.js.map +1 -1
  44. package/dist/subtle.d.ts +38 -0
  45. package/dist/subtle.d.ts.map +1 -0
  46. package/dist/subtle.js +72 -0
  47. package/dist/subtle.js.map +1 -0
  48. package/dist/undo.d.ts +23 -0
  49. package/dist/undo.d.ts.map +1 -0
  50. package/dist/undo.js +44 -0
  51. package/dist/undo.js.map +1 -0
  52. package/dist/webSocket.d.ts +73 -0
  53. package/dist/webSocket.d.ts.map +1 -0
  54. package/dist/webSocket.js +145 -0
  55. package/dist/webSocket.js.map +1 -0
  56. package/package.json +3 -3
  57. package/src/blob.ts +186 -0
  58. package/src/bridge.ts +74 -1
  59. package/src/fetch.ts +29 -3
  60. package/src/hostRequests.ts +216 -0
  61. package/src/index.ts +8 -0
  62. package/src/matchMedia.ts +79 -0
  63. package/src/notifications.ts +317 -0
  64. package/src/run.ts +21 -2
  65. package/src/runtime.ts +71 -0
  66. package/src/secureStorage.ts +39 -0
  67. package/src/site.ts +3 -3
  68. package/src/subtle.ts +87 -0
  69. package/src/undo.ts +47 -0
  70. package/src/webSocket.ts +175 -0
  71. package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +199 -0
  72. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +154 -0
  73. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +211 -0
  74. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +451 -0
  75. package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
  76. package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
  77. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +1 -0
  78. package/swift/Sources/SwiftBrowserNative/Host/TextMeasureBridge.swift +139 -0
  79. package/swift/Sources/SwiftBrowserNative/Host/UndoBridge.swift +170 -0
  80. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
  81. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +56 -0
  82. package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
  83. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +362 -16
  84. package/swift/Sources/SwiftBrowserNative/Views/Kinds/DeleteBackward.swift +147 -0
  85. package/swift/Sources/SwiftBrowserNative/Views/Kinds/KeyboardBar.swift +84 -0
  86. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +116 -11
  87. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +12 -1
  88. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +30 -9
  89. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +163 -10
  90. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +17 -4
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +8 -3
  92. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +8 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +41 -0
  94. package/swift/Sources/SwiftBrowserProtocol/Op.swift +27 -4
  95. package/swift/Sources/SwiftBrowserProtocol/Props.swift +110 -3
  96. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +47 -0
  97. package/swift/Sources/SwiftBrowserProtocol/SVGPath.swift +260 -0
  98. package/swift/Sources/SwiftBrowserProtocol/Style.swift +58 -0
  99. package/swift/Sources/SwiftBrowserTree/OpTree.swift +22 -2
  100. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -11,6 +11,9 @@ import AppKit
11
11
  #if canImport(FoundationBridge)
12
12
  import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
13
13
  #endif
14
+ #if canImport(CryptoKit)
15
+ import CryptoKit
16
+ #endif
14
17
 
15
18
  /// The native primitives `@swiftbrowser/native` builds the web globals from
16
19
  /// (packages/native/src/runtime.ts), installed on `__sbNative`:
@@ -27,8 +30,17 @@ import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
27
30
  /// | `randomBytes(count)` | base64 of random bytes (`crypto.getRandomValues`) |
28
31
  /// | `openURL(url)` | the system opener |
29
32
  /// | `locale()` | the viewer's locale identifier |
33
+ /// | `measureText(text, fontJSON, width)` | @swiftbrowser/react's `measureText`: `{ width, height, lines }` of the text in the font wrapped to the width (one line when null), from `NSAttributedString`'s bounding rect, synchronously (TextMeasureBridge.swift); nothing where there is no text system |
30
34
  /// | `launchArguments()` | the process's arguments (`-screen list` from a scheme or `simctl launch`) |
35
+ /// | `appDirectories()` | the app's standard directories as `file:` URLs, a JSON object `{ documents, applicationSupport, caches, temporary }`, for `fileManager` (FilesBridge.swift); its `files` requests go through `hostRequest`. Apple platforms only |
31
36
  /// | `mediaSession(update)` | `navigator.mediaSession`: the lock screen's now-playing info and the remote commands the app handles, on MediaPlayer (NowPlayingBridge.swift); a command the viewer gives comes to `__sbMediaSessionCommand(json)` |
37
+ /// | `notificationsReady()`, `notificationPermission(id, request)`, `notificationShow(json)`, `notificationClose(json)`, `setBadge(count)`, `pushRegister(id, key?)` | notifications on UserNotifications (NotificationBridge.swift): the permission answers `__sbNotificationPermission(id, state)`, a registration `__sbPushRegistered(id, result)`, and what the viewer opens or what arrives comes to `__sbNotification(json)` |
38
+ /// | `wsOpen(id, url, protocols)`, `wsSend(id, json)`, `wsClose(id, code, reason)` | `WebSocket` on `URLSessionWebSocketTask` (WebSocketBridge.swift); what a socket receives comes to `__sbWebSocketEvent(id, json)` |
39
+ /// | `digest(algorithm, base64)`, `hmac(algorithm, key, base64)` | `crypto.subtle` on CryptoKit, synchronously: the base64 digest or HMAC, or nothing when the algorithm is not one of SHA-1, SHA-256, SHA-384 and SHA-512 |
40
+ /// | `haptic(kind)` | `navigator.vibrate`: a feedback generator's `impact`, `selection`, `success`, `warning` or `error` |
41
+ /// | `hostRequest(id, kind, json)` | the web renderer's `share`, `pickFiles`, `clipboard`, `pickColor` and `webAuth` requests, answered with the system's share sheet, pickers, pasteboard and sign-in session (HostRequestBridge.swift); the answer comes to `__sbHostResponse(id, result)` |
42
+ /// | `secureGet(key)`, `secureSet(key, value)`, `secureRemove(key)` | `secureStorage` over the Keychain (SecureStorage.swift) |
43
+ /// | `undoState(json)` | the app's undo manager's `canUndo`, `canRedo`, `undoActionName` and `redoActionName`, mirrored on the window's `UndoManager` (UndoBridge.swift) so the shake, the three-finger swipe, ⌘Z and the Edit menu reach the app; what the system asks comes to `__sbUndoCommand("undo" \| "redo")` |
32
44
  /// | `workerStart(id, name, engine)`, `workerPost(id, text)`, `workerTerminate(id)` | workers (JavaScriptCore only): a `JSContext` on a thread (`"context"`, WorkerHost.swift) or a hidden web view with WebKit's JIT (`"webview"`, WebViewWorkerHost.swift); their messages come to `__sbWorkerMessage(id, text)`, their errors to `__sbWorkerError(id, message)` |
33
45
  ///
34
46
  /// Everything JavaScript-facing runs on the main thread; work that is not
@@ -46,6 +58,16 @@ final class HostBridge {
46
58
  #if canImport(MediaPlayer)
47
59
  private var nowPlaying: NowPlayingBridge?
48
60
  #endif
61
+ #if canImport(UserNotifications)
62
+ private var notifications: NotificationBridge?
63
+ #endif
64
+ private var webSockets: WebSocketBridge?
65
+ private var hostRequests: HostRequestBridge?
66
+ #if !canImport(FoundationBridge)
67
+ private var undo: UndoBridge?
68
+ /// The window's undo manager, for the undo bridge when the app first sends a state.
69
+ private weak var undoManager: UndoManager?
70
+ #endif
49
71
  /// Keys of `localStorage` live under this prefix in `UserDefaults`.
50
72
  static let storagePrefix = "swiftbrowser.storage."
51
73
 
@@ -59,6 +81,10 @@ final class HostBridge {
59
81
  func invalidate() {
60
82
  for timer in timers.values { timer.invalidate() }
61
83
  timers.removeAll()
84
+ webSockets?.invalidate()
85
+ #if !canImport(FoundationBridge)
86
+ undo?.manager = nil
87
+ #endif
62
88
  #if canImport(JavaScriptCore)
63
89
  for worker in workers.values { worker.terminate() }
64
90
  workers.removeAll()
@@ -194,8 +220,18 @@ final class HostBridge {
194
220
 
195
221
  host.install("locale") { _ in .string(Locale.current.identifier) }
196
222
 
223
+ host.install("measureText") { arguments in
224
+ guard let text = arguments[safe: 0]?.string else { return nil }
225
+ let font = (try? JSONValue.parse(arguments[safe: 1]?.string ?? "{}")) ?? .object([:])
226
+ return TextMeasureBridge.measure(text: text, font: font, width: arguments[safe: 2]?.double)
227
+ }
228
+
197
229
  host.install("launchArguments") { _ in .array(ProcessInfo.processInfo.arguments.map(JSONValue.string)) }
198
230
 
231
+ #if !canImport(FoundationBridge)
232
+ host.install("appDirectories") { _ in .string(FilesBridge.directories().json) }
233
+ #endif
234
+
199
235
  #if canImport(MediaPlayer)
200
236
  host.install("mediaSession") { [weak self] arguments in
201
237
  guard let self, let update = arguments.first?.string else { return .string("mediaSession: expected a JSON string") }
@@ -206,6 +242,100 @@ final class HostBridge {
206
242
  }
207
243
  #endif
208
244
 
245
+ #if canImport(UserNotifications)
246
+ host.install("notificationsReady") { [weak self] _ in
247
+ self?.notificationBridge.listen()
248
+ return nil
249
+ }
250
+ host.install("notificationPermission") { [weak self] arguments in
251
+ guard let self, let id = arguments[safe: 0]?.int else { return nil }
252
+ notificationBridge.permission(id: id, request: arguments[safe: 1]?.bool ?? false)
253
+ return nil
254
+ }
255
+ host.install("notificationShow") { [weak self] arguments in
256
+ guard let self, let json = arguments.first?.string else { return .string("notification: expected a JSON string") }
257
+ return notificationBridge.show(json).map(JSONValue.string)
258
+ }
259
+ host.install("notificationClose") { [weak self] arguments in
260
+ guard let self, let json = arguments.first?.string else { return nil }
261
+ notificationBridge.close(json)
262
+ return nil
263
+ }
264
+ host.install("setBadge") { [weak self] arguments in
265
+ guard let self, let count = arguments.first?.int else { return nil }
266
+ notificationBridge.setBadge(count)
267
+ return nil
268
+ }
269
+ host.install("pushRegister") { [weak self] arguments in
270
+ guard let self, let id = arguments[safe: 0]?.int else { return nil }
271
+ notificationBridge.registerForPush(id: id, applicationServerKey: arguments[safe: 1]?.string)
272
+ return nil
273
+ }
274
+ #endif
275
+
276
+ host.install("wsOpen") { [weak self] arguments in
277
+ guard let self, let id = arguments[safe: 0]?.int, let url = arguments[safe: 1]?.string else { return nil }
278
+ let protocols = arguments[safe: 2]?.array?.compactMap(\.string) ?? []
279
+ webSocketBridge.open(id: id, url: url, protocols: protocols)
280
+ return nil
281
+ }
282
+ host.install("wsSend") { [weak self] arguments in
283
+ guard let self, let id = arguments[safe: 0]?.int, let json = arguments[safe: 1]?.string else { return nil }
284
+ webSocketBridge.send(id: id, json: json)
285
+ return nil
286
+ }
287
+ host.install("wsClose") { [weak self] arguments in
288
+ guard let self, let id = arguments[safe: 0]?.int else { return nil }
289
+ webSocketBridge.close(id: id, code: arguments[safe: 1]?.int ?? 1000, reason: arguments[safe: 2]?.string ?? "")
290
+ return nil
291
+ }
292
+
293
+ host.install("digest") { arguments in
294
+ guard let algorithm = arguments[safe: 0]?.string, let encoded = arguments[safe: 1]?.string, let data = Data(base64Encoded: encoded) else { return nil }
295
+ return Self.digest(algorithm, data).map { .string($0.base64EncodedString()) }
296
+ }
297
+ host.install("hmac") { arguments in
298
+ guard let algorithm = arguments[safe: 0]?.string, let key = arguments[safe: 1]?.string, let encoded = arguments[safe: 2]?.string,
299
+ let keyData = Data(base64Encoded: key), let data = Data(base64Encoded: encoded) else { return nil }
300
+ return Self.hmac(algorithm, key: keyData, data).map { .string($0.base64EncodedString()) }
301
+ }
302
+
303
+ host.install("haptic") { arguments in
304
+ Self.haptic(arguments.first?.string ?? "impact")
305
+ return nil
306
+ }
307
+
308
+ host.install("hostRequest") { [weak self] arguments in
309
+ guard let self, let id = arguments[safe: 0]?.int, let kind = arguments[safe: 1]?.string else { return nil }
310
+ if hostRequests == nil {
311
+ hostRequests = HostRequestBridge { [weak self] id, result in self?.callIn("__sbHostResponse", [.number(Double(id)), .string(result.json)]) }
312
+ }
313
+ hostRequests?.perform(id: id, kind: kind, json: arguments[safe: 2]?.string ?? "{}")
314
+ return nil
315
+ }
316
+
317
+ #if !canImport(FoundationBridge)
318
+ host.install("undoState") { [weak self] arguments in
319
+ guard let self, let json = arguments.first?.string else { return .string("undoState: expected a JSON string") }
320
+ return undoBridge.update(json).map(JSONValue.string)
321
+ }
322
+ #endif
323
+
324
+ host.install("secureGet") { arguments in
325
+ guard let key = arguments.first?.string else { return nil }
326
+ return SecureStorage.get(key).map(JSONValue.string) ?? .null
327
+ }
328
+ host.install("secureSet") { arguments in
329
+ guard let key = arguments[safe: 0]?.string, let value = arguments[safe: 1]?.string else { return nil }
330
+ SecureStorage.set(key, value)
331
+ return nil
332
+ }
333
+ host.install("secureRemove") { arguments in
334
+ guard let key = arguments.first?.string else { return nil }
335
+ SecureStorage.remove(key)
336
+ return nil
337
+ }
338
+
209
339
  #if canImport(JavaScriptCore)
210
340
  host.install("workerStart") { [weak self] arguments in
211
341
  guard let self, let id = arguments[safe: 0]?.int, let name = arguments[safe: 1]?.string else {
@@ -255,6 +385,87 @@ final class HostBridge {
255
385
  #endif
256
386
  }
257
387
 
388
+ #if canImport(UserNotifications)
389
+ /// The notification bridge, made when the app first uses notifications.
390
+ private var notificationBridge: NotificationBridge {
391
+ if let notifications { return notifications }
392
+ let bridge = NotificationBridge { [weak self] function, arguments in self?.callIn(function, arguments) }
393
+ notifications = bridge
394
+ return bridge
395
+ }
396
+ #endif
397
+
398
+ #if !canImport(FoundationBridge)
399
+ /// The undo bridge, made when the app first sends its undo manager's state.
400
+ private var undoBridge: UndoBridge {
401
+ if let undo { return undo }
402
+ let bridge = UndoBridge { [weak self] command in self?.callIn("__sbUndoCommand", [.string(command)]) }
403
+ bridge.manager = undoManager
404
+ undo = bridge
405
+ return bridge
406
+ }
407
+
408
+ /// The window's `UndoManager` (the root view's environment), which mirrors the app's.
409
+ func setUndoManager(_ manager: UndoManager?) {
410
+ undoManager = manager
411
+ undo?.manager = manager
412
+ }
413
+ #endif
414
+
415
+ /// The WebSocket bridge, made when the app opens its first socket.
416
+ private var webSocketBridge: WebSocketBridge {
417
+ if let webSockets { return webSockets }
418
+ let bridge = WebSocketBridge(session: session) { [weak self] id, event in self?.callIn("__sbWebSocketEvent", [.number(Double(id)), .string(event.json)]) }
419
+ webSockets = bridge
420
+ return bridge
421
+ }
422
+
423
+ // MARK: crypto.subtle and haptics
424
+
425
+ static func digest(_ algorithm: String, _ data: Data) -> Data? {
426
+ #if canImport(CryptoKit)
427
+ switch algorithm {
428
+ case "SHA-256": Data(SHA256.hash(data: data))
429
+ case "SHA-384": Data(SHA384.hash(data: data))
430
+ case "SHA-512": Data(SHA512.hash(data: data))
431
+ case "SHA-1": Data(Insecure.SHA1.hash(data: data))
432
+ default: nil
433
+ }
434
+ #else
435
+ nil
436
+ #endif
437
+ }
438
+
439
+ static func hmac(_ algorithm: String, key: Data, _ data: Data) -> Data? {
440
+ #if canImport(CryptoKit) && !canImport(FoundationBridge)
441
+ let symmetricKey = SymmetricKey(data: key)
442
+ return switch algorithm {
443
+ case "SHA-256": Data(HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey))
444
+ case "SHA-384": Data(HMAC<SHA384>.authenticationCode(for: data, using: symmetricKey))
445
+ case "SHA-512": Data(HMAC<SHA512>.authenticationCode(for: data, using: symmetricKey))
446
+ case "SHA-1": Data(HMAC<Insecure.SHA1>.authenticationCode(for: data, using: symmetricKey))
447
+ default: nil
448
+ }
449
+ #else
450
+ // The shim's CryptoKit has no HMAC.
451
+ nil
452
+ #endif
453
+ }
454
+
455
+ static func haptic(_ kind: String) {
456
+ #if canImport(UIKit) && !os(tvOS)
457
+ switch kind {
458
+ case "selection": UISelectionFeedbackGenerator().selectionChanged()
459
+ case "success": UINotificationFeedbackGenerator().notificationOccurred(.success)
460
+ case "warning": UINotificationFeedbackGenerator().notificationOccurred(.warning)
461
+ case "error": UINotificationFeedbackGenerator().notificationOccurred(.error)
462
+ default: UIImpactFeedbackGenerator(style: .medium).impactOccurred()
463
+ }
464
+ #elseif canImport(AppKit)
465
+ NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .now)
466
+ #endif
467
+ }
468
+
258
469
  /// Calls into JavaScript from a timer or a completion, where a throw has nowhere to go but the log.
259
470
  private func callIn(_ function: String, _ arguments: [JSONValue]) {
260
471
  do {