@thelacanians/vue-native-cli 0.4.14 → 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 +789 -200
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +156 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +33 -13
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +27 -6
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +9 -2
- 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/Helpers/EventThrottle.kt +57 -0
- 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 +80 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +244 -112
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +19 -2
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +9 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +8 -3
- 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,673 @@
|
|
|
1
|
+
import JavaScriptCore
|
|
2
|
+
import Security
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// Registers browser-like APIs in JSContext that the Vue runtime and application code expect.
|
|
6
|
+
/// All callbacks execute on the JS queue (not the main thread) unless noted otherwise.
|
|
7
|
+
///
|
|
8
|
+
/// This shared version registers everything EXCEPT requestAnimationFrame/cancelAnimationFrame,
|
|
9
|
+
/// which are platform-specific (CADisplayLink on iOS, CVDisplayLink/CADisplayLink on macOS).
|
|
10
|
+
/// Each platform adds its own RAF registration after calling `SharedJSPolyfills.register(in:)`.
|
|
11
|
+
public enum SharedJSPolyfills {
|
|
12
|
+
|
|
13
|
+
// MARK: - Timer storage
|
|
14
|
+
|
|
15
|
+
/// Holds a scheduled Timer alongside its JSManagedValue so both can be cleaned up together.
|
|
16
|
+
private struct TimerEntry {
|
|
17
|
+
let timer: Timer
|
|
18
|
+
let callbackRef: JSManagedValue?
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Serial queue that guards all mutable timer state.
|
|
22
|
+
/// Timer callbacks fire on the main thread and clear/read `timers`;
|
|
23
|
+
/// JS queue closures also mutate `timers` (via setTimeout/clearTimeout).
|
|
24
|
+
/// This queue serializes all access to prevent data races.
|
|
25
|
+
private static let stateQueue = DispatchQueue(label: "com.vuenative.polyfills.state")
|
|
26
|
+
|
|
27
|
+
/// Active timers, keyed by string ID. Access ONLY via `stateQueue`.
|
|
28
|
+
private static var timers: [String: TimerEntry] = [:]
|
|
29
|
+
/// Next timer ID counter. Access ONLY via `stateQueue`.
|
|
30
|
+
private static var nextTimerId: Int = 1
|
|
31
|
+
|
|
32
|
+
// MARK: - RAF storage (managed by platform-specific code)
|
|
33
|
+
|
|
34
|
+
/// Pending requestAnimationFrame callbacks. Access ONLY via `stateQueue`.
|
|
35
|
+
public private(set) static var rafCallbacks: [String: JSValue] = [:]
|
|
36
|
+
/// Next RAF ID counter. Access ONLY via `stateQueue`.
|
|
37
|
+
public private(set) static var nextRafId: Int = 1
|
|
38
|
+
|
|
39
|
+
/// Atomically allocate a RAF ID and store the callback. Returns the ID string.
|
|
40
|
+
/// Platform-specific RAF registration uses this to store callbacks.
|
|
41
|
+
public static func storeRAFCallback(_ callback: JSValue) -> String {
|
|
42
|
+
return stateQueue.sync {
|
|
43
|
+
let id = String(nextRafId)
|
|
44
|
+
nextRafId += 1
|
|
45
|
+
rafCallbacks[id] = callback
|
|
46
|
+
return id
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Remove a RAF callback by ID. Returns true if it was present.
|
|
51
|
+
@discardableResult
|
|
52
|
+
public static func removeRAFCallback(_ id: String) -> Bool {
|
|
53
|
+
return stateQueue.sync {
|
|
54
|
+
return rafCallbacks.removeValue(forKey: id) != nil
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Snapshot and clear all pending RAF callbacks (one-shot semantics).
|
|
59
|
+
/// Platform-specific display link handlers call this each frame.
|
|
60
|
+
public static func drainRAFCallbacks() -> [String: JSValue] {
|
|
61
|
+
return stateQueue.sync {
|
|
62
|
+
let snapshot = rafCallbacks
|
|
63
|
+
rafCallbacks.removeAll()
|
|
64
|
+
return snapshot
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Returns true if there are pending RAF callbacks.
|
|
69
|
+
public static var hasRAFCallbacks: Bool {
|
|
70
|
+
return stateQueue.sync { !rafCallbacks.isEmpty }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// MARK: - Reset
|
|
74
|
+
|
|
75
|
+
/// Reset all polyfill state. Call before creating a fresh JSContext on hot reload.
|
|
76
|
+
/// Safe to call from any thread — synchronizes internally via `stateQueue`.
|
|
77
|
+
/// NOTE: Does NOT touch display links — those are platform-specific.
|
|
78
|
+
/// Each platform must invalidate its own display link separately.
|
|
79
|
+
public static func reset() {
|
|
80
|
+
// Snapshot and clear timer/RAF state under the lock
|
|
81
|
+
let oldTimers: [String: TimerEntry] = stateQueue.sync {
|
|
82
|
+
let snapshot = timers
|
|
83
|
+
timers.removeAll()
|
|
84
|
+
rafCallbacks.removeAll()
|
|
85
|
+
nextTimerId = 1
|
|
86
|
+
nextRafId = 1
|
|
87
|
+
return snapshot
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Invalidate timers on the main thread (where they were scheduled)
|
|
91
|
+
DispatchQueue.main.async {
|
|
92
|
+
for (_, entry) in oldTimers {
|
|
93
|
+
entry.timer.invalidate()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Registration
|
|
99
|
+
|
|
100
|
+
/// Register all shared polyfills into the given JSRuntime's context.
|
|
101
|
+
/// MUST be called on the JS queue.
|
|
102
|
+
/// Does NOT register requestAnimationFrame — each platform adds that separately.
|
|
103
|
+
public static func register(in runtime: JSRuntime) {
|
|
104
|
+
guard let context = runtime.context else { return }
|
|
105
|
+
|
|
106
|
+
registerConsole(in: context)
|
|
107
|
+
registerTimers(in: context, runtime: runtime)
|
|
108
|
+
registerMicrotask(in: context)
|
|
109
|
+
registerPerformance(in: context, runtime: runtime)
|
|
110
|
+
registerGlobalThis(in: context)
|
|
111
|
+
registerFetch(in: context, runtime: runtime)
|
|
112
|
+
registerBase64(in: context)
|
|
113
|
+
registerTextEncoding(in: context)
|
|
114
|
+
registerURL(in: context)
|
|
115
|
+
registerCrypto(in: context)
|
|
116
|
+
registerBridgeStubs(in: context)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - Bridge callback stubs
|
|
120
|
+
|
|
121
|
+
/// Register no-op stubs for global functions that the bundle will overwrite.
|
|
122
|
+
/// Native modules may call dispatchGlobalEvent() before the JS bundle has
|
|
123
|
+
/// loaded and registered the real handlers — these stubs prevent the
|
|
124
|
+
/// "JS function 'X' not found" warnings during that window.
|
|
125
|
+
private static func registerBridgeStubs(in context: JSContext) {
|
|
126
|
+
context.evaluateScript("""
|
|
127
|
+
if (typeof __VN_handleGlobalEvent === 'undefined') {
|
|
128
|
+
globalThis.__VN_handleGlobalEvent = function() {};
|
|
129
|
+
}
|
|
130
|
+
if (typeof __VN_handleEvent === 'undefined') {
|
|
131
|
+
globalThis.__VN_handleEvent = function() {};
|
|
132
|
+
}
|
|
133
|
+
if (typeof __VN_resolveCallback === 'undefined') {
|
|
134
|
+
globalThis.__VN_resolveCallback = function() {};
|
|
135
|
+
}
|
|
136
|
+
""")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - console.log / warn / error
|
|
140
|
+
|
|
141
|
+
private static func registerConsole(in context: JSContext) {
|
|
142
|
+
// Create a console object
|
|
143
|
+
context.evaluateScript("var console = {};")
|
|
144
|
+
|
|
145
|
+
/// Helper: format all arguments passed from JS into a single space-separated string.
|
|
146
|
+
func formatArgs() -> String {
|
|
147
|
+
guard let args = JSContext.currentArguments() as? [JSValue], !args.isEmpty else {
|
|
148
|
+
return ""
|
|
149
|
+
}
|
|
150
|
+
return args.map { value in
|
|
151
|
+
value.isUndefined ? "undefined" : (value.toString() ?? "null")
|
|
152
|
+
}.joined(separator: " ")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let consoleLog: @convention(block) () -> Void = {
|
|
156
|
+
NSLog("[VueNative LOG] %@", formatArgs())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let consoleWarn: @convention(block) () -> Void = {
|
|
160
|
+
NSLog("[VueNative WARN] %@", formatArgs())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let consoleError: @convention(block) () -> Void = {
|
|
164
|
+
NSLog("[VueNative ERROR] %@", formatArgs())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let consoleDebug: @convention(block) () -> Void = {
|
|
168
|
+
NSLog("[VueNative DEBUG] %@", formatArgs())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let consoleInfo: @convention(block) () -> Void = {
|
|
172
|
+
NSLog("[VueNative INFO] %@", formatArgs())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
guard let consoleObj = context.objectForKeyedSubscript("console") else {
|
|
176
|
+
NSLog("[VueNative] Warning: failed to get console object")
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
consoleObj.setObject(consoleLog, forKeyedSubscript: "log" as NSString)
|
|
180
|
+
consoleObj.setObject(consoleWarn, forKeyedSubscript: "warn" as NSString)
|
|
181
|
+
consoleObj.setObject(consoleError, forKeyedSubscript: "error" as NSString)
|
|
182
|
+
consoleObj.setObject(consoleDebug, forKeyedSubscript: "debug" as NSString)
|
|
183
|
+
consoleObj.setObject(consoleInfo, forKeyedSubscript: "info" as NSString)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// MARK: - setTimeout / clearTimeout / setInterval / clearInterval
|
|
187
|
+
|
|
188
|
+
private static func registerTimers(in context: JSContext, runtime: JSRuntime) {
|
|
189
|
+
|
|
190
|
+
// setTimeout(callback, delay) -> timerId (String)
|
|
191
|
+
let setTimeout: @convention(block) (JSValue, JSValue) -> JSValue = { [weak runtime] callback, delay in
|
|
192
|
+
guard let runtime = runtime, let context = runtime.context else {
|
|
193
|
+
return JSValue(nullIn: JSContext.current())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let delayMs = delay.isUndefined ? 0.0 : delay.toDouble()
|
|
197
|
+
let timerId: String = stateQueue.sync {
|
|
198
|
+
let id = String(nextTimerId)
|
|
199
|
+
nextTimerId += 1
|
|
200
|
+
return id
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Protect callback from GC by storing in the context
|
|
204
|
+
let callbackRef = JSManagedValue(value: callback)
|
|
205
|
+
context.virtualMachine.addManagedReference(callbackRef, withOwner: context)
|
|
206
|
+
|
|
207
|
+
// Schedule timer on the main thread RunLoop, then dispatch callback to JS queue
|
|
208
|
+
DispatchQueue.main.async {
|
|
209
|
+
let timer = Timer.scheduledTimer(withTimeInterval: max(delayMs / 1000.0, 0.001), repeats: false) { [weak runtime] _ in
|
|
210
|
+
guard let runtime = runtime else { return }
|
|
211
|
+
runtime.jsQueue.async { [weak runtime] in
|
|
212
|
+
guard let runtime = runtime, let context = runtime.context else { return }
|
|
213
|
+
// Timer already fired — only invoke if not cleared
|
|
214
|
+
let stillActive: Bool = stateQueue.sync {
|
|
215
|
+
guard timers[timerId] != nil else { return false }
|
|
216
|
+
timers.removeValue(forKey: timerId)
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
guard stillActive else { return }
|
|
220
|
+
if let cb = callbackRef?.value, !cb.isUndefined {
|
|
221
|
+
cb.call(withArguments: [])
|
|
222
|
+
// Drain microtasks after timer callback
|
|
223
|
+
context.evaluateScript("void 0;")
|
|
224
|
+
}
|
|
225
|
+
context.virtualMachine.removeManagedReference(callbackRef, withOwner: context)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
229
|
+
stateQueue.async {
|
|
230
|
+
// Only store if not already cleared before the timer was created
|
|
231
|
+
if timers[timerId] == nil {
|
|
232
|
+
timers[timerId] = TimerEntry(timer: timer, callbackRef: callbackRef)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return JSValue(object: timerId, in: context)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// clearTimeout(timerId)
|
|
241
|
+
let clearTimeout: @convention(block) (JSValue) -> Void = { [weak runtime] timerId in
|
|
242
|
+
guard let runtime = runtime, let context = runtime.context else { return }
|
|
243
|
+
guard let id = timerId.toString() else { return }
|
|
244
|
+
let entry: TimerEntry? = stateQueue.sync {
|
|
245
|
+
return timers.removeValue(forKey: id)
|
|
246
|
+
}
|
|
247
|
+
if let entry = entry {
|
|
248
|
+
// Remove the managed reference so the JSValue can be GC'd
|
|
249
|
+
context.virtualMachine.removeManagedReference(entry.callbackRef, withOwner: context)
|
|
250
|
+
// Timer invalidation must happen on the main thread where it was created
|
|
251
|
+
DispatchQueue.main.async {
|
|
252
|
+
entry.timer.invalidate()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// setInterval(callback, delay) -> timerId (String)
|
|
258
|
+
let setInterval: @convention(block) (JSValue, JSValue) -> JSValue = { [weak runtime] callback, delay in
|
|
259
|
+
guard let runtime = runtime, let context = runtime.context else {
|
|
260
|
+
return JSValue(nullIn: JSContext.current())
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let delayMs = delay.isUndefined ? 0.0 : delay.toDouble()
|
|
264
|
+
let timerId: String = stateQueue.sync {
|
|
265
|
+
let id = String(nextTimerId)
|
|
266
|
+
nextTimerId += 1
|
|
267
|
+
return id
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let callbackRef = JSManagedValue(value: callback)
|
|
271
|
+
context.virtualMachine.addManagedReference(callbackRef, withOwner: context)
|
|
272
|
+
|
|
273
|
+
DispatchQueue.main.async {
|
|
274
|
+
let timer = Timer.scheduledTimer(withTimeInterval: max(delayMs / 1000.0, 0.001), repeats: true) { [weak runtime] _ in
|
|
275
|
+
guard let runtime = runtime else { return }
|
|
276
|
+
runtime.jsQueue.async { [weak runtime] in
|
|
277
|
+
guard let runtime = runtime, let context = runtime.context else { return }
|
|
278
|
+
// If the interval was cleared, do not invoke the callback
|
|
279
|
+
let stillActive: Bool = stateQueue.sync { timers[timerId] != nil }
|
|
280
|
+
guard stillActive else { return }
|
|
281
|
+
if let cb = callbackRef?.value, !cb.isUndefined {
|
|
282
|
+
cb.call(withArguments: [])
|
|
283
|
+
context.evaluateScript("void 0;")
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
288
|
+
stateQueue.async {
|
|
289
|
+
timers[timerId] = TimerEntry(timer: timer, callbackRef: callbackRef)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return JSValue(object: timerId, in: context)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// clearInterval(timerId)
|
|
297
|
+
let clearInterval: @convention(block) (JSValue) -> Void = { [weak runtime] timerId in
|
|
298
|
+
guard let runtime = runtime, let context = runtime.context else { return }
|
|
299
|
+
guard let id = timerId.toString() else { return }
|
|
300
|
+
let entry: TimerEntry? = stateQueue.sync {
|
|
301
|
+
return timers.removeValue(forKey: id)
|
|
302
|
+
}
|
|
303
|
+
if let entry = entry {
|
|
304
|
+
// Remove the managed reference so the JSValue can be GC'd
|
|
305
|
+
context.virtualMachine.removeManagedReference(entry.callbackRef, withOwner: context)
|
|
306
|
+
// Invalidate the timer on the main thread where it was scheduled
|
|
307
|
+
DispatchQueue.main.async {
|
|
308
|
+
entry.timer.invalidate()
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
context.setObject(setTimeout, forKeyedSubscript: "setTimeout" as NSString)
|
|
314
|
+
context.setObject(clearTimeout, forKeyedSubscript: "clearTimeout" as NSString)
|
|
315
|
+
context.setObject(setInterval, forKeyedSubscript: "setInterval" as NSString)
|
|
316
|
+
context.setObject(clearInterval, forKeyedSubscript: "clearInterval" as NSString)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// MARK: - queueMicrotask
|
|
320
|
+
|
|
321
|
+
private static func registerMicrotask(in context: JSContext) {
|
|
322
|
+
// queueMicrotask uses Promise.resolve().then() since JSC has native Promise support.
|
|
323
|
+
// This is exactly what Vue's scheduler uses internally.
|
|
324
|
+
context.evaluateScript("""
|
|
325
|
+
function queueMicrotask(callback) {
|
|
326
|
+
Promise.resolve().then(callback);
|
|
327
|
+
}
|
|
328
|
+
""")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// MARK: - performance.now()
|
|
332
|
+
|
|
333
|
+
private static func registerPerformance(in context: JSContext, runtime: JSRuntime) {
|
|
334
|
+
context.evaluateScript("var performance = {};")
|
|
335
|
+
|
|
336
|
+
let performanceNow: @convention(block) () -> Double = { [weak runtime] in
|
|
337
|
+
guard let runtime = runtime else { return 0 }
|
|
338
|
+
// Return milliseconds since runtime start
|
|
339
|
+
return (CFAbsoluteTimeGetCurrent() - runtime.startTime) * 1000.0
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
guard let perfObj = context.objectForKeyedSubscript("performance") else {
|
|
343
|
+
NSLog("[VueNative] Warning: failed to get performance object")
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
perfObj.setObject(performanceNow, forKeyedSubscript: "now" as NSString)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// MARK: - globalThis
|
|
350
|
+
|
|
351
|
+
private static func registerGlobalThis(in context: JSContext) {
|
|
352
|
+
// Ensure globalThis points to the global object (may already be set)
|
|
353
|
+
context.evaluateScript("""
|
|
354
|
+
if (typeof globalThis === 'undefined') {
|
|
355
|
+
var globalThis = this;
|
|
356
|
+
}
|
|
357
|
+
""")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// MARK: - fetch
|
|
361
|
+
|
|
362
|
+
private static func registerFetch(in context: JSContext, runtime: JSRuntime) {
|
|
363
|
+
// Register __VN_configurePins(pinsJSON) for certificate pinning from JS.
|
|
364
|
+
// pinsJSON is a JSON string: { "domain": ["sha256/hash1", "sha256/hash2"] }
|
|
365
|
+
let configurePins: @convention(block) (JSValue) -> Void = { pinsValue in
|
|
366
|
+
guard let jsonString = pinsValue.toString(),
|
|
367
|
+
let data = jsonString.data(using: .utf8),
|
|
368
|
+
let pinsDict = try? JSONSerialization.jsonObject(with: data) as? [String: [String]] else {
|
|
369
|
+
NSLog("[VueNative CertPin] Invalid pins configuration")
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
CertificatePinning.shared.configurePins(pinsDict)
|
|
373
|
+
NSLog("[VueNative CertPin] Configured pins for %d domains", pinsDict.count)
|
|
374
|
+
}
|
|
375
|
+
context.setObject(configurePins, forKeyedSubscript: "__VN_configurePins" as NSString)
|
|
376
|
+
|
|
377
|
+
// fetch(url, options?) -> Promise<Response>
|
|
378
|
+
let fetch: @convention(block) (JSValue, JSValue) -> JSValue = { [weak runtime] urlValue, optionsValue in
|
|
379
|
+
guard let runtime = runtime, let context = runtime.context else {
|
|
380
|
+
return JSValue(undefinedIn: JSContext.current())
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let urlString = urlValue.toString() ?? ""
|
|
384
|
+
guard let url = URL(string: urlString) else {
|
|
385
|
+
return context.evaluateScript("Promise.reject(new TypeError('Invalid URL'))") ?? JSValue(undefinedIn: context)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Build URLRequest
|
|
389
|
+
var request = URLRequest(url: url)
|
|
390
|
+
|
|
391
|
+
if !optionsValue.isUndefined && !optionsValue.isNull {
|
|
392
|
+
if let method = optionsValue.objectForKeyedSubscript("method")?.toString() {
|
|
393
|
+
request.httpMethod = method.uppercased()
|
|
394
|
+
} else {
|
|
395
|
+
request.httpMethod = "GET"
|
|
396
|
+
}
|
|
397
|
+
if let headersObj = optionsValue.objectForKeyedSubscript("headers"),
|
|
398
|
+
!headersObj.isUndefined, !headersObj.isNull,
|
|
399
|
+
let headers = headersObj.toDictionary() as? [String: String] {
|
|
400
|
+
for (key, val) in headers {
|
|
401
|
+
request.setValue(val, forHTTPHeaderField: key)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if let body = optionsValue.objectForKeyedSubscript("body"),
|
|
405
|
+
!body.isUndefined, !body.isNull,
|
|
406
|
+
let bodyStr = body.toString() {
|
|
407
|
+
request.httpBody = bodyStr.data(using: .utf8)
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
request.httpMethod = "GET"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Create a Promise via JS with captured resolve/reject
|
|
414
|
+
var resolveRef: JSValue?
|
|
415
|
+
var rejectRef: JSValue?
|
|
416
|
+
|
|
417
|
+
let captureExecutor: @convention(block) (JSValue, JSValue) -> Void = { resolve, reject in
|
|
418
|
+
resolveRef = resolve
|
|
419
|
+
rejectRef = reject
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let promiseCtor = context.evaluateScript("""
|
|
423
|
+
(function(executor) {
|
|
424
|
+
return new Promise(executor);
|
|
425
|
+
})
|
|
426
|
+
""")
|
|
427
|
+
let captureBlock = JSValue(object: captureExecutor as AnyObject, in: context)
|
|
428
|
+
let promise = promiseCtor?.call(withArguments: [captureBlock as Any])
|
|
429
|
+
|
|
430
|
+
// Use the pinning session when pins are configured for this host,
|
|
431
|
+
// otherwise fall back to URLSession.shared for zero overhead.
|
|
432
|
+
let host = url.host ?? ""
|
|
433
|
+
let urlSession = CertificatePinning.shared.hasPins(for: host)
|
|
434
|
+
? CertificatePinning.shared.session
|
|
435
|
+
: URLSession.shared
|
|
436
|
+
|
|
437
|
+
let task = urlSession.dataTask(with: request) { [weak runtime] data, response, error in
|
|
438
|
+
guard let runtime = runtime else { return }
|
|
439
|
+
runtime.jsQueue.async { [weak runtime] in
|
|
440
|
+
guard runtime != nil, let context = runtime?.context else { return }
|
|
441
|
+
|
|
442
|
+
if let error = error {
|
|
443
|
+
let errMsg = error.localizedDescription
|
|
444
|
+
if let reject = rejectRef, !reject.isUndefined {
|
|
445
|
+
let errObj = context.evaluateScript("new Error(\(SharedJSPolyfillsJSON.encode(errMsg)))")
|
|
446
|
+
reject.call(withArguments: [errObj as Any])
|
|
447
|
+
}
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let httpResponse = response as? HTTPURLResponse
|
|
452
|
+
let status = httpResponse?.statusCode ?? 200
|
|
453
|
+
let ok = (200...299).contains(status)
|
|
454
|
+
let bodyData = data ?? Data()
|
|
455
|
+
let bodyString = String(data: bodyData, encoding: .utf8) ?? ""
|
|
456
|
+
|
|
457
|
+
// Build headers dictionary
|
|
458
|
+
var headersDict: [String: String] = [:]
|
|
459
|
+
if let httpResp = httpResponse {
|
|
460
|
+
for (k, v) in httpResp.allHeaderFields {
|
|
461
|
+
headersDict["\(k)"] = "\(v)"
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Create response object in JS
|
|
466
|
+
if let resolve = resolveRef, !resolve.isUndefined {
|
|
467
|
+
guard let responseObj = JSValue(newObjectIn: context) else {
|
|
468
|
+
NSLog("[VueNative] Warning: failed to create response object")
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
responseObj.setObject(status, forKeyedSubscript: "status" as NSString)
|
|
472
|
+
responseObj.setObject(ok, forKeyedSubscript: "ok" as NSString)
|
|
473
|
+
responseObj.setObject(bodyString, forKeyedSubscript: "_body" as NSString)
|
|
474
|
+
|
|
475
|
+
// headers object
|
|
476
|
+
if let headersObj = JSValue(newObjectIn: context) {
|
|
477
|
+
for (k, v) in headersDict {
|
|
478
|
+
headersObj.setObject(v, forKeyedSubscript: k as NSString)
|
|
479
|
+
}
|
|
480
|
+
responseObj.setObject(headersObj, forKeyedSubscript: "headers" as NSString)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// .text() method
|
|
484
|
+
let bodyStringCopy = bodyString
|
|
485
|
+
let textMethod: @convention(block) () -> JSValue = {
|
|
486
|
+
return context.evaluateScript("Promise.resolve(\(SharedJSPolyfillsJSON.encode(bodyStringCopy)))") ?? JSValue(undefinedIn: context)
|
|
487
|
+
}
|
|
488
|
+
responseObj.setObject(textMethod, forKeyedSubscript: "text" as NSString)
|
|
489
|
+
|
|
490
|
+
// .json() method
|
|
491
|
+
let jsonMethod: @convention(block) () -> JSValue = {
|
|
492
|
+
return context.evaluateScript("(function(s){ try { return Promise.resolve(JSON.parse(s)); } catch(e) { return Promise.reject(e); } })(\(SharedJSPolyfillsJSON.encode(bodyStringCopy)))") ?? JSValue(undefinedIn: context)
|
|
493
|
+
}
|
|
494
|
+
responseObj.setObject(jsonMethod, forKeyedSubscript: "json" as NSString)
|
|
495
|
+
|
|
496
|
+
resolve.call(withArguments: [responseObj])
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
task.resume()
|
|
501
|
+
|
|
502
|
+
return promise ?? JSValue(undefinedIn: context)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
context.setObject(fetch, forKeyedSubscript: "fetch" as NSString)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// MARK: - atob / btoa (Base64)
|
|
509
|
+
|
|
510
|
+
private static func registerBase64(in context: JSContext) {
|
|
511
|
+
// atob — decode a base64-encoded string
|
|
512
|
+
let atob: @convention(block) (String) -> String = { encoded in
|
|
513
|
+
guard let data = Data(base64Encoded: encoded) else { return "" }
|
|
514
|
+
return String(data: data, encoding: .utf8) ?? ""
|
|
515
|
+
}
|
|
516
|
+
context.setObject(atob, forKeyedSubscript: "atob" as NSString)
|
|
517
|
+
|
|
518
|
+
// btoa — encode a string to base64
|
|
519
|
+
let btoa: @convention(block) (String) -> String = { str in
|
|
520
|
+
guard let data = str.data(using: .utf8) else { return "" }
|
|
521
|
+
return data.base64EncodedString()
|
|
522
|
+
}
|
|
523
|
+
context.setObject(btoa, forKeyedSubscript: "btoa" as NSString)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// MARK: - TextEncoder / TextDecoder
|
|
527
|
+
|
|
528
|
+
private static func registerTextEncoding(in context: JSContext) {
|
|
529
|
+
context.evaluateScript("""
|
|
530
|
+
class TextEncoder {
|
|
531
|
+
constructor(encoding = 'utf-8') { this.encoding = encoding; }
|
|
532
|
+
encode(str) {
|
|
533
|
+
const arr = [];
|
|
534
|
+
for (let i = 0; i < str.length; i++) {
|
|
535
|
+
let c = str.charCodeAt(i);
|
|
536
|
+
if (c < 0x80) { arr.push(c); }
|
|
537
|
+
else if (c < 0x800) { arr.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); }
|
|
538
|
+
else if (c < 0xd800 || c >= 0xe000) { arr.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }
|
|
539
|
+
else { i++; c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); arr.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }
|
|
540
|
+
}
|
|
541
|
+
return new Uint8Array(arr);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
class TextDecoder {
|
|
545
|
+
constructor(encoding = 'utf-8') { this.encoding = encoding; }
|
|
546
|
+
decode(buffer) {
|
|
547
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
548
|
+
let result = '';
|
|
549
|
+
for (let i = 0; i < bytes.length;) {
|
|
550
|
+
let c = bytes[i++];
|
|
551
|
+
if (c < 0x80) { result += String.fromCharCode(c); }
|
|
552
|
+
else if (c < 0xe0) { result += String.fromCharCode(((c & 0x1f) << 6) | (bytes[i++] & 0x3f)); }
|
|
553
|
+
else if (c < 0xf0) { result += String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f)); }
|
|
554
|
+
else { const cp = ((c & 0x07) << 18) | ((bytes[i++] & 0x3f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f); result += String.fromCodePoint(cp); }
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
globalThis.TextEncoder = TextEncoder;
|
|
560
|
+
globalThis.TextDecoder = TextDecoder;
|
|
561
|
+
""")
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// MARK: - URL / URLSearchParams
|
|
565
|
+
|
|
566
|
+
private static func registerURL(in context: JSContext) {
|
|
567
|
+
context.evaluateScript("""
|
|
568
|
+
if (typeof URL === 'undefined') {
|
|
569
|
+
class URL {
|
|
570
|
+
constructor(url, base) {
|
|
571
|
+
if (base) {
|
|
572
|
+
if (!url.match(/^[a-zA-Z]+:/)) {
|
|
573
|
+
const b = new URL(base);
|
|
574
|
+
url = b.origin + (url.startsWith('/') ? '' : '/') + url;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const match = url.match(/^([a-zA-Z]+:)\\/\\/([^/:]+)(:\\d+)?(\\/[^?#]*)?(\\?[^#]*)?(#.*)?$/);
|
|
578
|
+
if (!match) { this.href = url; this.protocol = ''; this.host = ''; this.hostname = ''; this.port = ''; this.pathname = '/'; this.search = ''; this.hash = ''; this.origin = ''; this.searchParams = new URLSearchParams(''); return; }
|
|
579
|
+
this.protocol = match[1] || '';
|
|
580
|
+
this.hostname = match[2] || '';
|
|
581
|
+
this.port = (match[3] || '').slice(1);
|
|
582
|
+
this.host = this.hostname + (this.port ? ':' + this.port : '');
|
|
583
|
+
this.pathname = match[4] || '/';
|
|
584
|
+
this.search = match[5] || '';
|
|
585
|
+
this.hash = match[6] || '';
|
|
586
|
+
this.origin = this.protocol + '//' + this.host;
|
|
587
|
+
this.href = url;
|
|
588
|
+
this.searchParams = new URLSearchParams(this.search);
|
|
589
|
+
}
|
|
590
|
+
toString() { return this.href; }
|
|
591
|
+
}
|
|
592
|
+
globalThis.URL = URL;
|
|
593
|
+
}
|
|
594
|
+
if (typeof URLSearchParams === 'undefined') {
|
|
595
|
+
class URLSearchParams {
|
|
596
|
+
constructor(init) {
|
|
597
|
+
this._params = [];
|
|
598
|
+
if (typeof init === 'string') {
|
|
599
|
+
init.replace(/^\\?/, '').split('&').filter(Boolean).forEach(p => {
|
|
600
|
+
const [k, ...v] = p.split('=');
|
|
601
|
+
this._params.push([decodeURIComponent(k), decodeURIComponent(v.join('='))]);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
get(name) { const p = this._params.find(([k]) => k === name); return p ? p[1] : null; }
|
|
606
|
+
getAll(name) { return this._params.filter(([k]) => k === name).map(([,v]) => v); }
|
|
607
|
+
has(name) { return this._params.some(([k]) => k === name); }
|
|
608
|
+
set(name, value) { this.delete(name); this._params.push([name, String(value)]); }
|
|
609
|
+
append(name, value) { this._params.push([name, String(value)]); }
|
|
610
|
+
delete(name) { this._params = this._params.filter(([k]) => k !== name); }
|
|
611
|
+
toString() { return this._params.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&'); }
|
|
612
|
+
forEach(cb) { this._params.forEach(([k, v]) => cb(v, k, this)); }
|
|
613
|
+
entries() { return this._params[Symbol.iterator](); }
|
|
614
|
+
keys() { return this._params.map(([k]) => k)[Symbol.iterator](); }
|
|
615
|
+
values() { return this._params.map(([,v]) => v)[Symbol.iterator](); }
|
|
616
|
+
[Symbol.iterator]() { return this.entries(); }
|
|
617
|
+
}
|
|
618
|
+
globalThis.URLSearchParams = URLSearchParams;
|
|
619
|
+
}
|
|
620
|
+
""")
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// MARK: - crypto.getRandomValues
|
|
624
|
+
|
|
625
|
+
private static func registerCrypto(in context: JSContext) {
|
|
626
|
+
// Native callback using SecRandomCopyBytes for cryptographic randomness
|
|
627
|
+
let cryptoGetRandomValues: @convention(block) (JSValue) -> JSValue = { typedArray in
|
|
628
|
+
let length = typedArray.forProperty("length").toInt32()
|
|
629
|
+
if length > 0 {
|
|
630
|
+
var bytes = [UInt8](repeating: 0, count: Int(length))
|
|
631
|
+
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
|
632
|
+
for i in 0..<Int(length) {
|
|
633
|
+
typedArray.setValue(bytes[i], at: i)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return typedArray
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Ensure the crypto global object exists
|
|
640
|
+
context.evaluateScript("""
|
|
641
|
+
if (typeof crypto === 'undefined') {
|
|
642
|
+
globalThis.crypto = {};
|
|
643
|
+
}
|
|
644
|
+
""")
|
|
645
|
+
|
|
646
|
+
if let cryptoObj = context.objectForKeyedSubscript("crypto") {
|
|
647
|
+
cryptoObj.setObject(cryptoGetRandomValues, forKeyedSubscript: "getRandomValues" as NSString)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// MARK: - JSON encode helper
|
|
653
|
+
|
|
654
|
+
/// Produce a JSON-safe string literal (with quotes) for embedding in JS eval strings.
|
|
655
|
+
public enum SharedJSPolyfillsJSON {
|
|
656
|
+
public static func encode(_ str: String) -> String {
|
|
657
|
+
// Wrap in array so JSONSerialization gets a valid top-level type.
|
|
658
|
+
// String alone causes an NSException that try? cannot catch.
|
|
659
|
+
if let data = try? JSONSerialization.data(withJSONObject: [str]),
|
|
660
|
+
let json = String(data: data, encoding: .utf8),
|
|
661
|
+
json.count >= 2 {
|
|
662
|
+
return String(json.dropFirst().dropLast())
|
|
663
|
+
}
|
|
664
|
+
// Fallback: manual escaping
|
|
665
|
+
let escaped = str
|
|
666
|
+
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
667
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
668
|
+
.replacingOccurrences(of: "\n", with: "\\n")
|
|
669
|
+
.replacingOccurrences(of: "\r", with: "\\r")
|
|
670
|
+
.replacingOccurrences(of: "\t", with: "\\t")
|
|
671
|
+
return "\"\(escaped)\""
|
|
672
|
+
}
|
|
673
|
+
}
|