craft-native 0.0.417 → 0.0.420

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
@@ -3047,7 +3047,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3047
3047
  `);
3048
3048
  }
3049
3049
  // package.json
3050
- var version = "0.0.417";
3050
+ var version = "0.0.420";
3051
3051
 
3052
3052
  // bin/cli.ts
3053
3053
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -375,7 +375,7 @@ function craftBinaryNotFoundMessage(triedPath) {
375
375
  `);
376
376
  }
377
377
  // package.json
378
- var version = "0.0.417";
378
+ var version = "0.0.420";
379
379
 
380
380
  // src/sdk-version.ts
381
381
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.417";
120
+ var version = "0.0.420";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -43,7 +43,7 @@ import process from "process";
43
43
  var macosToolchain = {
44
44
  zig: "0.18.0-dev.252+86dc5ce25",
45
45
  sources: {
46
- "zig-js": { repository: "zig-utils/zig-js", revision: "f0072c95780712d1bb789280e08e7e390e83ab94", paths: ["src", "include", "tests", "tools", "docs", "bench", "conformance"] },
46
+ "zig-js": { repository: "zig-utils/zig-js", revision: "fef92e7f6c5e110102df15e73bbea4a6be7493b5", paths: ["src", "include", "tests", "tools", "docs", "bench", "conformance"] },
47
47
  "zig-regex": { repository: "zig-utils/zig-regex", revision: "fdd9c2e333ea6ac2fd06d080d939ef7f11d61b1b", paths: ["src"] },
48
48
  "zig-gc": { repository: "zig-utils/zig-gc", revision: "0285ef801dd221a4ae8e88b31a09ae31627657a3", paths: ["src"] }
49
49
  }
@@ -9,7 +9,7 @@ export declare const macosToolchain: {
9
9
  readonly sources: {
10
10
  readonly 'zig-js': {
11
11
  readonly repository: 'zig-utils/zig-js';
12
- readonly revision: 'f0072c95780712d1bb789280e08e7e390e83ab94';
12
+ readonly revision: 'fef92e7f6c5e110102df15e73bbea4a6be7493b5';
13
13
  readonly paths: readonly ['src', 'include', 'tests', 'tools', 'docs', 'bench', 'conformance'];
14
14
  };
15
15
  readonly 'zig-regex': {
@@ -73,6 +73,8 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
73
73
  func windowWillClose(_ notification: Notification) {
74
74
  runtime.dispose()
75
75
  CraftMacApplication.shared.scenes.removeValue(forKey: name)
76
+ // The app's other windows learn it is gone (its tabs closed with it), so none waits on it.
77
+ for other in CraftMacApplication.shared.scenes.values { other.runtime.send("CRAFT_EVENT", ["event": "craftDesktopWindowClosed", "detail": ["name": name]]) }
76
78
  CraftMacApplication.shared.quitReady(scene: name)
77
79
  }
78
80
  func windowDidBecomeKey(_ notification: Notification) { runtime.send("SCREEN_FOCUS", ["focused": true]) }
@@ -419,6 +421,22 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
419
421
  // After this result is written, from the run loop rather than a main-queue block (see applicationShouldTerminate).
420
422
  if kind == "quit" { NSApp.perform(#selector(NSApplication.terminate(_:)), with: nil, afterDelay: 0); return ["ok": true] }
421
423
  if kind == "openURL", let url = (action["url"] as? String).flatMap(URL.init(string:)) { application(NSApp, open: [url]); return ["ok": true] }
424
+ #if CRAFT_WEBKIT
425
+ // Script in a tab's page, as a person's click or a page's own code would act: what the
426
+ // renderer cannot reach (a link inside the page, an element to zap). `value` is the
427
+ // script's result as text; return JSON.stringify(...) for anything structured.
428
+ if kind == "web", let id = action["tab"] as? String, let script = action["js"] as? String {
429
+ guard let tab = CraftMacWebTabs.shared.tabs[id] else { return ["ok": false, "error": "No tab \(id)"] }
430
+ var answer: [String: Any]?
431
+ tab.view.evaluateJavaScript(script) { value, error in
432
+ if let error { answer = ["ok": false, "error": error.localizedDescription]; return }
433
+ answer = ["ok": true, "value": value.map { "\($0)" } ?? NSNull()]
434
+ }
435
+ let deadline = Date().addingTimeInterval(5)
436
+ while answer == nil && Date() < deadline { RunLoop.main.run(until: Date().addingTimeInterval(0.02)) }
437
+ return answer ?? ["ok": false, "error": "The page did not answer within 5 s"]
438
+ }
439
+ #endif
422
440
  if kind == "appearance" {
423
441
  NSApp.appearance = NSAppearance(named: action["value"] as? String == "dark" ? .darkAqua : .aqua)
424
442
  appearanceChanged(); return ["ok": true]
@@ -68,7 +68,13 @@ final class CraftMacRuntime {
68
68
  #endif
69
69
  return list
70
70
  }
71
- let context = ZigJSContext()!
71
+ /// zig-js's precise collector, not the default arena: an arena frees nothing until the
72
+ /// context goes, so a window open all day kept every object its screen ever made.
73
+ let context: ZigJSContext = {
74
+ let created = ZJSGlobalContextCreateGarbageCollected(true)!
75
+ defer { JSGlobalContextRelease(created) }
76
+ return ZigJSContext(jsGlobalContextRef: created)!
77
+ }()
72
78
  let route: String
73
79
  let network: CraftMacNetwork
74
80
  weak var scene: CraftMacScene?
@@ -40,8 +40,17 @@ final class CraftMacWebProfiles {
40
40
  return configuration.copy() as! WKWebViewConfiguration
41
41
  }
42
42
 
43
+ /// One store per private profile for as long as the app runs, so clearing data or adding
44
+ /// cookies reaches the store its tabs use (each `.nonPersistent()` call makes a new, empty one).
45
+ private var ephemeralStores: [String: WKWebsiteDataStore] = [:]
46
+
43
47
  func store(profile: String, ephemeral: Bool) -> WKWebsiteDataStore {
44
- if ephemeral { return .nonPersistent() }
48
+ if ephemeral {
49
+ if let existing = ephemeralStores[profile] { return existing }
50
+ let made = WKWebsiteDataStore.nonPersistent()
51
+ ephemeralStores[profile] = made
52
+ return made
53
+ }
45
54
  if profile == "default" { return .default() }
46
55
  if #available(macOS 14.0, *) { return WKWebsiteDataStore(forIdentifier: Self.identifier(profile)) }
47
56
  return .default()
@@ -494,6 +503,38 @@ final class CraftMacWebTabs {
494
503
  }
495
504
  }
496
505
 
506
+ /// Cookies put into a profile's store, as another browser's import brings them: `domain` with
507
+ /// a leading dot covers subdomains, `expires` is milliseconds since 1970 (absent for a session
508
+ /// cookie), `sameSite` is "lax", "strict" or "none". Answers how many were set, and how many
509
+ /// could not form a cookie (no name or domain).
510
+ func setCookies(_ options: [String: Any], answer: @escaping (Any) -> Void) {
511
+ let store = CraftMacWebProfiles.shared.store(profile: options["profile"] as? String ?? "default", ephemeral: options["private"] as? Bool == true).httpCookieStore
512
+ let cookies = (options["cookies"] as? [[String: Any]] ?? []).map(Self.cookie)
513
+ let group = DispatchGroup()
514
+ for case let cookie? in cookies {
515
+ group.enter()
516
+ store.setCookie(cookie) { group.leave() }
517
+ }
518
+ let set = cookies.compactMap { $0 }.count
519
+ group.notify(queue: .main) { answer(["set": set, "skipped": cookies.count - set]) }
520
+ }
521
+
522
+ static func cookie(_ fields: [String: Any]) -> HTTPCookie? {
523
+ guard let name = fields["name"] as? String, !name.isEmpty, let domain = fields["domain"] as? String, !domain.isEmpty else { return nil }
524
+ var properties: [HTTPCookiePropertyKey: Any] = [
525
+ .name: name, .value: fields["value"] as? String ?? "", .domain: domain, .path: fields["path"] as? String ?? "/",
526
+ ]
527
+ if let expires = fields["expires"] as? Double { properties[.expires] = Date(timeIntervalSince1970: expires / 1000) }
528
+ if fields["secure"] as? Bool == true { properties[.secure] = "TRUE" }
529
+ if fields["httpOnly"] as? Bool == true { properties[HTTPCookiePropertyKey("HttpOnly")] = "TRUE" }
530
+ switch fields["sameSite"] as? String {
531
+ case "lax": properties[.sameSitePolicy] = HTTPCookieStringPolicy.sameSiteLax
532
+ case "strict": properties[.sameSitePolicy] = HTTPCookieStringPolicy.sameSiteStrict
533
+ default: break
534
+ }
535
+ return HTTPCookie(properties: properties)
536
+ }
537
+
497
538
  /// A closed window takes its tabs with it.
498
539
  func closeAll(owner: CraftMacRuntime) {
499
540
  for (id, tab) in tabs where tab.owner == nil || tab.owner === owner { tabs.removeValue(forKey: id); tab.close() }
@@ -502,6 +543,9 @@ final class CraftMacWebTabs {
502
543
  func perform(_ method: String, _ args: [Any], owner: CraftMacRuntime, answer: @escaping (Any) -> Void, fail: @escaping (String, String) -> Void) {
503
544
  let options = args.first as? [String: Any] ?? [:]
504
545
  if method == "create" {
546
+ // A tab that exists is answered as it is: loading the address again would reload a
547
+ // page that another window, say, hands over with its state intact.
548
+ if let id = options["id"] as? String, let existing = tabs[id] { answer(existing.state); return }
505
549
  let tab = make(id: options["id"] as? String, profile: options["profile"] as? String ?? "default", ephemeral: options["private"] as? Bool == true, owner: owner)
506
550
  tab.view.isInspectable = options["inspectable"] as? Bool ?? true
507
551
  if let address = options["url"] as? String, !address.isEmpty {
@@ -525,6 +569,7 @@ final class CraftMacWebTabs {
525
569
  let decision: WKPermissionDecision = { switch args.dropFirst().first as? String { case "grant": return .grant; case "prompt": return .prompt; default: return .deny } }()
526
570
  answer(self.answer(args.first as? String ?? "", decision)); return
527
571
  case "clearData": clearData(options, answer: answer); return
572
+ case "setCookies": setCookies(options, answer: answer); return
528
573
  case "cancelDownload": answer(CraftMacDownloads.shared.cancel(args.first as? String ?? "")); return
529
574
  case "revealDownload", "openDownload":
530
575
  guard let file = CraftMacDownloads.downloadedFile(args.first as? String ?? "") else { fail("NOT_FOUND", "Not a file in Downloads"); return }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.417",
3
+ "version": "0.0.420",
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>",