expo-modules-macros 0.23.0 → 0.24.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.
Binary file
@@ -141,7 +141,17 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
141
141
  let parameters = closure.parameters.indices.map { "p\($0)" }
142
142
  let header = parameters.isEmpty ? "{ @Sendable in" : "{ @Sendable \(parameters.joined(separator: ", ")) in"
143
143
 
144
+ // A sync closure with parameters passes them as one value, a tuple when there are several, and an
145
+ // `emplacing` closure moves each one into the arguments buffer at its index. Capturing the
146
+ // parameters instead would send non-`Sendable` values to the `@JavaScriptActor` closure, which
147
+ // Swift 6 rejects. The closure captures nothing, so it needs no allocation. Without parameters
148
+ // there is nothing to emplace, and `invokeAsync` takes the parameters variadically.
149
+ let emplaces = !closure.isAsync && !parameters.isEmpty
144
150
  var arguments = parameters
151
+ if emplaces {
152
+ let values = parameters.count == 1 ? "p0" : "(" + parameters.joined(separator: ", ") + ")"
153
+ arguments = [values, "argumentCount: \(parameters.count)"]
154
+ }
145
155
  if let returnType = closure.returnType {
146
156
  arguments.append("returning: \(expressionType(returnType)).self")
147
157
  }
@@ -156,6 +166,14 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
156
166
  call = "\(callback).invokeDetached(\(argumentList))"
157
167
  }
158
168
 
169
+ if emplaces {
170
+ var lines = [header, " \(call) { _values, _arguments, _runtime in"]
171
+ for index in parameters.indices {
172
+ let value = parameters.count == 1 ? "_values" : "_values.\(index)"
173
+ lines.append(" try _arguments.emplace(\(value), at: \(index), in: _runtime)")
174
+ }
175
+ return lines + [" }", "}"]
176
+ }
159
177
  guard closure.isAsync && !closure.isThrowing else {
160
178
  return [header, " " + call, "}"]
161
179
  }
@@ -80,18 +80,19 @@ extension EventMacro: PeerMacro {
80
80
  return []
81
81
  }
82
82
  // One nested helper asserts everything in a single call: the payload type is JS-convertible
83
- // (the `P` parameter, dropped for no-payload events and known-conforming primitives) and the
84
- // enclosing type can emit (the `E` parameter, always present since any `@Event` dispatches
83
+ // (the first parameter, dropped for no-payload events and known-conforming primitives) and the
84
+ // enclosing type can emit (the last parameter, always present since any `@Event` dispatches
85
85
  // through `self.emit`). Named after the member so the member shows up in either diagnostic,
86
86
  // and asserting the enclosing type by its spelled name so the conformance error names the
87
87
  // user's type rather than 'Self'.
88
88
  let name = event.swiftName
89
89
  let payload = event.payloadType.flatMap { assertableBoundaryType($0, constraint: jsConvertibleProtocolName) }
90
90
  let owner = enclosingTypeName(in: context) ?? "Self"
91
+ let emitterParameter = opaqueMetatypeParameter(constraint: eventEmitterProtocolName)
91
92
  let helper =
92
93
  payload != nil
93
- ? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
94
- : "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
94
+ ? "func \(name)(\(opaqueMetatypeParameter(constraint: jsConvertibleProtocolName)), \(emitterParameter)) {}"
95
+ : "func \(name)(\(emitterParameter)) {}"
95
96
  let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
96
97
  let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
97
98
  return [
@@ -43,14 +43,17 @@ import SwiftSyntaxMacros
43
43
  /// (`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
44
44
  /// values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
45
45
  /// user modules without any internal core symbols. Every property type must therefore conform to both
46
- /// `AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`.
46
+ /// `AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`. The conformance assertion checks only
47
+ /// the JS codable protocols; the compiler reports a missing `AnyArgument` at the `getDynamicType()`
48
+ /// call.
47
49
  ///
48
50
  /// A property whose type mentions `Any` (`Any?`, `[String: Any]?`, `[String: [Any]]`, …) can't conform to
49
- /// the JS codable protocols. On the JS-value paths it converts through the `JavaScriptValue` free-form
50
- /// methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and the matching `encodeAny…`
51
- /// for `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic `decodeAny(_:as:in:)` /
52
- /// `encodeAny(_:in:)` for any other type. Such a property only needs `AnyArgument` for the dictionary
53
- /// paths, and a bare `Any` needs nothing, since the dictionary paths pass it through unchanged.
51
+ /// the JS codable protocols, so it isn't asserted. On the JS-value paths it converts through the
52
+ /// `JavaScriptValue` free-form methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and
53
+ /// the matching `encodeAny…` for `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic
54
+ /// `decodeAny(_:as:in:)` / `encodeAny(_:in:)` for any other type. Such a property only needs
55
+ /// `AnyArgument` for the dictionary paths, and a bare `Any` needs nothing, since the dictionary paths
56
+ /// pass it through unchanged.
54
57
  ///
55
58
  /// A record class must be `final` and have no inheritance clause. The synthesized factories construct
56
59
  /// `Self` through the memberwise init, which only a final class can do without a `required` init, and
@@ -87,23 +90,16 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
87
90
 
88
91
  var members: [DeclSyntax] = []
89
92
 
90
- // A single never-called member that makes the compiler verify each property type is convertible
91
- // both ways (the JS-value paths call `decode`/`encode`; the native-`Any` dictionary paths call
92
- // the dynamic-type API). Each property keeps its own named assertion inside, so the compiler's
93
- // conformance diagnostic names the offending property (see `typeConformanceAssertions`). Emitted
94
- // first so that, for a non-conforming type, this clear "requires that '…' conform to '…'" error
95
- // is reported ahead of the noisier "no member 'decode'"/"getDynamicType" errors from the
96
- // conversion code below.
93
+ // A single never-called member that makes the compiler verify each property type converts both
94
+ // ways (the JS-value paths call `decode`/`encode`). Each property keeps its own named assertion
95
+ // inside, so the compiler's conformance diagnostic names the offending property (see
96
+ // `typeConformanceAssertions`). Emitted first so that, for a non-conforming type, this clear
97
+ // "requires that '…' conform to '…'" error is reported ahead of the noisier "no member 'decode'"
98
+ // errors from the conversion code below.
97
99
  let assertions = properties.map { property in
98
- // A bare `Any` can't conform to anything and needs no conformance, since every path converts it
99
- // without one.
100
- ConformanceAssertion(
101
- name: property.name,
102
- types: property.freeForm?.isBareAny == true ? [] : [property.type],
103
- constraint: property.freeForm != nil ? jsConvertibleProtocolName : nil
104
- )
100
+ ConformanceAssertion(name: property.name, types: property.freeForm == nil ? [property.type] : [])
105
101
  }
106
- if let assertionMember = typeConformanceAssertions(for: assertions, constraint: recordFieldProtocolName) {
102
+ if let assertionMember = typeConformanceAssertions(for: assertions, constraint: javaScriptCodableProtocolName) {
107
103
  members.append(assertionMember)
108
104
  }
109
105
 
@@ -1,9 +1,8 @@
1
1
  import SwiftSyntax
2
2
  import SwiftSyntaxBuilder
3
3
 
4
- /// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
5
- /// eventual rename (this is a placeholder name) is a single edit, and shared by the macros whose
6
- /// generated conversions go through the dynamic-type API (`@Record`, `@Event`).
4
+ /// The protocol of the 1.0 dynamic-type API (`getDynamicType()`). `@Event` asserts payload types
5
+ /// against it.
7
6
  internal let jsConvertibleProtocolName = "AnyArgument"
8
7
 
9
8
  /// The constraint a `@JS` boundary type must satisfy, by direction: an argument (and a settable
@@ -15,23 +14,10 @@ internal let jsConvertibleProtocolName = "AnyArgument"
15
14
  internal let javaScriptDecodableProtocolName = "JavaScriptDecodable"
16
15
  internal let javaScriptEncodableProtocolName = "JavaScriptEncodable"
17
16
 
18
- /// The constraint a `@Record` property type must satisfy: it must be both `AnyArgument` (the
19
- /// `from(dictionary:)` / `toDictionary(appContext:)` paths still convert native `Any` through the
20
- /// dynamic-type API) and `JavaScriptDecodable & JavaScriptEncodable` (the `from(object:)` /
21
- /// `toObject(appContext:)` paths convert JS values through `decode`/`encode`). A record field has to
22
- /// support both directions, so the assertion requires the intersection.
23
- ///
24
- /// The exception is a property whose type mentions `Any` (`[String: Any]?`, `[Any]`, …): it can't
25
- /// conform to the JS codable protocols, so the JS-value paths convert it through the `JavaScriptValue`
26
- /// free-form methods, and it's asserted against `jsConvertibleProtocolName` alone for the dictionary
27
- /// paths.
28
- internal let recordFieldProtocolName = "AnyArgument & JavaScriptDecodable & JavaScriptEncodable"
29
-
30
- /// The constraint a `@Union` case payload type must satisfy: the union decodes by trying each payload's
31
- /// `decode` and encodes through the matching payload's `encode`, so every alternative must convert in
32
- /// both directions. Unlike a `@Record` field, a union never crosses the native-`Any` dictionary path, so
33
- /// `AnyArgument` is not required.
34
- internal let unionPayloadProtocolName = "JavaScriptDecodable & JavaScriptEncodable"
17
+ /// The constraint a `@Record` property type and a `@Union` case payload type must satisfy. A record
18
+ /// decodes and encodes each property, and a union decodes by trying each payload's `decode` and
19
+ /// encodes through the matching payload's `encode`, so both convert in both directions.
20
+ internal let javaScriptCodableProtocolName = "JavaScriptDecodable & JavaScriptEncodable"
35
21
 
36
22
  /// The protocol a type must conform to for `self.emit(event:…)` to resolve; core conforms
37
23
  /// `BaseModule` and `SharedObject` to it. Asserted by `@Event` so attaching it to a type that can't
@@ -52,20 +38,15 @@ internal struct ConformanceAssertion {
52
38
  /// Declared types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
53
39
  /// conditional conformances transitively constrain the elements.
54
40
  let types: [String]
55
- /// The constraint for this member's types, overriding the one passed to `typeConformanceAssertions`.
56
- /// `@Record` uses it to assert only `AnyArgument` for a property that mentions `Any`, which can't be
57
- /// `JavaScriptDecodable & JavaScriptEncodable`.
58
- var constraint: String? = nil
59
41
  }
60
42
 
61
43
  /// A directional conformance-assertion peer for a `@JS` member: a never-called `private func` whose
62
44
  /// body asserts each argument type is `JavaScriptDecodable` and the return/getter type is
63
45
  /// `JavaScriptEncodable`, matching how the generated binding converts each (decode on the way in,
64
- /// encode on the way out). The check is a single member-named nested helper with one `A0…` generic
65
- /// parameter per asserted argument and a single `Return` parameter for the value, called once with
66
- /// every type's metatype; naming the helper after the member puts the member in the compiler's
67
- /// diagnostic, and the per-slot constraint reports the offending type against the protocol for *its*
68
- /// direction.
46
+ /// encode on the way out). The check is a single member-named nested helper with one opaque metatype
47
+ /// parameter per asserted type, called once with every type's metatype; naming the helper after the
48
+ /// member puts the member in the compiler's diagnostic, and the per-parameter constraint reports the
49
+ /// offending type against the protocol for *its* direction.
69
50
  ///
70
51
  /// `decodableTypes` are the argument types (and a settable property's value type); `encodableType` is
71
52
  /// the single return type (or a property's value type on read), or `nil` when there's none.
@@ -87,31 +68,18 @@ internal func directionalConformanceAssertion(
87
68
  return nil
88
69
  }
89
70
 
90
- // Build the generic parameter list and the matching call arguments in lockstep: one `A0…` slot
91
- // constrained `JavaScriptDecodable` per argument, one `E0…` slot constrained `JavaScriptEncodable`
92
- // per closure-argument parameter, and a single `Return` slot constrained `JavaScriptEncodable` for
93
- // the return/getter value (there's only ever one, so it isn't indexed). Every slot suppresses
94
- // `Copyable`, so a non-copyable boundary type such as `JavaScriptValue` can fill it.
95
- var parameters: [String] = []
96
- var typeParameters: [String] = []
97
- var arguments: [String] = []
98
- for (index, type) in decodables.enumerated() {
99
- parameters.append("A\(index): \(javaScriptDecodableProtocolName) & ~Copyable")
100
- typeParameters.append("_: A\(index).Type")
101
- arguments.append("\(type).self")
102
- }
103
- for (index, type) in callbackArguments.enumerated() {
104
- parameters.append("E\(index): \(javaScriptEncodableProtocolName) & ~Copyable")
105
- typeParameters.append("_: E\(index).Type")
106
- arguments.append("\(type).self")
107
- }
108
- if let encodable {
109
- parameters.append("Return: \(javaScriptEncodableProtocolName) & ~Copyable")
110
- typeParameters.append("_: Return.Type")
111
- arguments.append("\(encodable).self")
112
- }
113
-
114
- let helper = "func \(name)<\(parameters.joined(separator: ", "))>(\(typeParameters.joined(separator: ", "))) {}"
71
+ // Arguments are constrained `JavaScriptDecodable`, and closure-argument parameters and the
72
+ // return/getter value `JavaScriptEncodable`. Every parameter suppresses `Copyable`, so a
73
+ // non-copyable boundary type such as `JavaScriptValue` can fill it.
74
+ let encodables = callbackArguments + [encodable].compactMap { $0 }
75
+ let decodableParameter = opaqueMetatypeParameter(constraint: "\(javaScriptDecodableProtocolName) & ~Copyable")
76
+ let encodableParameter = opaqueMetatypeParameter(constraint: "\(javaScriptEncodableProtocolName) & ~Copyable")
77
+ let parameters =
78
+ Array(repeating: decodableParameter, count: decodables.count)
79
+ + Array(repeating: encodableParameter, count: encodables.count)
80
+ let arguments = (decodables + encodables).map { "\($0).self" }
81
+
82
+ let helper = "func \(name)(\(parameters.joined(separator: ", "))) {}"
115
83
  let call = "\(name)(\(arguments.joined(separator: ", ")))"
116
84
  let staticKeyword = isStatic ? "static " : ""
117
85
  let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
@@ -132,9 +100,9 @@ internal func directionalConformanceAssertion(
132
100
  /// caller emits nothing.
133
101
  internal func typeConformanceAssertions(
134
102
  for assertions: [ConformanceAssertion],
135
- constraint: String = jsConvertibleProtocolName
103
+ constraint: String
136
104
  ) -> DeclSyntax? {
137
- let bodies = assertions.compactMap { conformanceAssertionBody($0, constraint: $0.constraint ?? constraint) }
105
+ let bodies = assertions.compactMap { conformanceAssertionBody($0, constraint: constraint) }
138
106
  guard !bodies.isEmpty else {
139
107
  return nil
140
108
  }
@@ -186,25 +154,33 @@ private func setsConform(to constraint: String) -> Bool {
186
154
  }
187
155
  }
188
156
 
189
- /// The assertion's body fragment: a nested generic helper named after the member, plus one call per
190
- /// distinct type that needs an assertion. Nesting the helper keeps the constraint entirely local — no
191
- /// shared symbol, nothing to collide, nothing left in the type's namespace — and naming it after the
192
- /// member puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every
193
- /// type was known to conform or the list was empty.
157
+ /// The assertion's body fragment: a nested helper named after the member, plus one call per distinct
158
+ /// type that needs an assertion. Nesting the helper keeps the constraint entirely local — no shared
159
+ /// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
160
+ /// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
161
+ /// known to conform or the list was empty.
194
162
  private func conformanceAssertionBody(
195
163
  _ assertion: ConformanceAssertion,
196
- constraint: String = jsConvertibleProtocolName
164
+ constraint: String
197
165
  ) -> String? {
198
166
  let distinct = distinctAssertableTypes(assertion.types, constraint: constraint)
199
167
  guard !distinct.isEmpty else {
200
168
  return nil
201
169
  }
202
170
 
203
- var lines = ["func \(assertion.name)<T: \(constraint)>(_: T.Type) {}"]
171
+ var lines = ["func \(assertion.name)(\(opaqueMetatypeParameter(constraint: constraint))) {}"]
204
172
  lines.append(contentsOf: distinct.map { "\(assertion.name)(\($0).self)" })
205
173
  return lines.joined(separator: "\n")
206
174
  }
207
175
 
176
+ /// An unlabeled parameter that takes the metatype of any type satisfying `constraint`, for an
177
+ /// assertion helper.
178
+ internal func opaqueMetatypeParameter(constraint: String) -> String {
179
+ // A named generic parameter would shadow a generic parameter of the same name on an enclosing
180
+ // type, which is an error. An opaque parameter declares no name.
181
+ return "_: (some \(constraint)).Type"
182
+ }
183
+
208
184
  /// The distinct types to assert against `constraint` from a list, mapping each through
209
185
  /// `assertableBoundaryType` (which drops what needs no assertion) and deduping in first-seen order.
210
186
  private func distinctAssertableTypes(_ types: [String], constraint: String) -> [String] {
@@ -57,7 +57,7 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
57
57
  // reports the clear "requires that '…' conform to …" error ahead of the noisier "no member
58
58
  // 'decode'"/"'encode'" errors from the conversion code below.
59
59
  let assertions = union.cases.map { ConformanceAssertion(name: $0.name, types: [$0.payloadType]) }
60
- if let assertionMember = typeConformanceAssertions(for: assertions, constraint: unionPayloadProtocolName) {
60
+ if let assertionMember = typeConformanceAssertions(for: assertions, constraint: javaScriptCodableProtocolName) {
61
61
  members.append(assertionMember)
62
62
  }
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-macros",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -26,9 +26,9 @@
26
26
  "typescript": "^5.4.0"
27
27
  },
28
28
  "optionalDependencies": {
29
- "@expo/modules-macros-linux-arm64": "0.23.0",
30
- "@expo/modules-macros-linux-x64": "0.23.0",
31
- "@expo/modules-macros-win32-arm64": "0.23.0",
32
- "@expo/modules-macros-win32-x64": "0.23.0"
29
+ "@expo/modules-macros-linux-arm64": "0.24.0",
30
+ "@expo/modules-macros-linux-x64": "0.24.0",
31
+ "@expo/modules-macros-win32-arm64": "0.24.0",
32
+ "@expo/modules-macros-win32-x64": "0.24.0"
33
33
  }
34
34
  }