@thelacanians/vue-native-cli 0.4.15 → 0.6.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/dist/cli.js +329 -15
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +118 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
- package/native/android/gradle.properties +1 -0
- package/native/android/gradlew +1 -1
- package/native/ios/VueNativeCore/Package.swift +1 -1
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +1 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +143 -5
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
- package/native/macos/VueNativeMacOS/Package.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
- package/native/shared/VueNativeShared/AGENTS.md +129 -0
- package/native/shared/VueNativeShared/Package.swift +14 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
- package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
- package/package.json +8 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Manages a WebSocket connection to the Vue Native dev server for hot reload.
|
|
4
|
+
/// When a new bundle is broadcast, calls `onBundleReceived` so the platform-specific
|
|
5
|
+
/// bridge can perform the reload.
|
|
6
|
+
///
|
|
7
|
+
/// Usage in your app (debug builds only):
|
|
8
|
+
/// ```swift
|
|
9
|
+
/// #if DEBUG
|
|
10
|
+
/// HotReloadManager.shared.onBundleReceived = { bundle in
|
|
11
|
+
/// NativeBridge.shared.reloadWithBundle(bundle)
|
|
12
|
+
/// }
|
|
13
|
+
/// HotReloadManager.shared.connect(to: URL(string: "ws://localhost:8174")!)
|
|
14
|
+
/// #endif
|
|
15
|
+
/// ```
|
|
16
|
+
public final class HotReloadManager: NSObject, URLSessionWebSocketDelegate {
|
|
17
|
+
|
|
18
|
+
public static let shared = HotReloadManager()
|
|
19
|
+
|
|
20
|
+
private var webSocketTask: URLSessionWebSocketTask?
|
|
21
|
+
private var session: URLSession?
|
|
22
|
+
private var serverURL: URL?
|
|
23
|
+
private var isConnecting = false
|
|
24
|
+
private var reconnectAttempts = 0
|
|
25
|
+
private let maxReconnectAttempts = 10
|
|
26
|
+
|
|
27
|
+
/// Called when a new bundle is received from the dev server.
|
|
28
|
+
/// Platform-specific code sets this to trigger a full app reload.
|
|
29
|
+
public var onBundleReceived: ((String) -> Void)?
|
|
30
|
+
|
|
31
|
+
private override init() {
|
|
32
|
+
super.init()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Public API
|
|
36
|
+
|
|
37
|
+
/// Connect to the dev server. Safe to call multiple times.
|
|
38
|
+
public func connect(to url: URL) {
|
|
39
|
+
serverURL = url
|
|
40
|
+
reconnectAttempts = 0
|
|
41
|
+
scheduleConnect(delay: 0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Disconnect and stop reconnecting.
|
|
45
|
+
public func disconnect() {
|
|
46
|
+
serverURL = nil
|
|
47
|
+
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
|
48
|
+
webSocketTask = nil
|
|
49
|
+
session?.invalidateAndCancel()
|
|
50
|
+
session = nil
|
|
51
|
+
NSLog("[VueNative HotReload] Disconnected")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Connection
|
|
55
|
+
|
|
56
|
+
private func scheduleConnect(delay: TimeInterval) {
|
|
57
|
+
guard serverURL != nil, !isConnecting else { return }
|
|
58
|
+
isConnecting = true
|
|
59
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
60
|
+
self?.openConnection()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func openConnection() {
|
|
65
|
+
guard let url = serverURL else {
|
|
66
|
+
isConnecting = false
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create a fresh session each time to avoid stale state
|
|
71
|
+
session?.invalidateAndCancel()
|
|
72
|
+
let config = URLSessionConfiguration.default
|
|
73
|
+
config.timeoutIntervalForRequest = 5
|
|
74
|
+
session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
|
|
75
|
+
|
|
76
|
+
webSocketTask = session?.webSocketTask(with: url)
|
|
77
|
+
webSocketTask?.resume()
|
|
78
|
+
NSLog("[VueNative HotReload] Connecting to \(url)...")
|
|
79
|
+
receiveNextMessage()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// MARK: - Message Handling
|
|
83
|
+
|
|
84
|
+
private func receiveNextMessage() {
|
|
85
|
+
webSocketTask?.receive { [weak self] result in
|
|
86
|
+
guard let self = self else { return }
|
|
87
|
+
switch result {
|
|
88
|
+
case .success(let message):
|
|
89
|
+
self.handleMessage(message)
|
|
90
|
+
self.receiveNextMessage()
|
|
91
|
+
case .failure(let error):
|
|
92
|
+
NSLog("[VueNative HotReload] Receive error: \(error.localizedDescription)")
|
|
93
|
+
self.scheduleReconnect()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
|
|
99
|
+
let text: String
|
|
100
|
+
switch message {
|
|
101
|
+
case .string(let s): text = s
|
|
102
|
+
case .data(let d): text = String(data: d, encoding: .utf8) ?? ""
|
|
103
|
+
@unknown default: return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
guard let data = text.data(using: .utf8),
|
|
107
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
108
|
+
let type = json["type"] as? String else {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
switch type {
|
|
113
|
+
case "connected":
|
|
114
|
+
isConnecting = false
|
|
115
|
+
reconnectAttempts = 0
|
|
116
|
+
NSLog("[VueNative HotReload] Connected — hot reload active")
|
|
117
|
+
|
|
118
|
+
case "bundle":
|
|
119
|
+
guard let bundle = json["bundle"] as? String else { return }
|
|
120
|
+
NSLog("[VueNative HotReload] Received bundle (\(bundle.count) bytes) — reloading...")
|
|
121
|
+
DispatchQueue.main.async { [weak self] in
|
|
122
|
+
self?.onBundleReceived?(bundle)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "ping":
|
|
126
|
+
// Respond to keep-alive pings
|
|
127
|
+
let pong = "{\"type\":\"pong\"}"
|
|
128
|
+
webSocketTask?.send(.string(pong)) { _ in }
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// MARK: - Reconnection
|
|
136
|
+
|
|
137
|
+
private func scheduleReconnect() {
|
|
138
|
+
guard serverURL != nil else { return }
|
|
139
|
+
reconnectAttempts += 1
|
|
140
|
+
if reconnectAttempts > maxReconnectAttempts {
|
|
141
|
+
NSLog("[VueNative HotReload] Giving up after %d attempts — start `bun run dev` and relaunch the app", maxReconnectAttempts)
|
|
142
|
+
disconnect()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
isConnecting = false
|
|
146
|
+
NSLog("[VueNative HotReload] Reconnecting in 2s... (attempt %d/%d)", reconnectAttempts, maxReconnectAttempts)
|
|
147
|
+
scheduleConnect(delay: 2.0)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// MARK: - URLSessionWebSocketDelegate
|
|
151
|
+
|
|
152
|
+
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
|
|
153
|
+
didOpenWithProtocol protocol: String?) {
|
|
154
|
+
NSLog("[VueNative HotReload] WebSocket opened")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
|
|
158
|
+
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
|
159
|
+
NSLog("[VueNative HotReload] WebSocket closed (code: \(closeCode.rawValue))")
|
|
160
|
+
scheduleReconnect()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import JavaScriptCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
// MARK: - Bundle Source
|
|
5
|
+
|
|
6
|
+
/// Describes where to load the JS application bundle from.
|
|
7
|
+
public enum BundleSource {
|
|
8
|
+
/// Load from an embedded resource in the app bundle.
|
|
9
|
+
case embedded(name: String)
|
|
10
|
+
/// Load from a development server URL (for live reload).
|
|
11
|
+
case devServer(url: URL)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// MARK: - JSRuntime
|
|
15
|
+
|
|
16
|
+
/// Core JavaScript runtime manager. Wraps JSContext on a dedicated serial DispatchQueue.
|
|
17
|
+
/// All JS operations are guaranteed to execute on the JS queue. UI operations
|
|
18
|
+
/// are never performed on this queue.
|
|
19
|
+
///
|
|
20
|
+
/// Thread safety contract:
|
|
21
|
+
/// - All JSContext access happens exclusively on `jsQueue`
|
|
22
|
+
/// - Never pass JSValue across threads — extract primitives first
|
|
23
|
+
/// - All closures registered with JSContext use [weak self]
|
|
24
|
+
public final class JSRuntime: @unchecked Sendable {
|
|
25
|
+
|
|
26
|
+
// MARK: - Singleton
|
|
27
|
+
|
|
28
|
+
public static let shared = JSRuntime()
|
|
29
|
+
|
|
30
|
+
// MARK: - Properties
|
|
31
|
+
|
|
32
|
+
/// Dedicated serial queue for all JavaScript execution.
|
|
33
|
+
/// QoS is userInteractive because JS drives the UI pipeline.
|
|
34
|
+
public let jsQueue = DispatchQueue(label: "com.vuenative.js", qos: .userInteractive)
|
|
35
|
+
|
|
36
|
+
/// The underlying JavaScriptCore context. Only access on jsQueue.
|
|
37
|
+
public private(set) var context: JSContext!
|
|
38
|
+
|
|
39
|
+
/// Whether the runtime has been initialized.
|
|
40
|
+
public private(set) var isInitialized = false
|
|
41
|
+
|
|
42
|
+
/// Startup time reference for performance.now()
|
|
43
|
+
public let startTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()
|
|
44
|
+
|
|
45
|
+
/// Callback invoked in DEBUG builds when JS throws an exception.
|
|
46
|
+
/// Platform-specific layers set this to show an error overlay.
|
|
47
|
+
public var onError: ((String) -> Void)?
|
|
48
|
+
|
|
49
|
+
// MARK: - Initialization
|
|
50
|
+
|
|
51
|
+
private init() {}
|
|
52
|
+
|
|
53
|
+
/// Initialize the JS runtime. Creates the JSContext on the JS queue,
|
|
54
|
+
/// configures exception handling, and registers polyfills.
|
|
55
|
+
/// Must be called before any other method.
|
|
56
|
+
public func initialize(completion: (() -> Void)? = nil) {
|
|
57
|
+
jsQueue.async { [weak self] in
|
|
58
|
+
guard let self = self else { return }
|
|
59
|
+
guard !self.isInitialized else {
|
|
60
|
+
completion?()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create the JSContext
|
|
65
|
+
self.context = JSContext()
|
|
66
|
+
|
|
67
|
+
// Configure exception handler
|
|
68
|
+
self.context.exceptionHandler = { [weak self] context, exception in
|
|
69
|
+
guard let exception = exception else { return }
|
|
70
|
+
let message = exception.toString() ?? "Unknown JS error"
|
|
71
|
+
let line = exception.objectForKeyedSubscript("line")?.toInt32() ?? 0
|
|
72
|
+
let column = exception.objectForKeyedSubscript("column")?.toInt32() ?? 0
|
|
73
|
+
let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
|
|
74
|
+
NSLog("[VueNative JS Error] \(message) at line \(line):\(column)")
|
|
75
|
+
if !stack.isEmpty {
|
|
76
|
+
NSLog("[VueNative JS Stack] \(stack)")
|
|
77
|
+
}
|
|
78
|
+
#if DEBUG
|
|
79
|
+
let fullMessage = stack.isEmpty ? message : "\(message)\n\n\(stack)"
|
|
80
|
+
self?.onError?(fullMessage)
|
|
81
|
+
#endif
|
|
82
|
+
_ = self // prevent unused warning
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Set globalThis = global object
|
|
86
|
+
self.context.evaluateScript("var globalThis = this;")
|
|
87
|
+
|
|
88
|
+
// Register shared polyfills (everything except platform-specific RAF)
|
|
89
|
+
SharedJSPolyfills.register(in: self)
|
|
90
|
+
|
|
91
|
+
self.isInitialized = true
|
|
92
|
+
completion?()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Script Evaluation
|
|
97
|
+
|
|
98
|
+
/// Evaluate a JavaScript string on the JS queue.
|
|
99
|
+
/// The completion handler is called on the JS queue with the result.
|
|
100
|
+
public func evaluateScript(_ script: String, sourceURL: String? = nil, completion: ((JSValue?) -> Void)? = nil) {
|
|
101
|
+
jsQueue.async { [weak self] in
|
|
102
|
+
guard let self = self, let context = self.context else {
|
|
103
|
+
completion?(nil)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let result: JSValue?
|
|
108
|
+
if let sourceURL = sourceURL, let url = URL(string: sourceURL) {
|
|
109
|
+
result = context.evaluateScript(script, withSourceURL: url)
|
|
110
|
+
} else {
|
|
111
|
+
result = context.evaluateScript(script)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Force microtask drain after evaluation.
|
|
115
|
+
// JSC may not automatically drain microtasks after evaluateScript returns.
|
|
116
|
+
// This ensures Vue's Promise-based scheduler flushes.
|
|
117
|
+
context.evaluateScript("void 0;")
|
|
118
|
+
|
|
119
|
+
completion?(result)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Evaluate a script synchronously. MUST only be called from the JS queue.
|
|
124
|
+
/// Returns the JSValue result directly.
|
|
125
|
+
@discardableResult
|
|
126
|
+
public func evaluateScriptSync(_ script: String, sourceURL: String? = nil) -> JSValue? {
|
|
127
|
+
dispatchPrecondition(condition: .onQueue(jsQueue))
|
|
128
|
+
guard let context = context else { return nil }
|
|
129
|
+
|
|
130
|
+
let result: JSValue?
|
|
131
|
+
if let sourceURL = sourceURL, let url = URL(string: sourceURL) {
|
|
132
|
+
result = context.evaluateScript(script, withSourceURL: url)
|
|
133
|
+
} else {
|
|
134
|
+
result = context.evaluateScript(script)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Force microtask drain
|
|
138
|
+
context.evaluateScript("void 0;")
|
|
139
|
+
return result
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// MARK: - Function Calls
|
|
143
|
+
|
|
144
|
+
/// Call a global JS function by name with arguments. Runs on the JS queue.
|
|
145
|
+
/// Uses objectForKeyedSubscript().call() for performance (avoids re-parsing).
|
|
146
|
+
public func callFunction(_ name: String, withArguments args: [Any], completion: ((JSValue?) -> Void)? = nil) {
|
|
147
|
+
jsQueue.async { [weak self] in
|
|
148
|
+
guard let self = self, let context = self.context else {
|
|
149
|
+
completion?(nil)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let function = context.objectForKeyedSubscript(name)
|
|
154
|
+
guard let fn = function, !fn.isUndefined else {
|
|
155
|
+
NSLog("[VueNative] Warning: JS function '\(name)' not found")
|
|
156
|
+
completion?(nil)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let result = fn.call(withArguments: args)
|
|
161
|
+
|
|
162
|
+
// Force microtask drain
|
|
163
|
+
context.evaluateScript("void 0;")
|
|
164
|
+
|
|
165
|
+
completion?(result)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Call a global JS function synchronously. MUST only be called from the JS queue.
|
|
170
|
+
@discardableResult
|
|
171
|
+
public func callFunctionSync(_ name: String, withArguments args: [Any]) -> JSValue? {
|
|
172
|
+
dispatchPrecondition(condition: .onQueue(jsQueue))
|
|
173
|
+
guard let context = context else { return nil }
|
|
174
|
+
|
|
175
|
+
let function = context.objectForKeyedSubscript(name)
|
|
176
|
+
guard let fn = function, !fn.isUndefined else {
|
|
177
|
+
NSLog("[VueNative] Warning: JS function '\(name)' not found")
|
|
178
|
+
return nil
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let result = fn.call(withArguments: args)
|
|
182
|
+
|
|
183
|
+
// Force microtask drain
|
|
184
|
+
context.evaluateScript("void 0;")
|
|
185
|
+
return result
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// MARK: - Function Registration
|
|
189
|
+
|
|
190
|
+
/// Register a Swift function as a global JS function.
|
|
191
|
+
/// The block receives a single JSValue argument (use .toArray() etc. to extract args).
|
|
192
|
+
/// IMPORTANT: The block executes on the JS queue.
|
|
193
|
+
public func registerFunction(_ name: String, block: @escaping @convention(block) (JSValue) -> Void) {
|
|
194
|
+
jsQueue.async { [weak self] in
|
|
195
|
+
guard let self = self, let context = self.context else { return }
|
|
196
|
+
let wrappedBlock: @convention(block) (JSValue) -> Void = { [weak self] value in
|
|
197
|
+
_ = self // prevent retain cycle through closure capture
|
|
198
|
+
block(value)
|
|
199
|
+
}
|
|
200
|
+
context.setObject(wrappedBlock, forKeyedSubscript: name as NSString)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Register a Swift function with multiple arguments.
|
|
205
|
+
public func registerFunctionMultiArg(_ name: String, argCount: Int, block: @escaping ([JSValue]) -> Void) {
|
|
206
|
+
jsQueue.async { [weak self] in
|
|
207
|
+
guard let self = self, let context = self.context else { return }
|
|
208
|
+
|
|
209
|
+
switch argCount {
|
|
210
|
+
case 0:
|
|
211
|
+
let wrapped: @convention(block) () -> Void = { [weak self] in
|
|
212
|
+
_ = self
|
|
213
|
+
block([])
|
|
214
|
+
}
|
|
215
|
+
context.setObject(wrapped, forKeyedSubscript: name as NSString)
|
|
216
|
+
case 1:
|
|
217
|
+
let wrapped: @convention(block) (JSValue) -> Void = { [weak self] a in
|
|
218
|
+
_ = self
|
|
219
|
+
block([a])
|
|
220
|
+
}
|
|
221
|
+
context.setObject(wrapped, forKeyedSubscript: name as NSString)
|
|
222
|
+
case 2:
|
|
223
|
+
let wrapped: @convention(block) (JSValue, JSValue) -> Void = { [weak self] a, b in
|
|
224
|
+
_ = self
|
|
225
|
+
block([a, b])
|
|
226
|
+
}
|
|
227
|
+
context.setObject(wrapped, forKeyedSubscript: name as NSString)
|
|
228
|
+
case 3:
|
|
229
|
+
let wrapped: @convention(block) (JSValue, JSValue, JSValue) -> Void = { [weak self] a, b, c in
|
|
230
|
+
_ = self
|
|
231
|
+
block([a, b, c])
|
|
232
|
+
}
|
|
233
|
+
context.setObject(wrapped, forKeyedSubscript: name as NSString)
|
|
234
|
+
default:
|
|
235
|
+
// For 4+ args, use the single-arg version and extract from JSValue
|
|
236
|
+
let wrapped: @convention(block) (JSValue) -> Void = { [weak self] value in
|
|
237
|
+
_ = self
|
|
238
|
+
block([value])
|
|
239
|
+
}
|
|
240
|
+
context.setObject(wrapped, forKeyedSubscript: name as NSString)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// MARK: - Bundle Loading
|
|
246
|
+
|
|
247
|
+
/// Load a JS application bundle from the given source.
|
|
248
|
+
/// Polyfills are already registered during initialize().
|
|
249
|
+
public func loadBundle(source: BundleSource, completion: ((Bool) -> Void)? = nil) {
|
|
250
|
+
jsQueue.async { [weak self] in
|
|
251
|
+
guard let self = self, self.context != nil else {
|
|
252
|
+
completion?(false)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
switch source {
|
|
257
|
+
case .embedded(let name):
|
|
258
|
+
self.loadEmbeddedBundle(name: name, completion: completion)
|
|
259
|
+
|
|
260
|
+
case .devServer(let url):
|
|
261
|
+
self.loadDevServerBundle(url: url, completion: completion)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// MARK: - Private: Bundle Loading
|
|
267
|
+
|
|
268
|
+
private func loadEmbeddedBundle(name: String, completion: ((Bool) -> Void)?) {
|
|
269
|
+
// Try to find the bundle in the main app bundle
|
|
270
|
+
let bundleName: String
|
|
271
|
+
let bundleExtension: String
|
|
272
|
+
|
|
273
|
+
if name.contains(".") {
|
|
274
|
+
let parts = name.split(separator: ".", maxSplits: 1)
|
|
275
|
+
bundleName = String(parts[0])
|
|
276
|
+
bundleExtension = String(parts[1])
|
|
277
|
+
} else {
|
|
278
|
+
bundleName = name
|
|
279
|
+
bundleExtension = "js"
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Search in main bundle
|
|
283
|
+
let scriptURL = Bundle.main.url(forResource: bundleName, withExtension: bundleExtension)
|
|
284
|
+
|
|
285
|
+
guard let url = scriptURL else {
|
|
286
|
+
NSLog("[VueNative] Error: Bundle '\(name)' not found in app resources")
|
|
287
|
+
completion?(false)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
do {
|
|
292
|
+
let script = try String(contentsOf: url, encoding: .utf8)
|
|
293
|
+
NSLog("[VueNative] Loading bundle: \(name) (\(script.count) bytes)")
|
|
294
|
+
self.context.evaluateScript(script, withSourceURL: url)
|
|
295
|
+
// Force microtask drain after bundle load
|
|
296
|
+
self.context.evaluateScript("void 0;")
|
|
297
|
+
NSLog("[VueNative] Bundle loaded successfully")
|
|
298
|
+
completion?(true)
|
|
299
|
+
} catch {
|
|
300
|
+
NSLog("[VueNative] Error loading bundle '\(name)': \(error.localizedDescription)")
|
|
301
|
+
completion?(false)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private func loadDevServerBundle(url: URL, completion: ((Bool) -> Void)?) {
|
|
306
|
+
// Fetch from dev server on a background queue, then evaluate on JS queue
|
|
307
|
+
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
|
|
308
|
+
guard let self = self else {
|
|
309
|
+
completion?(false)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
self.jsQueue.async { [weak self] in
|
|
314
|
+
guard let self = self, self.context != nil else {
|
|
315
|
+
completion?(false)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if let error = error {
|
|
320
|
+
NSLog("[VueNative] Dev server error: \(error.localizedDescription)")
|
|
321
|
+
completion?(false)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
guard let data = data, let script = String(data: data, encoding: .utf8) else {
|
|
326
|
+
NSLog("[VueNative] Dev server returned invalid data")
|
|
327
|
+
completion?(false)
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
NSLog("[VueNative] Loading dev bundle from \(url) (\(script.count) bytes)")
|
|
332
|
+
self.context.evaluateScript(script, withSourceURL: url)
|
|
333
|
+
// Force microtask drain
|
|
334
|
+
self.context.evaluateScript("void 0;")
|
|
335
|
+
NSLog("[VueNative] Dev bundle loaded successfully")
|
|
336
|
+
completion?(true)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
task.resume()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// MARK: - Hot Reload
|
|
343
|
+
|
|
344
|
+
/// Reload the runtime with a new JavaScript bundle string.
|
|
345
|
+
/// Creates a fresh JSContext, re-registers polyfills, and evaluates the bundle.
|
|
346
|
+
/// Calls completion on the JS queue with success/failure.
|
|
347
|
+
public func reload(bundle: String, completion: ((Bool) -> Void)? = nil) {
|
|
348
|
+
jsQueue.async { [weak self] in
|
|
349
|
+
guard let self = self else {
|
|
350
|
+
completion?(false)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
NSLog("[VueNative] Hot reload: tearing down old context...")
|
|
355
|
+
|
|
356
|
+
// Tear down old Vue app gracefully
|
|
357
|
+
if let teardown = self.context?.objectForKeyedSubscript("__VN_teardown"),
|
|
358
|
+
!teardown.isUndefined {
|
|
359
|
+
teardown.call(withArguments: [])
|
|
360
|
+
self.context?.evaluateScript("void 0;")
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Reset polyfill state (timers) before creating new context
|
|
364
|
+
SharedJSPolyfills.reset()
|
|
365
|
+
|
|
366
|
+
// Create fresh context
|
|
367
|
+
self.context = JSContext()
|
|
368
|
+
self.context.exceptionHandler = { [weak self] _, exception in
|
|
369
|
+
guard let exception = exception else { return }
|
|
370
|
+
let message = exception.toString() ?? "Unknown JS error"
|
|
371
|
+
let line = exception.objectForKeyedSubscript("line")?.toInt32() ?? 0
|
|
372
|
+
let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
|
|
373
|
+
NSLog("[VueNative JS Error] \(message) at line \(line)")
|
|
374
|
+
#if DEBUG
|
|
375
|
+
let fullMessage = stack.isEmpty ? message : "\(message)\n\n\(stack)"
|
|
376
|
+
self?.onError?(fullMessage)
|
|
377
|
+
#endif
|
|
378
|
+
_ = self
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Re-register globalThis and polyfills
|
|
382
|
+
self.context.evaluateScript("var globalThis = this;")
|
|
383
|
+
SharedJSPolyfills.register(in: self)
|
|
384
|
+
|
|
385
|
+
NSLog("[VueNative] Hot reload: evaluating new bundle (\(bundle.count) bytes)...")
|
|
386
|
+
self.context.evaluateScript(bundle)
|
|
387
|
+
if let exception = self.context.exception {
|
|
388
|
+
NSLog("[VueNative] Hot reload bundle error: %@", exception.toString() ?? "unknown")
|
|
389
|
+
self.context.exception = nil
|
|
390
|
+
completion?(false)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
self.context.evaluateScript("void 0;")
|
|
394
|
+
NSLog("[VueNative] Hot reload: complete")
|
|
395
|
+
|
|
396
|
+
completion?(true)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// MARK: - Teardown
|
|
401
|
+
|
|
402
|
+
/// Invalidate the runtime and release resources.
|
|
403
|
+
/// After calling this, the runtime cannot be used again.
|
|
404
|
+
public func invalidate() {
|
|
405
|
+
jsQueue.async { [weak self] in
|
|
406
|
+
guard let self = self else { return }
|
|
407
|
+
self.context = nil
|
|
408
|
+
self.isInitialized = false
|
|
409
|
+
NSLog("[VueNative] JS runtime invalidated")
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Native module providing async key-value storage backed by UserDefaults.
|
|
4
|
+
///
|
|
5
|
+
/// Methods:
|
|
6
|
+
/// - getItem(key: String) -> String?
|
|
7
|
+
/// - setItem(key: String, value: String)
|
|
8
|
+
/// - removeItem(key: String)
|
|
9
|
+
/// - getAllKeys() -> [String]
|
|
10
|
+
/// - clear()
|
|
11
|
+
public final class AsyncStorageModule: NativeModule {
|
|
12
|
+
public let moduleName = "AsyncStorage"
|
|
13
|
+
|
|
14
|
+
private let defaults = UserDefaults.standard
|
|
15
|
+
private let keyPrefix = "VueNative.AsyncStorage."
|
|
16
|
+
|
|
17
|
+
public init() {}
|
|
18
|
+
|
|
19
|
+
public func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
20
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
21
|
+
guard let self = self else { return }
|
|
22
|
+
switch method {
|
|
23
|
+
case "getItem":
|
|
24
|
+
guard let key = args.first as? String else {
|
|
25
|
+
callback(nil, "getItem: missing key")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
let result = self.defaults.string(forKey: self.keyPrefix + key)
|
|
29
|
+
callback(result, nil)
|
|
30
|
+
|
|
31
|
+
case "setItem":
|
|
32
|
+
guard args.count >= 2,
|
|
33
|
+
let key = args[0] as? String,
|
|
34
|
+
let val = args[1] as? String else {
|
|
35
|
+
callback(nil, "setItem: missing key or value")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
self.defaults.set(val, forKey: self.keyPrefix + key)
|
|
39
|
+
callback(nil, nil)
|
|
40
|
+
|
|
41
|
+
case "removeItem":
|
|
42
|
+
guard let key = args.first as? String else {
|
|
43
|
+
callback(nil, "removeItem: missing key")
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
self.defaults.removeObject(forKey: self.keyPrefix + key)
|
|
47
|
+
callback(nil, nil)
|
|
48
|
+
|
|
49
|
+
case "getAllKeys":
|
|
50
|
+
let allKeys = self.defaults.dictionaryRepresentation().keys
|
|
51
|
+
.filter { $0.hasPrefix(self.keyPrefix) }
|
|
52
|
+
.map { String($0.dropFirst(self.keyPrefix.count)) }
|
|
53
|
+
callback(allKeys, nil)
|
|
54
|
+
|
|
55
|
+
case "clear":
|
|
56
|
+
let allKeys = self.defaults.dictionaryRepresentation().keys
|
|
57
|
+
.filter { $0.hasPrefix(self.keyPrefix) }
|
|
58
|
+
for key in allKeys {
|
|
59
|
+
self.defaults.removeObject(forKey: key)
|
|
60
|
+
}
|
|
61
|
+
callback(nil, nil)
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
callback(nil, "AsyncStorageModule: Unknown method '\(method)'")
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|