craft-native 0.0.78 → 0.0.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/dist/cli.js CHANGED
@@ -4191,7 +4191,7 @@ function craftBinaryNotFoundMessage(triedPath) {
4191
4191
  `);
4192
4192
  }
4193
4193
  // package.json
4194
- var version = "0.0.78";
4194
+ var version = "0.0.80";
4195
4195
 
4196
4196
  // bin/cli.ts
4197
4197
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -364,7 +364,12 @@ struct CraftWebView: UIViewRepresentable {
364
364
  func updateUIView(_ webView: WKWebView, context: Context) {}
365
365
 
366
366
  func makeCoordinator() -> Coordinator {
367
- Coordinator(config: config)
367
+ let coordinator = Coordinator(config: config)
368
+ // The Zig dispatcher finds CraftSwiftShim by class name; the shim finds
369
+ // its way back to the live coordinator through this. Weak, because
370
+ // SwiftUI owns the coordinator's lifetime.
371
+ CraftSwiftShim.coordinator = coordinator
372
+ return coordinator
368
373
  }
369
374
 
370
375
  // MARK: - Coordinator (Native Bridge)
@@ -511,8 +516,19 @@ struct CraftWebView: UIViewRepresentable {
511
516
  guard let body = message.body as? [String: Any],
512
517
  let action = body["action"] as? String else { return }
513
518
 
514
- let callbackId = body["callbackId"] as? String
519
+ dispatch(action: action, body: body, callbackId: body["callbackId"] as? String)
520
+ }
515
521
 
522
+ /// The action switch, reachable from two directions: the
523
+ /// WKScriptMessageHandler above, and `CraftSwiftShim.handleAction` when
524
+ /// the Zig dispatcher hands over an action it has not migrated yet.
525
+ ///
526
+ /// A hand-off call arrives with a synthetic callbackId of the form
527
+ /// "zig:<requestId>". The reply helpers recognise that prefix and route
528
+ /// the answer back through the Zig runtime's exported
529
+ /// `craft_ios_deliver_result` instead of evaluating JavaScript here —
530
+ /// one reply path, owned by whichever side received the page's message.
531
+ func dispatch(action: String, body: [String: Any], callbackId: String?) {
516
532
  switch action {
517
533
  case "startListening":
518
534
  if config.enableSpeechRecognition { startSpeechRecognition() }
@@ -2426,6 +2442,10 @@ struct CraftWebView: UIViewRepresentable {
2426
2442
  rejectCallback(callbackId, error: "Native result could not be serialized", code: "SERIALIZATION_ERROR")
2427
2443
  return
2428
2444
  }
2445
+ // A hand-off from the Zig dispatcher replies through Zig, which
2446
+ // owns the wire format, the request id, and the escaping. Replying
2447
+ // by JavaScript here as well would give the page two answers.
2448
+ if CraftSwiftShim.deliverResultIfHandOff(id, json: resultStr) { return }
2429
2449
  let script = "window.craft._resolveCallback('\(id)', \(resultStr));"
2430
2450
  DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2431
2451
  }
@@ -2436,7 +2456,17 @@ struct CraftWebView: UIViewRepresentable {
2436
2456
 
2437
2457
  private func rejectCallback(_ callbackId: String?, error: String, code: String = "CRAFT_ERROR") {
2438
2458
  guard let id = callbackId else { return }
2439
- let escapedError = error.replacingOccurrences(of: "'", with: "\\'").replacingOccurrences(of: "\n", with: "\\n")
2459
+ // A hand-off rejection goes back through Zig's error route, so the
2460
+ // page's promise *rejects*. Delivering it as a result would run the
2461
+ // app's then-branch with an error-shaped object — fabricated
2462
+ // success wearing a different hat.
2463
+ if CraftSwiftShim.deliverErrorIfHandOff(id, message: error, code: code) { return }
2464
+ // Backslashes first: escaping ' and \n but not \ let an error
2465
+ // message containing a backslash break out of the string literal.
2466
+ let escapedError = error
2467
+ .replacingOccurrences(of: "\\", with: "\\\\")
2468
+ .replacingOccurrences(of: "'", with: "\\'")
2469
+ .replacingOccurrences(of: "\n", with: "\\n")
2440
2470
  let script = "window.craft._rejectCallback('\(id)', '\(escapedError)', '\(code)');"
2441
2471
  DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2442
2472
  }
@@ -5159,3 +5189,98 @@ extension CraftWebView.Coordinator: WCSessionDelegate {
5159
5189
  }
5160
5190
  }
5161
5191
  }
5192
+
5193
+
5194
+ // MARK: - Zig hand-off shim
5195
+
5196
+ /// The seam the Zig dispatcher hands unmigrated actions through.
5197
+ ///
5198
+ /// Discovery is symmetric and both directions are runtime-only. Zig finds this
5199
+ /// class with `objc_getClass("CraftSwiftShim")`; this class finds Zig's
5200
+ /// delivery exports with `dlsym`. Neither side links the other, so a
5201
+ /// pure-Swift app (no Zig runtime) and a fully-migrated Zig app (no shim work
5202
+ /// left) both build and run without dead dependencies.
5203
+ ///
5204
+ /// The shim decides *what* the answer is, never *how* it reaches the page.
5205
+ /// Replies go back through Zig's `craft_ios_deliver_result` /
5206
+ /// `craft_ios_deliver_error`, which own the wire format, the request id, and
5207
+ /// the escaping. Two components replying to one page by different routes is
5208
+ /// how this codebase accumulated five envelopes.
5209
+ @objc(CraftSwiftShim)
5210
+ final class CraftSwiftShim: NSObject {
5211
+ /// The coordinator serving hand-offs. Set by `makeCoordinator`.
5212
+ static weak var coordinator: CraftWebView.Coordinator?
5213
+
5214
+ private typealias DeliverResultFn = @convention(c) (
5215
+ UnsafePointer<CChar>, UInt, UnsafePointer<CChar>, UInt, Int64
5216
+ ) -> Void
5217
+ private typealias DeliverErrorFn = @convention(c) (
5218
+ UnsafePointer<CChar>, UInt, UnsafePointer<CChar>, UInt,
5219
+ UnsafePointer<CChar>, UInt, Int64
5220
+ ) -> Void
5221
+
5222
+ private static let deliverResult: DeliverResultFn? = {
5223
+ guard let sym = dlsym(dlopen(nil, RTLD_NOW), "craft_ios_deliver_result") else { return nil }
5224
+ return unsafeBitCast(sym, to: DeliverResultFn.self)
5225
+ }()
5226
+
5227
+ private static let deliverError: DeliverErrorFn? = {
5228
+ guard let sym = dlsym(dlopen(nil, RTLD_NOW), "craft_ios_deliver_error") else { return nil }
5229
+ return unsafeBitCast(sym, to: DeliverErrorFn.self)
5230
+ }()
5231
+
5232
+ /// Entry point for the Zig dispatcher. Selector: handleAction:payload:requestId:
5233
+ ///
5234
+ /// Returns false when this shim cannot serve the call — no live
5235
+ /// coordinator, or no Zig runtime to reply through — so Zig answers the
5236
+ /// page with UnknownAction instead of the call vanishing.
5237
+ @objc static func handleAction(_ action: String, payload: String, requestId: Int64) -> Bool {
5238
+ guard let coordinator, deliverResult != nil else { return false }
5239
+
5240
+ let body = ((try? JSONSerialization.jsonObject(with: Data(payload.utf8))) as? [String: Any]) ?? [:]
5241
+
5242
+ // The synthetic callbackId routes this call's reply back through Zig.
5243
+ // It carries the action too, because the reply helpers do not otherwise
5244
+ // know it, and Zig's reply names the action for the page's
5245
+ // action-matching fallback.
5246
+ coordinator.dispatch(action: action, body: body, callbackId: "zig:\(requestId):\(action)")
5247
+ return true
5248
+ }
5249
+
5250
+ /// Route a resolve through Zig when the callbackId marks a hand-off.
5251
+ /// Returns true when the reply has been (or could only be) handled here.
5252
+ static func deliverResultIfHandOff(_ callbackId: String, json: String) -> Bool {
5253
+ guard let (requestId, action) = parseHandOffId(callbackId) else { return false }
5254
+ guard let deliverResult else { return true } // hand-off id but no runtime: drop, never eval JS
5255
+ action.withCString { a in
5256
+ json.withCString { j in
5257
+ deliverResult(a, UInt(strlen(a)), j, UInt(strlen(j)), requestId)
5258
+ }
5259
+ }
5260
+ return true
5261
+ }
5262
+
5263
+ /// Route a rejection through Zig's error path, so the page's promise
5264
+ /// rejects rather than resolving with an error-shaped object.
5265
+ static func deliverErrorIfHandOff(_ callbackId: String, message: String, code: String) -> Bool {
5266
+ guard let (requestId, action) = parseHandOffId(callbackId) else { return false }
5267
+ guard let deliverError else { return true }
5268
+ action.withCString { a in
5269
+ message.withCString { m in
5270
+ code.withCString { c in
5271
+ deliverError(a, UInt(strlen(a)), m, UInt(strlen(m)), c, UInt(strlen(c)), requestId)
5272
+ }
5273
+ }
5274
+ }
5275
+ return true
5276
+ }
5277
+
5278
+ /// "zig:<requestId>:<action>" -> (requestId, action). Nil for ordinary
5279
+ /// page-issued callback ids, which keep their JavaScript reply path.
5280
+ private static func parseHandOffId(_ callbackId: String) -> (Int64, String)? {
5281
+ guard callbackId.hasPrefix("zig:") else { return nil }
5282
+ let rest = callbackId.dropFirst(4)
5283
+ guard let sep = rest.firstIndex(of: ":"), let id = Int64(rest[..<sep]) else { return nil }
5284
+ return (id, String(rest[rest.index(after: sep)...]))
5285
+ }
5286
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",