@astryxdesign/native 0.7.4-canary.720bb10 → 0.7.5-canary.04434e7

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.
Files changed (119) hide show
  1. package/README.md +116 -4
  2. package/apple/Sources/AstryxNative/Generated/AstryxBase.swift +1 -1
  3. package/apple/Sources/AstryxNative/Generated/AstryxNode.swift +69 -0
  4. package/apple/Sources/AstryxNative/Generated/AstryxWire.swift +67 -0
  5. package/apple/Sources/AstryxNative/Generated/Badge.swift +89 -0
  6. package/apple/Sources/AstryxNative/Generated/Button.swift +154 -0
  7. package/apple/Sources/AstryxNative/Generated/Card.swift +117 -0
  8. package/apple/Sources/AstryxNative/Generated/Divider.swift +97 -0
  9. package/apple/Sources/AstryxNative/Generated/HStack.swift +308 -0
  10. package/apple/Sources/AstryxNative/Generated/Heading.swift +169 -0
  11. package/apple/Sources/AstryxNative/Generated/ProgressBar.swift +144 -0
  12. package/apple/Sources/AstryxNative/Generated/Selector.swift +133 -9
  13. package/apple/Sources/AstryxNative/Generated/Switch.swift +202 -0
  14. package/apple/Sources/AstryxNative/Generated/Text.swift +172 -0
  15. package/apple/Sources/AstryxNative/Generated/TextInput.swift +243 -0
  16. package/apple/Sources/AstryxNative/Generated/VStack.swift +308 -0
  17. package/apple/Sources/AstryxNative/Logic/AstryxLogicHost.swift +102 -0
  18. package/apple/Tests/AstryxNativeTests/ControlPropsTests.swift +29 -0
  19. package/apple/Tests/AstryxNativeTests/LogicHostTests.swift +85 -0
  20. package/apple/Tests/AstryxNativeTests/WireTests.swift +53 -0
  21. package/dist/components/Badge.d.ts +10 -0
  22. package/dist/components/Badge.d.ts.map +1 -0
  23. package/dist/components/Badge.js +7 -0
  24. package/dist/components/Button.d.ts +10 -0
  25. package/dist/components/Button.d.ts.map +1 -0
  26. package/dist/components/Button.js +15 -0
  27. package/dist/components/Card.d.ts +10 -0
  28. package/dist/components/Card.d.ts.map +1 -0
  29. package/dist/components/Card.js +7 -0
  30. package/dist/components/Divider.d.ts +10 -0
  31. package/dist/components/Divider.d.ts.map +1 -0
  32. package/dist/components/Divider.js +6 -0
  33. package/dist/components/HStack.d.ts +10 -0
  34. package/dist/components/HStack.d.ts.map +1 -0
  35. package/dist/components/HStack.js +9 -0
  36. package/dist/components/Heading.d.ts +10 -0
  37. package/dist/components/Heading.d.ts.map +1 -0
  38. package/dist/components/Heading.js +14 -0
  39. package/dist/components/ProgressBar.d.ts +10 -0
  40. package/dist/components/ProgressBar.d.ts.map +1 -0
  41. package/dist/components/ProgressBar.js +5 -0
  42. package/dist/components/Switch.d.ts +10 -0
  43. package/dist/components/Switch.d.ts.map +1 -0
  44. package/dist/components/Switch.js +6 -0
  45. package/dist/components/Text.d.ts +10 -0
  46. package/dist/components/Text.d.ts.map +1 -0
  47. package/dist/components/Text.js +13 -0
  48. package/dist/components/TextInput.d.ts +10 -0
  49. package/dist/components/TextInput.d.ts.map +1 -0
  50. package/dist/components/TextInput.js +9 -0
  51. package/dist/components/VStack.d.ts +10 -0
  52. package/dist/components/VStack.d.ts.map +1 -0
  53. package/dist/components/VStack.js +9 -0
  54. package/dist/components/index.d.ts.map +1 -1
  55. package/dist/components/index.js +25 -1
  56. package/dist/components/stackShared.d.ts +9 -0
  57. package/dist/components/stackShared.d.ts.map +1 -0
  58. package/dist/components/stackShared.js +4 -0
  59. package/dist/contract/fromComponentDoc.d.ts +44 -5
  60. package/dist/contract/fromComponentDoc.d.ts.map +1 -1
  61. package/dist/contract/fromComponentDoc.js +47 -4
  62. package/dist/contract/generateSwift.d.ts.map +1 -1
  63. package/dist/contract/generateSwift.js +193 -10
  64. package/dist/contract/generateTypeScript.d.ts.map +1 -1
  65. package/dist/contract/generateTypeScript.js +69 -9
  66. package/dist/contract/index.d.ts +7 -3
  67. package/dist/contract/index.d.ts.map +1 -1
  68. package/dist/contract/index.js +2 -1
  69. package/dist/contract/parseDocType.d.ts +5 -2
  70. package/dist/contract/parseDocType.d.ts.map +1 -1
  71. package/dist/contract/parseDocType.js +34 -5
  72. package/dist/contract/rules.d.ts +5 -2
  73. package/dist/contract/rules.d.ts.map +1 -1
  74. package/dist/contract/rules.js +8 -4
  75. package/dist/contract/types.d.ts +11 -0
  76. package/dist/contract/types.d.ts.map +1 -1
  77. package/dist/logic/connectBridge.d.ts +29 -0
  78. package/dist/logic/connectBridge.d.ts.map +1 -0
  79. package/dist/logic/connectBridge.js +32 -0
  80. package/dist/logic/createLogicCore.d.ts +44 -0
  81. package/dist/logic/createLogicCore.d.ts.map +1 -0
  82. package/dist/logic/createLogicCore.js +40 -0
  83. package/dist/logic/index.d.ts +12 -0
  84. package/dist/logic/index.d.ts.map +1 -0
  85. package/dist/logic/index.js +10 -0
  86. package/dist/types/android.d.ts +136 -2
  87. package/dist/types/core.d.ts +150 -0
  88. package/dist/types/ios.d.ts +136 -2
  89. package/dist/types/macos.d.ts +136 -2
  90. package/dist/types/shared.d.ts +136 -2
  91. package/dist/types/windows.d.ts +136 -2
  92. package/package.json +10 -4
  93. package/src/components/Badge.ts +18 -0
  94. package/src/components/Button.ts +26 -0
  95. package/src/components/Card.ts +18 -0
  96. package/src/components/Divider.ts +17 -0
  97. package/src/components/HStack.ts +20 -0
  98. package/src/components/Heading.ts +25 -0
  99. package/src/components/ProgressBar.ts +16 -0
  100. package/src/components/Switch.ts +17 -0
  101. package/src/components/Text.ts +24 -0
  102. package/src/components/TextInput.ts +20 -0
  103. package/src/components/VStack.ts +20 -0
  104. package/src/components/index.ts +25 -1
  105. package/src/components/stackShared.ts +14 -0
  106. package/src/contract/contracts.test.ts +58 -0
  107. package/src/contract/fromComponentDoc.test.ts +97 -0
  108. package/src/contract/fromComponentDoc.ts +106 -9
  109. package/src/contract/generateSwift.ts +223 -10
  110. package/src/contract/generateTypeScript.ts +83 -10
  111. package/src/contract/index.ts +13 -4
  112. package/src/contract/parseDocType.test.ts +45 -0
  113. package/src/contract/parseDocType.ts +41 -6
  114. package/src/contract/rules.ts +11 -4
  115. package/src/contract/types.ts +6 -0
  116. package/src/logic/connectBridge.ts +72 -0
  117. package/src/logic/createLogicCore.ts +97 -0
  118. package/src/logic/index.ts +19 -0
  119. package/src/logic/logic.test.ts +160 -0
@@ -0,0 +1,308 @@
1
+ // Generated by @astryxdesign/native. Do not edit.
2
+ public enum VStackGap: Double, CaseIterable, Codable, Sendable {
3
+ case v0 = 0
4
+ case v0_5 = 0.5
5
+ case v1 = 1
6
+ case v1_5 = 1.5
7
+ case v2 = 2
8
+ case v3 = 3
9
+ case v4 = 4
10
+ case v5 = 5
11
+ case v6 = 6
12
+ case v8 = 8
13
+ case v10 = 10
14
+ }
15
+ public enum VStackPadding: Double, CaseIterable, Codable, Sendable {
16
+ case v0 = 0
17
+ case v0_5 = 0.5
18
+ case v1 = 1
19
+ case v1_5 = 1.5
20
+ case v2 = 2
21
+ case v3 = 3
22
+ case v4 = 4
23
+ case v5 = 5
24
+ case v6 = 6
25
+ case v8 = 8
26
+ case v10 = 10
27
+ }
28
+ public enum VStackPaddingInline: Double, CaseIterable, Codable, Sendable {
29
+ case v0 = 0
30
+ case v0_5 = 0.5
31
+ case v1 = 1
32
+ case v1_5 = 1.5
33
+ case v2 = 2
34
+ case v3 = 3
35
+ case v4 = 4
36
+ case v5 = 5
37
+ case v6 = 6
38
+ case v8 = 8
39
+ case v10 = 10
40
+ }
41
+ public enum VStackPaddingInlineStart: Double, CaseIterable, Codable, Sendable {
42
+ case v0 = 0
43
+ case v0_5 = 0.5
44
+ case v1 = 1
45
+ case v1_5 = 1.5
46
+ case v2 = 2
47
+ case v3 = 3
48
+ case v4 = 4
49
+ case v5 = 5
50
+ case v6 = 6
51
+ case v8 = 8
52
+ case v10 = 10
53
+ }
54
+ public enum VStackPaddingInlineEnd: Double, CaseIterable, Codable, Sendable {
55
+ case v0 = 0
56
+ case v0_5 = 0.5
57
+ case v1 = 1
58
+ case v1_5 = 1.5
59
+ case v2 = 2
60
+ case v3 = 3
61
+ case v4 = 4
62
+ case v5 = 5
63
+ case v6 = 6
64
+ case v8 = 8
65
+ case v10 = 10
66
+ }
67
+ public enum VStackPaddingBlock: Double, CaseIterable, Codable, Sendable {
68
+ case v0 = 0
69
+ case v0_5 = 0.5
70
+ case v1 = 1
71
+ case v1_5 = 1.5
72
+ case v2 = 2
73
+ case v3 = 3
74
+ case v4 = 4
75
+ case v5 = 5
76
+ case v6 = 6
77
+ case v8 = 8
78
+ case v10 = 10
79
+ }
80
+ public enum VStackPaddingBlockStart: Double, CaseIterable, Codable, Sendable {
81
+ case v0 = 0
82
+ case v0_5 = 0.5
83
+ case v1 = 1
84
+ case v1_5 = 1.5
85
+ case v2 = 2
86
+ case v3 = 3
87
+ case v4 = 4
88
+ case v5 = 5
89
+ case v6 = 6
90
+ case v8 = 8
91
+ case v10 = 10
92
+ }
93
+ public enum VStackPaddingBlockEnd: Double, CaseIterable, Codable, Sendable {
94
+ case v0 = 0
95
+ case v0_5 = 0.5
96
+ case v1 = 1
97
+ case v1_5 = 1.5
98
+ case v2 = 2
99
+ case v3 = 3
100
+ case v4 = 4
101
+ case v5 = 5
102
+ case v6 = 6
103
+ case v8 = 8
104
+ case v10 = 10
105
+ }
106
+ public enum VStackHAlign: String, CaseIterable, Codable, Sendable {
107
+ case start = "start"
108
+ case center = "center"
109
+ case end = "end"
110
+ case stretch = "stretch"
111
+ }
112
+ public enum VStackVAlign: String, CaseIterable, Codable, Sendable {
113
+ case start = "start"
114
+ case center = "center"
115
+ case end = "end"
116
+ case between = "between"
117
+ case around = "around"
118
+ case evenly = "evenly"
119
+ }
120
+ public enum VStackJustify: String, CaseIterable, Codable, Sendable {
121
+ case start = "start"
122
+ case center = "center"
123
+ case end = "end"
124
+ case between = "between"
125
+ case around = "around"
126
+ case evenly = "evenly"
127
+ }
128
+ public enum VStackAlign: String, CaseIterable, Codable, Sendable {
129
+ case start = "start"
130
+ case center = "center"
131
+ case end = "end"
132
+ case stretch = "stretch"
133
+ }
134
+ public enum VStackWrap: String, CaseIterable, Codable, Sendable {
135
+ case nowrap = "nowrap"
136
+ case wrap = "wrap"
137
+ case wrapReverse = "wrap-reverse"
138
+ }
139
+ #if os(iOS)
140
+ public struct VStackProps {
141
+ /// Base props attach to this part on every platform.
142
+ public static let baseTarget: AstryxBaseTarget = .container
143
+ public var base: AstryxBaseProps
144
+ public var gap: VStackGap?
145
+ public var padding: VStackPadding?
146
+ public var paddingInline: VStackPaddingInline?
147
+ public var paddingInlineStart: VStackPaddingInlineStart?
148
+ public var paddingInlineEnd: VStackPaddingInlineEnd?
149
+ public var paddingBlock: VStackPaddingBlock?
150
+ public var paddingBlockStart: VStackPaddingBlockStart?
151
+ public var paddingBlockEnd: VStackPaddingBlockEnd?
152
+ public var isScrollable: Bool?
153
+ public var hAlign: VStackHAlign?
154
+ public var vAlign: VStackVAlign?
155
+ public var justify: VStackJustify?
156
+ public var align: VStackAlign?
157
+ public var wrap: VStackWrap?
158
+ public init(gap: VStackGap? = nil, padding: VStackPadding? = nil, paddingInline: VStackPaddingInline? = nil, paddingInlineStart: VStackPaddingInlineStart? = nil, paddingInlineEnd: VStackPaddingInlineEnd? = nil, paddingBlock: VStackPaddingBlock? = nil, paddingBlockStart: VStackPaddingBlockStart? = nil, paddingBlockEnd: VStackPaddingBlockEnd? = nil, isScrollable: Bool? = nil, hAlign: VStackHAlign? = nil, vAlign: VStackVAlign? = nil, justify: VStackJustify? = nil, align: VStackAlign? = nil, wrap: VStackWrap? = nil, base: AstryxBaseProps = AstryxBaseProps()) {
159
+ self.gap = gap
160
+ self.padding = padding
161
+ self.paddingInline = paddingInline
162
+ self.paddingInlineStart = paddingInlineStart
163
+ self.paddingInlineEnd = paddingInlineEnd
164
+ self.paddingBlock = paddingBlock
165
+ self.paddingBlockStart = paddingBlockStart
166
+ self.paddingBlockEnd = paddingBlockEnd
167
+ self.isScrollable = isScrollable
168
+ self.hAlign = hAlign
169
+ self.vAlign = vAlign
170
+ self.justify = justify
171
+ self.align = align
172
+ self.wrap = wrap
173
+ self.base = base
174
+ }
175
+ }
176
+ public struct VStackNode: Decodable, Equatable, Sendable {
177
+ public struct Props: Decodable, Equatable, Sendable {
178
+ public var gap: VStackGap?
179
+ public var padding: VStackPadding?
180
+ public var paddingInline: VStackPaddingInline?
181
+ public var paddingInlineStart: VStackPaddingInlineStart?
182
+ public var paddingInlineEnd: VStackPaddingInlineEnd?
183
+ public var paddingBlock: VStackPaddingBlock?
184
+ public var paddingBlockStart: VStackPaddingBlockStart?
185
+ public var paddingBlockEnd: VStackPaddingBlockEnd?
186
+ public var isScrollable: Bool?
187
+ public var hAlign: VStackHAlign?
188
+ public var vAlign: VStackVAlign?
189
+ public var justify: VStackJustify?
190
+ public var align: VStackAlign?
191
+ public var wrap: VStackWrap?
192
+ public var children: [AstryxNode]?
193
+ }
194
+ public let key: String
195
+ public let base: AstryxBaseProps?
196
+ public let props: Props
197
+ /// The callbacks the core listens to, by prop name.
198
+ public let events: [String]?
199
+
200
+ public func listens(_ event: String) -> Bool { events?.contains(event) ?? false }
201
+
202
+ /// The contract a VStack view takes; its callbacks send actions to the core.
203
+ public func viewProps(send: @escaping @Sendable (AstryxAction) -> Void) -> VStackProps {
204
+ return VStackProps(
205
+ gap: props.gap,
206
+ padding: props.padding,
207
+ paddingInline: props.paddingInline,
208
+ paddingInlineStart: props.paddingInlineStart,
209
+ paddingInlineEnd: props.paddingInlineEnd,
210
+ paddingBlock: props.paddingBlock,
211
+ paddingBlockStart: props.paddingBlockStart,
212
+ paddingBlockEnd: props.paddingBlockEnd,
213
+ isScrollable: props.isScrollable,
214
+ hAlign: props.hAlign,
215
+ vAlign: props.vAlign,
216
+ justify: props.justify,
217
+ align: props.align,
218
+ wrap: props.wrap,
219
+ base: base ?? AstryxBaseProps()
220
+ )
221
+ }
222
+ }
223
+ #endif
224
+ #if os(macOS)
225
+ public struct VStackProps {
226
+ /// Base props attach to this part on every platform.
227
+ public static let baseTarget: AstryxBaseTarget = .container
228
+ public var base: AstryxBaseProps
229
+ public var gap: VStackGap?
230
+ public var padding: VStackPadding?
231
+ public var paddingInline: VStackPaddingInline?
232
+ public var paddingInlineStart: VStackPaddingInlineStart?
233
+ public var paddingInlineEnd: VStackPaddingInlineEnd?
234
+ public var paddingBlock: VStackPaddingBlock?
235
+ public var paddingBlockStart: VStackPaddingBlockStart?
236
+ public var paddingBlockEnd: VStackPaddingBlockEnd?
237
+ public var isScrollable: Bool?
238
+ public var hAlign: VStackHAlign?
239
+ public var vAlign: VStackVAlign?
240
+ public var justify: VStackJustify?
241
+ public var align: VStackAlign?
242
+ public var wrap: VStackWrap?
243
+ public init(gap: VStackGap? = nil, padding: VStackPadding? = nil, paddingInline: VStackPaddingInline? = nil, paddingInlineStart: VStackPaddingInlineStart? = nil, paddingInlineEnd: VStackPaddingInlineEnd? = nil, paddingBlock: VStackPaddingBlock? = nil, paddingBlockStart: VStackPaddingBlockStart? = nil, paddingBlockEnd: VStackPaddingBlockEnd? = nil, isScrollable: Bool? = nil, hAlign: VStackHAlign? = nil, vAlign: VStackVAlign? = nil, justify: VStackJustify? = nil, align: VStackAlign? = nil, wrap: VStackWrap? = nil, base: AstryxBaseProps = AstryxBaseProps()) {
244
+ self.gap = gap
245
+ self.padding = padding
246
+ self.paddingInline = paddingInline
247
+ self.paddingInlineStart = paddingInlineStart
248
+ self.paddingInlineEnd = paddingInlineEnd
249
+ self.paddingBlock = paddingBlock
250
+ self.paddingBlockStart = paddingBlockStart
251
+ self.paddingBlockEnd = paddingBlockEnd
252
+ self.isScrollable = isScrollable
253
+ self.hAlign = hAlign
254
+ self.vAlign = vAlign
255
+ self.justify = justify
256
+ self.align = align
257
+ self.wrap = wrap
258
+ self.base = base
259
+ }
260
+ }
261
+ public struct VStackNode: Decodable, Equatable, Sendable {
262
+ public struct Props: Decodable, Equatable, Sendable {
263
+ public var gap: VStackGap?
264
+ public var padding: VStackPadding?
265
+ public var paddingInline: VStackPaddingInline?
266
+ public var paddingInlineStart: VStackPaddingInlineStart?
267
+ public var paddingInlineEnd: VStackPaddingInlineEnd?
268
+ public var paddingBlock: VStackPaddingBlock?
269
+ public var paddingBlockStart: VStackPaddingBlockStart?
270
+ public var paddingBlockEnd: VStackPaddingBlockEnd?
271
+ public var isScrollable: Bool?
272
+ public var hAlign: VStackHAlign?
273
+ public var vAlign: VStackVAlign?
274
+ public var justify: VStackJustify?
275
+ public var align: VStackAlign?
276
+ public var wrap: VStackWrap?
277
+ public var children: [AstryxNode]?
278
+ }
279
+ public let key: String
280
+ public let base: AstryxBaseProps?
281
+ public let props: Props
282
+ /// The callbacks the core listens to, by prop name.
283
+ public let events: [String]?
284
+
285
+ public func listens(_ event: String) -> Bool { events?.contains(event) ?? false }
286
+
287
+ /// The contract a VStack view takes; its callbacks send actions to the core.
288
+ public func viewProps(send: @escaping @Sendable (AstryxAction) -> Void) -> VStackProps {
289
+ return VStackProps(
290
+ gap: props.gap,
291
+ padding: props.padding,
292
+ paddingInline: props.paddingInline,
293
+ paddingInlineStart: props.paddingInlineStart,
294
+ paddingInlineEnd: props.paddingInlineEnd,
295
+ paddingBlock: props.paddingBlock,
296
+ paddingBlockStart: props.paddingBlockStart,
297
+ paddingBlockEnd: props.paddingBlockEnd,
298
+ isScrollable: props.isScrollable,
299
+ hAlign: props.hAlign,
300
+ vAlign: props.vAlign,
301
+ justify: props.justify,
302
+ align: props.align,
303
+ wrap: props.wrap,
304
+ base: base ?? AstryxBaseProps()
305
+ )
306
+ }
307
+ }
308
+ #endif
@@ -0,0 +1,102 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ //
3
+ // The host side of arm C (AST-082 FR12): a TypeScript logic core runs in a
4
+ // JavaScript engine inside the app and native views draw its tree. Its shape
5
+ // follows the ClientHost of EJ Campbell's astryx-swift spike
6
+ // (github.com/ejc3/astryx-swift, MIT), which runs the core in Hermes. This host
7
+ // uses JavaScriptCore, part of the platform SDK, so the package needs nothing
8
+ // else; a Hermes host speaks the same bridge.
9
+
10
+ #if os(iOS) || os(macOS)
11
+ import Foundation
12
+ import JavaScriptCore
13
+ import Observation
14
+
15
+ /// Runs a logic core bundle and carries its tree to native views and their
16
+ /// actions back, as the JSON `@astryxdesign/native/logic`'s `connectBridge`
17
+ /// speaks.
18
+ ///
19
+ /// The bundle defines a global function, `astryxStart` by default, that takes
20
+ /// the host (`connected(version)` and `changed(json)`) and returns the bridge
21
+ /// (`getSnapshot()` and `dispatch(json)`).
22
+ @MainActor
23
+ @Observable
24
+ public final class AstryxLogicHost {
25
+ /// The bridge version this host speaks (`BRIDGE_VERSION` in TypeScript).
26
+ public static let bridgeVersion = 1
27
+
28
+ /// The current tree; nil before the first snapshot and once the host fails.
29
+ public private(set) var root: AstryxNode?
30
+ /// Why the core stopped or a snapshot could not be read.
31
+ public private(set) var failure: String?
32
+
33
+ @ObservationIgnored private let context: JSContext
34
+ @ObservationIgnored private var bridge: JSValue?
35
+
36
+ public init(bundle source: String, entry: String = "astryxStart") {
37
+ context = JSContext()
38
+ context.exceptionHandler = { [weak self] _, exception in
39
+ let message = exception?.toString() ?? "An unknown JavaScript error"
40
+ MainActor.assumeIsolated { self?.fail(message) }
41
+ }
42
+ context.evaluateScript(source)
43
+ guard failure == nil else { return }
44
+ guard let start = context.objectForKeyedSubscript(entry), start.isObject else {
45
+ fail("The bundle defines no \(entry) function")
46
+ return
47
+ }
48
+ let host = JSValue(newObjectIn: context)
49
+ let connected: @convention(block) (Int) -> Void = { [weak self] version in
50
+ MainActor.assumeIsolated {
51
+ if version != Self.bridgeVersion {
52
+ self?.fail("The core speaks bridge \(version); this host speaks \(Self.bridgeVersion)")
53
+ }
54
+ }
55
+ }
56
+ let changed: @convention(block) (String) -> Void = { [weak self] json in
57
+ MainActor.assumeIsolated { self?.receive(json) }
58
+ }
59
+ host?.setObject(connected, forKeyedSubscript: "connected" as NSString)
60
+ host?.setObject(changed, forKeyedSubscript: "changed" as NSString)
61
+ bridge = start.call(withArguments: [host as Any])
62
+ guard failure == nil, let snapshot = bridge?.invokeMethod("getSnapshot", withArguments: []),
63
+ snapshot.isString, let json = snapshot.toString() else {
64
+ if failure == nil { fail("The bridge returned no snapshot") }
65
+ return
66
+ }
67
+ receive(json)
68
+ }
69
+
70
+ /// Sends a view's action to the core: `node.viewProps(send: host.send)`.
71
+ /// Views call it on the main thread, where it dispatches at once; from any
72
+ /// other thread it dispatches on the main actor.
73
+ public nonisolated func send(_ action: AstryxAction) {
74
+ if Thread.isMainThread {
75
+ MainActor.assumeIsolated { dispatch(action) }
76
+ } else {
77
+ Task { @MainActor in self.dispatch(action) }
78
+ }
79
+ }
80
+
81
+ private func dispatch(_ action: AstryxAction) {
82
+ guard failure == nil, let bridge else { return }
83
+ bridge.invokeMethod("dispatch", withArguments: [action.json])
84
+ }
85
+
86
+ private func receive(_ json: String) {
87
+ // A failed host stays failed: a later tree does not bring it back.
88
+ guard failure == nil else { return }
89
+ do {
90
+ root = try JSONDecoder().decode(AstryxNode.self, from: Data(json.utf8))
91
+ } catch {
92
+ fail("The core sent a tree this binding cannot read: \(error)")
93
+ }
94
+ }
95
+
96
+ /// Stops the host: no tree to draw and no actions sent.
97
+ private func fail(_ message: String) {
98
+ failure = message
99
+ root = nil
100
+ }
101
+ }
102
+ #endif
@@ -0,0 +1,29 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import Testing
4
+ @testable import AstryxNative
5
+
6
+ @Test func valueCallbacksCarryTheValueOnly() {
7
+ var received: Bool?
8
+ let props = SwitchProps(label: "Wi-Fi", value: false, onChange: { received = $0 })
9
+ props.onChange?(true)
10
+ #expect(received == true)
11
+ #expect(SwitchProps.baseTarget == .root)
12
+ }
13
+
14
+ @Test func textSlotsAndNumberSetsAreTyped() {
15
+ let heading = HeadingProps(level: .v2, children: "Orders")
16
+ #expect(heading.level.rawValue == 2)
17
+ #expect(heading.children == "Orders")
18
+ let badge = BadgeProps(variant: .success, label: "Paid")
19
+ #expect(badge.label == "Paid")
20
+ let divider = DividerProps(label: "Or")
21
+ #expect(divider.label == "Or")
22
+ }
23
+
24
+ @Test func aButtonPressTakesNoArguments() {
25
+ var pressed = 0
26
+ let button = ButtonProps(label: "Pay", onClick: { pressed += 1 })
27
+ button.onClick?()
28
+ #expect(pressed == 1)
29
+ }
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import Testing
4
+ @testable import AstryxNative
5
+
6
+ /// A logic core with the bridge `@astryxdesign/native/logic` exposes, written
7
+ /// out by hand so the Swift tests need no build step.
8
+ private let settingsBundle = """
9
+ function astryxStart(host) {
10
+ var state = {wifi: true};
11
+ function view() {
12
+ return {component: 'VStack', key: 'root', props: {children: [
13
+ {component: 'Switch', key: 'wifi', props: {label: 'Wi-Fi', value: state.wifi}, events: ['onChange']},
14
+ {component: 'Text', key: 'status', props: {children: state.wifi ? 'Connected' : 'Off'}}
15
+ ]}};
16
+ }
17
+ host.connected(1);
18
+ return {
19
+ getSnapshot: function () { return JSON.stringify(view()); },
20
+ dispatch: function (json) {
21
+ var action = JSON.parse(json);
22
+ if (action.key === 'wifi' && action.event === 'onChange') {
23
+ state = {wifi: action.args[0]};
24
+ host.changed(JSON.stringify(view()));
25
+ }
26
+ }
27
+ };
28
+ }
29
+ """
30
+
31
+ @MainActor
32
+ @Test func aViewsActionReachesTheCoreAndTheNewTreeComesBack() throws {
33
+ let host = AstryxLogicHost(bundle: settingsBundle)
34
+ #expect(host.failure == nil)
35
+ guard case .vStack(let stack) = host.root, case .switch(let wifi) = stack.props.children?[0] else {
36
+ Issue.record("unexpected first tree")
37
+ return
38
+ }
39
+ #expect(wifi.props.value == true)
40
+
41
+ // The view calls its onChange; the action goes to the core.
42
+ let props = wifi.viewProps(send: host.send)
43
+ props.onChange?(false)
44
+
45
+ guard case .vStack(let next) = host.root, case .text(let status) = next.props.children?[1] else {
46
+ Issue.record("unexpected next tree")
47
+ return
48
+ }
49
+ #expect(status.props.children == "Off")
50
+ }
51
+
52
+ @MainActor
53
+ @Test func aLaterErrorClearsTheTreeAndStopsActions() throws {
54
+ let bundle = settingsBundle + "\nvar fragile = astryxStart; astryxStart = function (h) { var b = fragile(h); var d = b.dispatch; b.dispatch = function (j) { if (JSON.parse(j).args[0] === false) throw new Error('core broke'); d(j); }; return b; };"
55
+ let host = AstryxLogicHost(bundle: bundle)
56
+ guard case .vStack(let stack) = host.root, case .switch(let wifi) = stack.props.children?[0] else {
57
+ Issue.record("unexpected first tree")
58
+ return
59
+ }
60
+ wifi.viewProps(send: host.send).onChange?(false)
61
+ #expect(host.failure?.contains("core broke") == true)
62
+ #expect(host.root == nil)
63
+ }
64
+
65
+ @MainActor
66
+ @Test func aTreeAfterAFailureIsIgnored() {
67
+ let bundle = """
68
+ function astryxStart(h) {
69
+ h.connected(2);
70
+ h.changed(JSON.stringify({component: 'Text', key: 'late', props: {children: 'Too late'}}));
71
+ return {getSnapshot: function () { return '{}'; }, dispatch: function () {}};
72
+ }
73
+ """
74
+ let host = AstryxLogicHost(bundle: bundle)
75
+ #expect(host.failure?.contains("bridge 2") == true)
76
+ #expect(host.root == nil)
77
+ }
78
+
79
+ @MainActor
80
+ @Test func aBundleWithoutTheEntryOrWithAnErrorFails() {
81
+ #expect(AstryxLogicHost(bundle: "var x = 1;").failure == "The bundle defines no astryxStart function")
82
+ #expect(AstryxLogicHost(bundle: "throw new Error('boom');").failure?.contains("boom") == true)
83
+ let wrongVersion = AstryxLogicHost(bundle: "function astryxStart(h) { h.connected(2); return {getSnapshot: function () { return '{}'; }}; }")
84
+ #expect(wrongVersion.failure?.contains("bridge 2") == true)
85
+ }
@@ -0,0 +1,53 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import Foundation
4
+ import Testing
5
+ @testable import AstryxNative
6
+
7
+ private let tree = """
8
+ {"component": "VStack", "key": "root", "props": {"gap": 2, "children": [
9
+ {"component": "Heading", "key": "title", "props": {"level": 2, "children": "Settings"}},
10
+ {"component": "Switch", "key": "wifi", "props": {"label": "Wi-Fi", "value": true}, "events": ["onChange"]},
11
+ {"component": "Button", "key": "save", "base": {"id": "save"}, "props": {"label": "Save", "variant": "primary"}},
12
+ {"component": "Chart", "key": "later", "props": {}},
13
+ {"component": "Button", "key": "newer", "props": {"label": "Go", "variant": "glass"}}
14
+ ]}}
15
+ """
16
+
17
+ private final class Sent: @unchecked Sendable {
18
+ var actions: [AstryxAction] = []
19
+ }
20
+
21
+ @Test func decodesATreeByComponentName() throws {
22
+ let node = try JSONDecoder().decode(AstryxNode.self, from: Data(tree.utf8))
23
+ guard case .vStack(let stack) = node else { Issue.record("not a VStack"); return }
24
+ #expect(stack.props.gap == .v2)
25
+ let children = stack.props.children ?? []
26
+ #expect(children.map(\.key) == ["title", "wifi", "save", "later", "newer"])
27
+ guard case .heading(let heading) = children[0] else { Issue.record("not a Heading"); return }
28
+ #expect(heading.props.level == .v2)
29
+ #expect(heading.props.children == "Settings")
30
+ #expect(children[3] == .unsupported(component: "Chart", key: "later"))
31
+ // A value this binding's enum lacks makes only that node unsupported.
32
+ #expect(children[4] == .unsupported(component: "Button", key: "newer"))
33
+ }
34
+
35
+ @Test func callbacksTheCoreListensToSendActions() throws {
36
+ let node = try JSONDecoder().decode(AstryxNode.self, from: Data(tree.utf8))
37
+ guard case .vStack(let stack) = node,
38
+ case .switch(let wifi) = stack.props.children?[1],
39
+ case .button(let save) = stack.props.children?[2] else {
40
+ Issue.record("unexpected tree")
41
+ return
42
+ }
43
+ let sent = Sent()
44
+ let switchProps = wifi.viewProps { sent.actions.append($0) }
45
+ switchProps.onChange?(false)
46
+ #expect(sent.actions == [AstryxAction(key: "wifi", component: "Switch", event: "onChange", args: [.bool(false)])])
47
+ #expect(sent.actions[0].json == #"{"args":[false],"component":"Switch","event":"onChange","key":"wifi"}"#)
48
+
49
+ // The core did not listen to the button's press, so the view gets no callback.
50
+ let buttonProps = save.viewProps { sent.actions.append($0) }
51
+ #expect(buttonProps.onClick == nil)
52
+ #expect(buttonProps.base.id == "save")
53
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file components/Badge.ts
3
+ * @input None
4
+ * @output What @astryxdesign/native adds to the core Badge contract
5
+ * @position Native additions read by fromComponentDoc together with core's
6
+ * Badge documentation
7
+ */
8
+ import type { NativeAdditions } from '../contract/fromComponentDoc.js';
9
+ export declare const badge: NativeAdditions;
10
+ //# sourceMappingURL=Badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/components/Badge.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iCAAiC,CAAC;AAErE,eAAO,MAAM,KAAK,EAAE,eAKnB,CAAC"}
@@ -0,0 +1,7 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ export const badge = {
3
+ name: 'Badge',
4
+ baseTarget: 'root',
5
+ // `icon` stays web-only until the core has an icon slot.
6
+ slots: { label: 'text' },
7
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file components/Button.ts
3
+ * @input None
4
+ * @output What @astryxdesign/native adds to the core Button contract
5
+ * @position Native additions read by fromComponentDoc together with core's
6
+ * Button documentation
7
+ */
8
+ import type { NativeAdditions } from '../contract/fromComponentDoc.js';
9
+ export declare const button: NativeAdditions;
10
+ //# sourceMappingURL=Button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/components/Button.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iCAAiC,CAAC;AAErE,eAAO,MAAM,MAAM,EAAE,eAapB,CAAC"}
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ export const button = {
3
+ name: 'Button',
4
+ baseTarget: 'root',
5
+ callbacks: ['onClick'],
6
+ sameMeaning: { onClick: 'a press, as in Compose' },
7
+ webOnly: {
8
+ type: 'HTML form submission type',
9
+ name: 'HTML form field name',
10
+ form: 'HTML form owner',
11
+ target: 'HTML link target',
12
+ rel: 'HTML link relationship',
13
+ isIconOnly: 'needs an icon, which has no core slot yet',
14
+ },
15
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file components/Card.ts
3
+ * @input None
4
+ * @output What @astryxdesign/native adds to the core Card contract
5
+ * @position Native additions read by fromComponentDoc together with core's
6
+ * Card documentation
7
+ */
8
+ import type { NativeAdditions } from '../contract/fromComponentDoc.js';
9
+ export declare const card: NativeAdditions;
10
+ //# sourceMappingURL=Card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../src/components/Card.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iCAAiC,CAAC;AAErE,eAAO,MAAM,IAAI,EAAE,eAKlB,CAAC"}
@@ -0,0 +1,7 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ export const card = {
3
+ name: 'Card',
4
+ baseTarget: 'container',
5
+ slots: { children: 'content' },
6
+ sameMeaning: { children: "the component's content, as on the web" },
7
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file components/Divider.ts
3
+ * @input None
4
+ * @output What @astryxdesign/native adds to the core Divider contract
5
+ * @position Native additions read by fromComponentDoc together with core's
6
+ * Divider documentation
7
+ */
8
+ import type { NativeAdditions } from '../contract/fromComponentDoc.js';
9
+ export declare const divider: NativeAdditions;
10
+ //# sourceMappingURL=Divider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Divider.d.ts","sourceRoot":"","sources":["../../src/components/Divider.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iCAAiC,CAAC;AAErE,eAAO,MAAM,OAAO,EAAE,eAIrB,CAAC"}
@@ -0,0 +1,6 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ export const divider = {
3
+ name: 'Divider',
4
+ baseTarget: 'root',
5
+ slots: { label: 'text' },
6
+ };