@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,80 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import VueNativeMacOS
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
final class ComponentFactoryTests: XCTestCase {
|
|
7
|
+
|
|
8
|
+
func testVListFactoryAppliesPublicProps() {
|
|
9
|
+
let factory = VListFactory()
|
|
10
|
+
guard let container = factory.createView() as? VListContainerView else {
|
|
11
|
+
return XCTFail("Expected VListContainerView")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
factory.updateProp(view: container, key: "estimatedItemHeight", value: 72)
|
|
15
|
+
factory.updateProp(view: container, key: "showsScrollIndicator", value: false)
|
|
16
|
+
factory.updateProp(view: container, key: "bounces", value: false)
|
|
17
|
+
|
|
18
|
+
XCTAssertEqual(container.estimatedItemHeight, 72)
|
|
19
|
+
XCTAssertFalse(container.scrollView.hasVerticalScroller)
|
|
20
|
+
XCTAssertEqual(container.scrollView.verticalScrollElasticity, .none)
|
|
21
|
+
XCTAssertEqual(container.scrollView.horizontalScrollElasticity, .none)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testVSectionListFactoryBuildsRowsFromInsertedChildren() {
|
|
25
|
+
let factory = VSectionListFactory()
|
|
26
|
+
guard let container = factory.createView() as? VSectionListContainerView else {
|
|
27
|
+
return XCTFail("Expected VSectionListContainerView")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let header = FlippedView(frame: NSRect(x: 0, y: 0, width: 200, height: 28))
|
|
31
|
+
header.ensureLayoutNode()
|
|
32
|
+
StyleEngine.setInternalPropDirect("__sectionHeader", value: true, on: header)
|
|
33
|
+
|
|
34
|
+
let item1 = FlippedView(frame: NSRect(x: 0, y: 0, width: 200, height: 44))
|
|
35
|
+
item1.ensureLayoutNode()
|
|
36
|
+
let item2 = FlippedView(frame: NSRect(x: 0, y: 0, width: 200, height: 44))
|
|
37
|
+
item2.ensureLayoutNode()
|
|
38
|
+
|
|
39
|
+
factory.insertChild(header, into: container, before: nil)
|
|
40
|
+
factory.insertChild(item1, into: container, before: nil)
|
|
41
|
+
factory.insertChild(item2, into: container, before: nil)
|
|
42
|
+
|
|
43
|
+
XCTAssertEqual(container.numberOfRows(in: container.tableView), 3)
|
|
44
|
+
XCTAssertTrue(container.tableView(container.tableView, isGroupRow: 0))
|
|
45
|
+
XCTAssertFalse(container.tableView(container.tableView, isGroupRow: 1))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func testVSectionListFactoryEmitsFlatScrollPayload() {
|
|
49
|
+
let factory = VSectionListFactory()
|
|
50
|
+
guard let container = factory.createView() as? VSectionListContainerView else {
|
|
51
|
+
return XCTFail("Expected VSectionListContainerView")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var payload: [String: Any]?
|
|
55
|
+
factory.addEventListener(view: container, event: "scroll") { eventPayload in
|
|
56
|
+
payload = eventPayload as? [String: Any]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
container.scrollView.frame = NSRect(x: 0, y: 0, width: 120, height: 240)
|
|
60
|
+
container.tableView.frame = NSRect(x: 0, y: 0, width: 240, height: 960)
|
|
61
|
+
container.scrollView.contentView.scroll(to: NSPoint(x: 12, y: 34))
|
|
62
|
+
NotificationCenter.default.post(
|
|
63
|
+
name: NSView.boundsDidChangeNotification,
|
|
64
|
+
object: container.scrollView.contentView
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
guard let payload else {
|
|
68
|
+
return XCTFail("Expected scroll payload")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
XCTAssertEqual(payload["x"] as? CGFloat, 12)
|
|
72
|
+
XCTAssertEqual(payload["y"] as? CGFloat, 34)
|
|
73
|
+
XCTAssertEqual(payload["contentWidth"] as? CGFloat, 240)
|
|
74
|
+
XCTAssertEqual(payload["contentHeight"] as? CGFloat, 960)
|
|
75
|
+
XCTAssertEqual(payload["layoutWidth"] as? CGFloat, 120)
|
|
76
|
+
XCTAssertEqual(payload["layoutHeight"] as? CGFloat, 240)
|
|
77
|
+
XCTAssertNil(payload["contentOffset"])
|
|
78
|
+
XCTAssertNil(payload["layoutMeasurement"])
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import VueNativeMacOS
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
final class VueNativeMacOSTests: XCTestCase {
|
|
6
|
+
|
|
7
|
+
// MARK: - FlippedView
|
|
8
|
+
|
|
9
|
+
func testFlippedViewIsFlipped() {
|
|
10
|
+
let view = FlippedView()
|
|
11
|
+
XCTAssertTrue(view.isFlipped, "FlippedView must have isFlipped = true")
|
|
12
|
+
XCTAssertTrue(view.wantsLayer, "FlippedView must be layer-backed")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// MARK: - LayoutNode
|
|
16
|
+
|
|
17
|
+
func testLayoutNodeDefaults() {
|
|
18
|
+
let node = LayoutNode()
|
|
19
|
+
XCTAssertEqual(node.flexDirection, .column)
|
|
20
|
+
XCTAssertEqual(node.justifyContent, .flexStart)
|
|
21
|
+
XCTAssertEqual(node.alignItems, .stretch)
|
|
22
|
+
XCTAssertEqual(node.flexGrow, 0)
|
|
23
|
+
XCTAssertEqual(node.flexShrink, 1)
|
|
24
|
+
XCTAssertEqual(node.positionType, .relative)
|
|
25
|
+
XCTAssertEqual(node.display, .flex)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testLayoutValueResolvePoints() {
|
|
29
|
+
let value = LayoutValue.points(50)
|
|
30
|
+
XCTAssertEqual(value.resolve(relativeTo: 200), 50)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testLayoutValueResolvePercent() {
|
|
34
|
+
let value = LayoutValue.percent(50)
|
|
35
|
+
XCTAssertEqual(value.resolve(relativeTo: 200), 100)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func testLayoutValueResolveAuto() {
|
|
39
|
+
let value = LayoutValue.auto
|
|
40
|
+
XCTAssertNil(value.resolve(relativeTo: 200))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func testLayoutValueResolveUndefined() {
|
|
44
|
+
let value = LayoutValue.undefined
|
|
45
|
+
XCTAssertNil(value.resolve(relativeTo: 200))
|
|
46
|
+
XCTAssertTrue(value.isUndefined)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func testEdgeInsetsZero() {
|
|
50
|
+
let insets = EdgeInsets.zero
|
|
51
|
+
XCTAssertEqual(insets.horizontal, 0)
|
|
52
|
+
XCTAssertEqual(insets.vertical, 0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// MARK: - NSView LayoutNode Extension
|
|
56
|
+
|
|
57
|
+
func testNSViewLayoutNodeAssociation() {
|
|
58
|
+
let view = FlippedView()
|
|
59
|
+
XCTAssertNil(view.layoutNode)
|
|
60
|
+
|
|
61
|
+
let node = view.ensureLayoutNode()
|
|
62
|
+
XCTAssertNotNil(view.layoutNode)
|
|
63
|
+
XCTAssertTrue(node === view.layoutNode)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - Percentage Operator
|
|
67
|
+
|
|
68
|
+
func testPercentageOperator() {
|
|
69
|
+
let value: LayoutValue = 50%
|
|
70
|
+
XCTAssertEqual(value, .percent(50))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// MARK: - Basic Layout
|
|
74
|
+
|
|
75
|
+
func testSimpleColumnLayout() {
|
|
76
|
+
let parent = FlippedView(frame: NSRect(x: 0, y: 0, width: 200, height: 400))
|
|
77
|
+
let parentNode = parent.ensureLayoutNode()
|
|
78
|
+
parentNode.flexDirection = .column
|
|
79
|
+
parentNode.width = .points(200)
|
|
80
|
+
parentNode.height = .points(400)
|
|
81
|
+
|
|
82
|
+
let child1 = FlippedView()
|
|
83
|
+
let child1Node = child1.ensureLayoutNode()
|
|
84
|
+
child1Node.height = .points(100)
|
|
85
|
+
child1Node.width = .points(200)
|
|
86
|
+
parent.addSubview(child1)
|
|
87
|
+
|
|
88
|
+
let child2 = FlippedView()
|
|
89
|
+
let child2Node = child2.ensureLayoutNode()
|
|
90
|
+
child2Node.height = .points(100)
|
|
91
|
+
child2Node.width = .points(200)
|
|
92
|
+
parent.addSubview(child2)
|
|
93
|
+
|
|
94
|
+
parentNode.layout(availableWidth: 200, availableHeight: 400)
|
|
95
|
+
|
|
96
|
+
XCTAssertEqual(child1.frame.origin.y, 0)
|
|
97
|
+
XCTAssertEqual(child2.frame.origin.y, 100)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func testFlexGrow() {
|
|
101
|
+
let parent = FlippedView(frame: NSRect(x: 0, y: 0, width: 200, height: 400))
|
|
102
|
+
let parentNode = parent.ensureLayoutNode()
|
|
103
|
+
parentNode.flexDirection = .column
|
|
104
|
+
parentNode.width = .points(200)
|
|
105
|
+
parentNode.height = .points(400)
|
|
106
|
+
|
|
107
|
+
let child1 = FlippedView()
|
|
108
|
+
let child1Node = child1.ensureLayoutNode()
|
|
109
|
+
child1Node.flexGrow = 1
|
|
110
|
+
parent.addSubview(child1)
|
|
111
|
+
|
|
112
|
+
let child2 = FlippedView()
|
|
113
|
+
let child2Node = child2.ensureLayoutNode()
|
|
114
|
+
child2Node.flexGrow = 1
|
|
115
|
+
parent.addSubview(child2)
|
|
116
|
+
|
|
117
|
+
parentNode.layout(availableWidth: 200, availableHeight: 400)
|
|
118
|
+
|
|
119
|
+
// Both children should share space equally
|
|
120
|
+
XCTAssertEqual(child1.frame.size.height, 200, accuracy: 1)
|
|
121
|
+
XCTAssertEqual(child2.frame.size.height, 200, accuracy: 1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: - NSColor+Hex
|
|
125
|
+
|
|
126
|
+
func testColorFromHex6() {
|
|
127
|
+
let color = NSColor.fromHex("#FF0000")
|
|
128
|
+
XCTAssertNotNil(color)
|
|
129
|
+
// Should be red
|
|
130
|
+
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
|
131
|
+
color.usingColorSpace(.sRGB)?.getRed(&r, green: &g, blue: &b, alpha: &a)
|
|
132
|
+
XCTAssertEqual(r, 1.0, accuracy: 0.01)
|
|
133
|
+
XCTAssertEqual(g, 0.0, accuracy: 0.01)
|
|
134
|
+
XCTAssertEqual(b, 0.0, accuracy: 0.01)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func testColorFromHexNamed() {
|
|
138
|
+
let transparent = NSColor.fromHex("transparent")
|
|
139
|
+
XCTAssertEqual(transparent, .clear)
|
|
140
|
+
|
|
141
|
+
let white = NSColor.fromHex("white")
|
|
142
|
+
XCTAssertEqual(white, .white)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func testColorFromHexInvalid() {
|
|
146
|
+
let color = NSColor.fromHex("notacolor")
|
|
147
|
+
XCTAssertEqual(color, .clear)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# AGENTS.md — VueNativeShared
|
|
2
|
+
|
|
3
|
+
Guidelines for AI agents working on the shared Swift package.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What this package is
|
|
8
|
+
|
|
9
|
+
VueNativeShared is a cross-platform Swift package containing code shared between iOS (`VueNativeCore`) and macOS (`VueNativeMacOS`). It provides the JS engine interface, infrastructure protocols, and platform-agnostic native modules.
|
|
10
|
+
|
|
11
|
+
**Platforms:** iOS 16+, macOS 13+
|
|
12
|
+
**Swift:** 5.9+, no SwiftUI
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Package structure
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Sources/VueNativeShared/
|
|
20
|
+
JSRuntime.swift JSContext wrapper on serial DispatchQueue
|
|
21
|
+
SharedJSPolyfills.swift JSON encoding helpers for polyfill strings
|
|
22
|
+
EventThrottle.swift Throttle events to ~60fps (DispatchSourceTimer)
|
|
23
|
+
HotReloadManager.swift WebSocket-based hot reload (URLSessionWebSocketTask)
|
|
24
|
+
CertificatePinning.swift TLS certificate pinning (URLSession + Security)
|
|
25
|
+
NativeModule.swift Protocol — all native modules conform to this
|
|
26
|
+
NativeModuleRegistry.swift Singleton registry mapping module names → instances
|
|
27
|
+
NativeEventDispatcher.swift Protocol for cross-platform event dispatch
|
|
28
|
+
Modules/
|
|
29
|
+
AsyncStorageModule.swift UserDefaults key-value storage
|
|
30
|
+
NetworkModule.swift NWPathMonitor network connectivity
|
|
31
|
+
FileSystemModule.swift FileManager file operations
|
|
32
|
+
SecureStorageModule.swift Keychain secure storage
|
|
33
|
+
AudioModule.swift AVAudioPlayer audio playback
|
|
34
|
+
GeolocationModule.swift CLLocationManager location services
|
|
35
|
+
DatabaseModule.swift SQLite database operations
|
|
36
|
+
WebSocketModule.swift URLSession WebSocket client
|
|
37
|
+
PerformanceModule.swift ProcessInfo performance metrics
|
|
38
|
+
Tests/VueNativeSharedTests/
|
|
39
|
+
VueNativeSharedTests.swift XCTest suite (6 tests)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Build & test
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
swift build # Must compile without errors
|
|
48
|
+
swift test # All tests must pass (currently 6)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or from the monorepo root:
|
|
52
|
+
```bash
|
|
53
|
+
bun run test:shared
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Architecture rules
|
|
59
|
+
|
|
60
|
+
### NativeModule protocol
|
|
61
|
+
- All native modules conform to `public protocol NativeModule: AnyObject`
|
|
62
|
+
- Required: `var moduleName: String` and `func invoke(method:args:callback:)`
|
|
63
|
+
- Optional: `func invokeSync(method:args:) -> Any?` (default returns nil)
|
|
64
|
+
- Modules are registered in `NativeModuleRegistry.shared`
|
|
65
|
+
- **Do NOT redefine this protocol in platform packages** — import VueNativeShared instead
|
|
66
|
+
|
|
67
|
+
### JSRuntime
|
|
68
|
+
- JSContext runs on a dedicated serial `DispatchQueue` (`jsQueue`)
|
|
69
|
+
- **Never pass JSValue across threads** — extract primitives first
|
|
70
|
+
- Always use `[weak self]` in closures registered with JSContext
|
|
71
|
+
- JSC has native Promise support (iOS 13+ / macOS 10.15+)
|
|
72
|
+
|
|
73
|
+
### EventThrottle
|
|
74
|
+
- Uses `CACurrentMediaTime()` for high-precision timing
|
|
75
|
+
- `DispatchSourceTimer` for deferred firing — NOT `Timer` or `RunLoop`
|
|
76
|
+
- Thread-safe: can be created and fired from any queue
|
|
77
|
+
|
|
78
|
+
### Thread safety
|
|
79
|
+
- `NativeModuleRegistry` is **not** thread-safe — access from main thread only
|
|
80
|
+
- `CertificatePinning` uses a serial queue internally
|
|
81
|
+
- `HotReloadManager` runs WebSocket on a background queue, dispatches UI updates to main
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Coding conventions
|
|
86
|
+
|
|
87
|
+
- All public API types must be marked `public`
|
|
88
|
+
- Use `guard let` / `if let` — no force-unwraps (`!`) in production code
|
|
89
|
+
- Modules should handle errors gracefully: call `callback(nil, errorMessage)` on failure
|
|
90
|
+
- Keep modules platform-agnostic — use only Foundation, Security, CoreLocation, and similar frameworks available on both iOS and macOS
|
|
91
|
+
- Do NOT import UIKit or AppKit in this package
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## How to add a shared module
|
|
96
|
+
|
|
97
|
+
1. Create `Sources/VueNativeShared/Modules/<Name>Module.swift`
|
|
98
|
+
2. Conform to `NativeModule` protocol
|
|
99
|
+
3. Implement `moduleName`, `invoke(method:args:callback:)`, and optionally `invokeSync`
|
|
100
|
+
4. Register in both platform packages' `NativeModuleRegistry.registerDefaults()`
|
|
101
|
+
5. Add tests in `Tests/VueNativeSharedTests/`
|
|
102
|
+
6. Only use frameworks available on both iOS 16+ and macOS 13+
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Common pitfalls
|
|
107
|
+
|
|
108
|
+
| Mistake | Why it breaks | Fix |
|
|
109
|
+
|---------|---------------|-----|
|
|
110
|
+
| Importing UIKit or AppKit | Package must be platform-agnostic | Use Foundation, CoreLocation, etc. |
|
|
111
|
+
| Force-unwrapping module args | Crashes if JS passes unexpected types | Use `as?` with guard/if-let |
|
|
112
|
+
| Accessing JSValue off jsQueue | JSContext is not thread-safe | Extract primitives on jsQueue first |
|
|
113
|
+
| Redefining NativeModule in platform package | Creates shadowing, type mismatches | Import VueNativeShared instead |
|
|
114
|
+
| Forgetting to register module | Module exists but is unreachable from JS | Add to registerDefaults() in both platforms |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Key file locations
|
|
119
|
+
|
|
120
|
+
| What | Path |
|
|
121
|
+
|------|------|
|
|
122
|
+
| NativeModule protocol | `Sources/VueNativeShared/NativeModule.swift` |
|
|
123
|
+
| Module registry | `Sources/VueNativeShared/NativeModuleRegistry.swift` |
|
|
124
|
+
| JS runtime | `Sources/VueNativeShared/JSRuntime.swift` |
|
|
125
|
+
| Event throttle | `Sources/VueNativeShared/EventThrottle.swift` |
|
|
126
|
+
| Hot reload | `Sources/VueNativeShared/HotReloadManager.swift` |
|
|
127
|
+
| Certificate pinning | `Sources/VueNativeShared/CertificatePinning.swift` |
|
|
128
|
+
| Tests | `Tests/VueNativeSharedTests/VueNativeSharedTests.swift` |
|
|
129
|
+
| Package manifest | `Package.swift` |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "VueNativeShared",
|
|
6
|
+
platforms: [.iOS(.v16), .macOS(.v13)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(name: "VueNativeShared", targets: ["VueNativeShared"])
|
|
9
|
+
],
|
|
10
|
+
targets: [
|
|
11
|
+
.target(name: "VueNativeShared", path: "Sources/VueNativeShared"),
|
|
12
|
+
.testTarget(name: "VueNativeSharedTests", dependencies: ["VueNativeShared"], path: "Tests/VueNativeSharedTests")
|
|
13
|
+
]
|
|
14
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CommonCrypto
|
|
3
|
+
|
|
4
|
+
/// Manages certificate pinning for network requests.
|
|
5
|
+
/// Stores SHA-256 pin hashes per domain and provides a URLSession with
|
|
6
|
+
/// a delegate that validates server certificates against the pinned hashes.
|
|
7
|
+
///
|
|
8
|
+
/// Usage from JS (via the bridge):
|
|
9
|
+
/// __VN_configurePins({ "api.example.com": ["sha256/BBBBBBB..."] })
|
|
10
|
+
///
|
|
11
|
+
/// The fetch polyfill uses `CertificatePinning.shared.session` instead of
|
|
12
|
+
/// `URLSession.shared` when pins are configured for the request's host.
|
|
13
|
+
public final class CertificatePinning: NSObject, URLSessionDelegate {
|
|
14
|
+
|
|
15
|
+
public static let shared = CertificatePinning()
|
|
16
|
+
|
|
17
|
+
/// Maps lowercase domain to an array of base64-encoded SHA-256 hashes of the
|
|
18
|
+
/// Subject Public Key Info (SPKI).
|
|
19
|
+
private var pins: [String: [String]] = [:]
|
|
20
|
+
|
|
21
|
+
/// A URLSession configured with this object as its delegate for TLS validation.
|
|
22
|
+
/// Lazily created so we don't pay the cost if pinning is never configured.
|
|
23
|
+
public private(set) lazy var session: URLSession = {
|
|
24
|
+
let config = URLSessionConfiguration.default
|
|
25
|
+
config.timeoutIntervalForRequest = 30
|
|
26
|
+
config.timeoutIntervalForResource = 60
|
|
27
|
+
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
|
28
|
+
}()
|
|
29
|
+
|
|
30
|
+
private override init() {
|
|
31
|
+
super.init()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: - Configuration
|
|
35
|
+
|
|
36
|
+
/// Configure certificate pins for one or more domains.
|
|
37
|
+
/// Each pin string must be in the format "sha256/<base64-encoded-hash>".
|
|
38
|
+
///
|
|
39
|
+
/// - Parameter domainPins: Dictionary mapping domain names to arrays of pin strings.
|
|
40
|
+
public func configurePins(_ domainPins: [String: [String]]) {
|
|
41
|
+
for (domain, pinList) in domainPins {
|
|
42
|
+
let hashes = pinList.compactMap { pin -> String? in
|
|
43
|
+
// Accept "sha256/XXXXX" format — strip the prefix
|
|
44
|
+
if pin.hasPrefix("sha256/") {
|
|
45
|
+
return String(pin.dropFirst(7))
|
|
46
|
+
}
|
|
47
|
+
return nil
|
|
48
|
+
}
|
|
49
|
+
if !hashes.isEmpty {
|
|
50
|
+
pins[domain.lowercased()] = hashes
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Returns true if there are pins configured for the given host.
|
|
56
|
+
public func hasPins(for host: String) -> Bool {
|
|
57
|
+
return pins[host.lowercased()] != nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Remove all configured pins.
|
|
61
|
+
public func clearPins() {
|
|
62
|
+
pins.removeAll()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - URLSessionDelegate
|
|
66
|
+
|
|
67
|
+
public func urlSession(
|
|
68
|
+
_ session: URLSession,
|
|
69
|
+
didReceive challenge: URLAuthenticationChallenge,
|
|
70
|
+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
|
71
|
+
) {
|
|
72
|
+
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
|
73
|
+
let serverTrust = challenge.protectionSpace.serverTrust else {
|
|
74
|
+
completionHandler(.performDefaultHandling, nil)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let host = challenge.protectionSpace.host.lowercased()
|
|
79
|
+
|
|
80
|
+
// If no pins configured for this host, use default handling
|
|
81
|
+
guard let expectedPins = pins[host] else {
|
|
82
|
+
completionHandler(.performDefaultHandling, nil)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Evaluate the server trust
|
|
87
|
+
var error: CFError?
|
|
88
|
+
guard SecTrustEvaluateWithError(serverTrust, &error) else {
|
|
89
|
+
NSLog("[VueNative CertPin] Trust evaluation failed for %@: %@", host, error?.localizedDescription ?? "unknown")
|
|
90
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check each certificate in the chain against our pins
|
|
95
|
+
let certCount = SecTrustGetCertificateCount(serverTrust)
|
|
96
|
+
guard let certChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] else {
|
|
97
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
for i in 0..<certCount {
|
|
101
|
+
guard i < certChain.count else { continue }
|
|
102
|
+
let certificate = certChain[i]
|
|
103
|
+
let spkiHash = sha256OfSPKI(for: certificate)
|
|
104
|
+
if expectedPins.contains(spkiHash) {
|
|
105
|
+
completionHandler(.useCredential, URLCredential(trust: serverTrust))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// No pin matched — reject the connection
|
|
111
|
+
NSLog("[VueNative CertPin] Pin validation failed for %@. No matching pin found.", host)
|
|
112
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - SPKI Hashing
|
|
116
|
+
|
|
117
|
+
/// Compute the base64-encoded SHA-256 hash of a certificate's Subject Public Key Info.
|
|
118
|
+
private func sha256OfSPKI(for certificate: SecCertificate) -> String {
|
|
119
|
+
guard let publicKey = SecCertificateCopyKey(certificate) else { return "" }
|
|
120
|
+
|
|
121
|
+
var error: Unmanaged<CFError>?
|
|
122
|
+
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
|
|
123
|
+
return ""
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Hash the raw public key data with SHA-256
|
|
127
|
+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
128
|
+
publicKeyData.withUnsafeBytes { bytes in
|
|
129
|
+
_ = CC_SHA256(bytes.baseAddress, CC_LONG(publicKeyData.count), &hash)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Data(hash).base64EncodedString()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import QuartzCore
|
|
3
|
+
|
|
4
|
+
/// Throttles high-frequency event handlers to avoid flooding the JS bridge.
|
|
5
|
+
///
|
|
6
|
+
/// When a high-frequency event (scroll, slider drag, text input) fires many
|
|
7
|
+
/// times per frame, each invocation becomes a bridge round-trip. This utility
|
|
8
|
+
/// ensures at most one call per `interval` seconds, with a trailing call
|
|
9
|
+
/// to deliver the latest value.
|
|
10
|
+
///
|
|
11
|
+
/// Default interval: 16ms (~60 FPS). Callers can customize via `interval`.
|
|
12
|
+
public final class EventThrottle {
|
|
13
|
+
|
|
14
|
+
/// Minimum time between handler invocations (seconds).
|
|
15
|
+
public let interval: TimeInterval
|
|
16
|
+
|
|
17
|
+
/// The throttled handler to call.
|
|
18
|
+
private let handler: (Any?) -> Void
|
|
19
|
+
|
|
20
|
+
/// Timestamp of the last invocation.
|
|
21
|
+
private var lastFireTime: CFTimeInterval = 0
|
|
22
|
+
|
|
23
|
+
/// Whether a trailing call is pending.
|
|
24
|
+
private var pendingTrailing = false
|
|
25
|
+
|
|
26
|
+
/// The most recent payload, used for the trailing call.
|
|
27
|
+
private var latestPayload: Any?
|
|
28
|
+
|
|
29
|
+
/// Timer for delivering the trailing call.
|
|
30
|
+
private var trailingTimer: DispatchSourceTimer?
|
|
31
|
+
|
|
32
|
+
/// Create a throttled event handler.
|
|
33
|
+
/// - Parameters:
|
|
34
|
+
/// - interval: Minimum seconds between invocations. Default 0.016 (~60fps).
|
|
35
|
+
/// - handler: The closure to invoke with the event payload.
|
|
36
|
+
public init(interval: TimeInterval = 0.016, handler: @escaping (Any?) -> Void) {
|
|
37
|
+
self.interval = interval
|
|
38
|
+
self.handler = handler
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deinit {
|
|
42
|
+
trailingTimer?.cancel()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Call this from the native event callback instead of the original handler.
|
|
46
|
+
/// Fires immediately if enough time has elapsed, otherwise schedules a trailing call.
|
|
47
|
+
public func fire(_ payload: Any?) {
|
|
48
|
+
let now = CACurrentMediaTime()
|
|
49
|
+
let elapsed = now - lastFireTime
|
|
50
|
+
|
|
51
|
+
latestPayload = payload
|
|
52
|
+
|
|
53
|
+
if elapsed >= interval {
|
|
54
|
+
// Enough time has passed — fire immediately
|
|
55
|
+
lastFireTime = now
|
|
56
|
+
pendingTrailing = false
|
|
57
|
+
trailingTimer?.cancel()
|
|
58
|
+
trailingTimer = nil
|
|
59
|
+
handler(payload)
|
|
60
|
+
} else if !pendingTrailing {
|
|
61
|
+
// Schedule a trailing call for the remaining time
|
|
62
|
+
pendingTrailing = true
|
|
63
|
+
let remaining = interval - elapsed
|
|
64
|
+
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
65
|
+
timer.schedule(deadline: .now() + remaining)
|
|
66
|
+
timer.setEventHandler { [weak self] in
|
|
67
|
+
guard let self = self else { return }
|
|
68
|
+
self.lastFireTime = CACurrentMediaTime()
|
|
69
|
+
self.pendingTrailing = false
|
|
70
|
+
self.trailingTimer = nil
|
|
71
|
+
self.handler(self.latestPayload)
|
|
72
|
+
}
|
|
73
|
+
trailingTimer = timer
|
|
74
|
+
timer.resume()
|
|
75
|
+
}
|
|
76
|
+
// If a trailing call is already pending, just update latestPayload (done above)
|
|
77
|
+
}
|
|
78
|
+
}
|