expo-modules-macros 0.24.0 → 0.26.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.
@@ -64,6 +64,7 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
64
64
  members.append(decodableKindsProperty(union: union))
65
65
  members.append(decodeMethod(union: union))
66
66
  members.append(encodeMethod(union: union))
67
+ members.append(encodeIntoSlotMethod(union: union))
67
68
  members.append(contentsOf: accessorMethods(union: union))
68
69
  members.append(payloadTypeNameProperty(union: union))
69
70
  return members
@@ -269,9 +270,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
269
270
  private func decodeMethod(union: UnionType) -> DeclSyntax {
270
271
  var lines: [String] = [" let kind = JavaScriptValueKinds(of: value)"]
271
272
  for unionCase in union.cases {
272
- let payloadType = expressionType(unionCase.payloadType)
273
+ let conversion = payloadConversion(unionCase)
273
274
  lines.append(
274
- " if \(payloadType).decodableKinds.contains(kind), let payload = try? \(payloadType).decode(value, in: runtime) {"
275
+ " if \(conversion.decodableKindsExpression).contains(kind), let payload = try? \(conversion.decodeExpression(of: "value")) {"
275
276
  )
276
277
  lines.append(" return \(unionCase.construction)")
277
278
  lines.append(" }")
@@ -292,7 +293,7 @@ private func decodeMethod(union: UnionType) -> DeclSyntax {
292
293
  /// `decodableKinds`: the union of the payloads' kinds, so a union nested in another union, or wrapped
293
294
  /// in an optional, is skipped as precisely as its payloads are.
294
295
  private func decodableKindsProperty(union: UnionType) -> DeclSyntax {
295
- let kinds = union.cases.map { "\(expressionType($0.payloadType)).decodableKinds" }
296
+ let kinds = union.cases.map { payloadConversion($0).decodableKindsExpression }
296
297
  // An option set's array literal is the union of its elements, which reads like the case list. The
297
298
  // optimizer folds it to a constant after specialization, the same as a chain of `union(_:)` calls.
298
299
  let expression = kinds.count == 1 ? kinds[0] : "[\(kinds.joined(separator: ", "))]"
@@ -310,7 +311,7 @@ private func encodeMethod(union: UnionType) -> DeclSyntax {
310
311
  var lines: [String] = [" switch value {"]
311
312
  for unionCase in union.cases {
312
313
  lines.append(" case \(unionCase.pattern):")
313
- lines.append(" return try \(expressionType(unionCase.payloadType)).encode(payload, in: runtime)")
314
+ lines.append(" return try \(payloadConversion(unionCase).encodeExpression(of: "payload"))")
314
315
  }
315
316
  lines.append(" }")
316
317
  let body = lines.joined(separator: "\n")
@@ -322,6 +323,35 @@ private func encodeMethod(union: UnionType) -> DeclSyntax {
322
323
  """
323
324
  }
324
325
 
326
+ /// `encode(_:into:in:)`: the same `switch` as `encode(_:in:)`, with each payload encoded straight into the
327
+ /// slot through the payload type's own `encode(_:into:in:)`. A payload that is a number, a boolean or a
328
+ /// string is then constructed in the slot (an array element, an argument or a result), with no
329
+ /// `JavaScriptValue` in between.
330
+ private func encodeIntoSlotMethod(union: UnionType) -> DeclSyntax {
331
+ var lines: [String] = [" switch value {"]
332
+ for unionCase in union.cases {
333
+ lines.append(" case \(unionCase.pattern):")
334
+ if let tuple = tupleConversion(of: unionCase.payloadType) {
335
+ // A payload that holds a tuple has no `encode(_:into:in:)`, so its encoded value is moved in.
336
+ lines.append(" slot.emplace(try \(tuple.encodeExpression(of: "payload")))")
337
+ } else {
338
+ lines.append(" try \(expressionType(unionCase.payloadType)).encode(payload, into: slot, in: runtime)")
339
+ }
340
+ }
341
+ lines.append(" }")
342
+ let body = lines.joined(separator: "\n")
343
+ return """
344
+ @JavaScriptActor
345
+ public static func encode(
346
+ _ value: Self,
347
+ into slot: borrowing JavaScriptValue.Slot,
348
+ in runtime: borrowing JavaScriptRuntime
349
+ ) throws {
350
+ \(raw: body)
351
+ }
352
+ """
353
+ }
354
+
325
355
  /// `as(_:)`: a typed accessor per case, selected by the payload's metatype rather than the case name,
326
356
  /// so a caller that only knows the type it wants writes `try value.as(String.self)` and gets a `String`.
327
357
  /// When
@@ -414,3 +444,9 @@ private struct UnionFixItMessage: FixItMessage {
414
444
  self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
415
445
  }
416
446
  }
447
+
448
+ /// How a case's payload converts: through the tuple methods when it holds a tuple (see
449
+ /// `TupleConversion`), and through its own `decode` / `encode` otherwise.
450
+ private func payloadConversion(_ unionCase: UnionCase) -> TupleConversion {
451
+ return tupleConversion(of: unionCase.payloadType) ?? .conforming(type: unionCase.payloadType)
452
+ }
@@ -463,8 +463,9 @@ struct ExportedSurface: Encodable, Equatable {
463
463
  /// qualified names and gives a resolved ref the name of its declaration; version 8 reports
464
464
  /// declarations in `#if` blocks, adds `platforms` (with deprecations), `accessLevel`, and
465
465
  /// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
466
- /// `sharedRef` type kind and the `sharedRef` of a shared object.
467
- let scanExportsSchemaVersion = 9
466
+ /// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
467
+ /// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes.
468
+ let scanExportsSchemaVersion = 10
468
469
 
469
470
  /// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
470
471
  /// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
@@ -136,6 +136,15 @@ extension TypeNode {
136
136
  )
137
137
  case .promise(let value):
138
138
  return .promise(value: value.resolvingRefs(using: index, in: scope))
139
+ case .tuple(let elements, let name):
140
+ return .tuple(elements: elements.map { $0.resolvingRefs(using: index, in: scope) }, name: name)
141
+ case .object(let properties, let name):
142
+ return .object(
143
+ properties: properties.map {
144
+ TypeNodeProperty(name: $0.name, type: $0.type.resolvingRefs(using: index, in: scope))
145
+ },
146
+ name: name
147
+ )
139
148
  case .function(let parameters, let returns, let isAsync, let isThrowing):
140
149
  return .function(
141
150
  parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
@@ -142,13 +142,14 @@ final class SurfaceVisitor: SyntaxVisitor {
142
142
  guard scope.isEmpty else {
143
143
  return
144
144
  }
145
+ let aliases = TupleTypeAliases(in: members, scope: qualified)
145
146
  let (functions, properties, events, _) = collectJSMembers(members)
146
147
  modules.append(
147
148
  ExportedModule(
148
149
  name: qualified,
149
150
  jsName: stringArgument(of: attribute) ?? name,
150
- functions: functions,
151
- properties: properties,
151
+ functions: functions.map(aliases.resolving),
152
+ properties: properties.map(aliases.resolving),
152
153
  events: events,
153
154
  // `jsName` is filled in by `ExportedSurface.resolvingRefs()`, since the `@SharedObject` can
154
155
  // be declared in another file.
@@ -164,7 +165,13 @@ final class SurfaceVisitor: SyntaxVisitor {
164
165
  }
165
166
 
166
167
  if let attribute = attributes.firstAttribute(named: DetectedMacro.sharedObject.rawValue) {
167
- let (functions, properties, events, initializer) = collectJSMembers(members)
168
+ let aliases = TupleTypeAliases(in: members, scope: qualified)
169
+ var (functions, properties, events, initializer) = collectJSMembers(members)
170
+ functions = functions.map(aliases.resolving)
171
+ properties = properties.map(aliases.resolving)
172
+ if let parameters = initializer?.parameters {
173
+ initializer?.parameters = parameters.map(aliases.resolving)
174
+ }
168
175
  sharedObjects.append(
169
176
  ExportedSharedObject(
170
177
  name: qualified,
@@ -0,0 +1,126 @@
1
+ import SwiftSyntax
2
+
3
+ /// The type aliases declared in the body of an `@ExpoModule` or `@SharedObject` class whose aliased type
4
+ /// holds a tuple, resolved to that type. Mirrors `TupleTypeAliases` in the macros, which resolve these
5
+ /// aliases in the `@JS` members the class binds, so that a member typed with such an alias reports the
6
+ /// tuple it converts as instead of an unresolvable ref.
7
+ ///
8
+ /// Like the macros, it reads only the class body: an alias declared anywhere else stays a ref. A generic
9
+ /// alias isn't resolved, and neither is an alias in an `@Event` payload, which `@Event` converts on its
10
+ /// own.
11
+ struct TupleTypeAliases {
12
+ /// The resolved type of each alias, by name.
13
+ private let resolved: [String: TypeNode]
14
+
15
+ /// Collects the aliases in `members`, the body of the type named `scope`. A tuple alias keeps its name
16
+ /// qualified with `scope`, like the other declarations in the scan, so a consumer can name the type it
17
+ /// stands for.
18
+ init(in members: MemberBlockItemListSyntax, scope: String) {
19
+ var declared: [String: TypeNode] = [:]
20
+ for member in members {
21
+ guard let alias = member.decl.as(TypeAliasDeclSyntax.self), alias.genericParameterClause == nil else {
22
+ continue
23
+ }
24
+ let name = alias.name.text
25
+ declared[name] = typeNode(from: alias.initializer.value).named("\(scope).\(name)")
26
+ }
27
+ // Aliases may refer to each other, so each one is resolved through the others. An alias that refers
28
+ // back to itself is invalid Swift, and is left as written after `declared.count` rounds.
29
+ var resolved = declared
30
+ for _ in 0..<declared.count {
31
+ resolved = resolved.mapValues { $0.replacingAliases(resolved) }
32
+ }
33
+ self.resolved = resolved.filter { $0.value.holdsTuple }
34
+ }
35
+
36
+ /// `node` with each ref to an alias that holds a tuple replaced by the aliased type.
37
+ func resolving(_ node: TypeNode) -> TypeNode {
38
+ guard !resolved.isEmpty else {
39
+ return node
40
+ }
41
+ return node.replacingAliases(resolved)
42
+ }
43
+
44
+ func resolving(_ function: ExportedFunction) -> ExportedFunction {
45
+ var resolvedFunction = function
46
+ resolvedFunction.parameters = function.parameters.map(resolving)
47
+ resolvedFunction.returns = function.returns.map(resolving)
48
+ return resolvedFunction
49
+ }
50
+
51
+ func resolving(_ property: ExportedProperty) -> ExportedProperty {
52
+ var resolvedProperty = property
53
+ resolvedProperty.type = property.type.map(resolving)
54
+ return resolvedProperty
55
+ }
56
+
57
+ func resolving(_ parameter: ExportedParameter) -> ExportedParameter {
58
+ var resolvedParameter = parameter
59
+ resolvedParameter.type = resolving(parameter.type)
60
+ return resolvedParameter
61
+ }
62
+ }
63
+
64
+ extension TypeNode {
65
+ /// This node with each ref named in `aliases`, bare (`Span`) or qualified with `Self` (`Self.Span`),
66
+ /// replaced by the aliased type.
67
+ fileprivate func replacingAliases(_ aliases: [String: TypeNode]) -> TypeNode {
68
+ switch self {
69
+ case .primitive, .unknown, .sharedRef:
70
+ return self
71
+ case .ref(let name, _, _):
72
+ let unqualified = name.hasPrefix("Self.") ? String(name.dropFirst("Self.".count)) : name
73
+ return aliases[unqualified] ?? self
74
+ case .optional(let wrapped):
75
+ return .optional(wrapped: wrapped.replacingAliases(aliases))
76
+ case .array(let element):
77
+ return .array(element: element.replacingAliases(aliases))
78
+ case .dictionary(let key, let value):
79
+ return .dictionary(key: key.replacingAliases(aliases), value: value.replacingAliases(aliases))
80
+ case .promise(let value):
81
+ return .promise(value: value.replacingAliases(aliases))
82
+ case .tuple(let elements, let name):
83
+ return .tuple(elements: elements.map { $0.replacingAliases(aliases) }, name: name)
84
+ case .object(let properties, let name):
85
+ return .object(
86
+ properties: properties.map { TypeNodeProperty(name: $0.name, type: $0.type.replacingAliases(aliases)) },
87
+ name: name
88
+ )
89
+ case .function(let parameters, let returns, let isAsync, let isThrowing):
90
+ return .function(
91
+ parameters: parameters.map { $0.replacingAliases(aliases) },
92
+ returns: returns?.replacingAliases(aliases),
93
+ isAsync: isAsync,
94
+ isThrowing: isThrowing
95
+ )
96
+ }
97
+ }
98
+
99
+ /// This node named `name` when it's a tuple or an object, which an alias names. Any other node, such as
100
+ /// an array of tuples, is returned as it is: its elements carry the names of their own aliases.
101
+ fileprivate func named(_ name: String) -> TypeNode {
102
+ switch self {
103
+ case .tuple(let elements, _):
104
+ return .tuple(elements: elements, name: name)
105
+ case .object(let properties, _):
106
+ return .object(properties: properties, name: name)
107
+ default:
108
+ return self
109
+ }
110
+ }
111
+
112
+ /// True for a type the macros convert through the tuple methods: a tuple (`.tuple`, or `.object` for a
113
+ /// labeled one), or an optional, array or `String`-keyed dictionary that holds one.
114
+ fileprivate var holdsTuple: Bool {
115
+ switch self {
116
+ case .tuple, .object:
117
+ return true
118
+ case .optional(let inner), .array(let inner):
119
+ return inner.holdsTuple
120
+ case .dictionary(.primitive("String", _), let value):
121
+ return value.holdsTuple
122
+ default:
123
+ return false
124
+ }
125
+ }
126
+ }
@@ -72,12 +72,22 @@ indirect enum TypeNode: Equatable {
72
72
  /// node parsed at a use site has never seen.
73
73
  case ref(name: String, refKind: RefKind? = nil, jsTypeOverride: JSType? = nil)
74
74
 
75
+ /// An unlabeled tuple of two or more elements, `(A, B)`, which crosses as an array with one value per
76
+ /// element, like a TypeScript tuple. `name` is the qualified name of the type alias it was written
77
+ /// through, such as `MyModule.Pair`, or `nil` when it was spelled out.
78
+ case tuple(elements: [TypeNode], name: String? = nil)
79
+
80
+ /// An object with a fixed set of properties, which a labeled tuple of two or more elements,
81
+ /// `(x: A, y: B)`, crosses as: one property per label. `name` is the qualified name of the type alias it
82
+ /// was written through, such as `MyModule.Span`, or `nil` when it was spelled out.
83
+ case object(properties: [TypeNodeProperty], name: String? = nil)
84
+
75
85
  /// `SharedRef<T>` (or `ExpoModulesCore.SharedRef<T>`): a shared object holding a native `T`, which
76
86
  /// JS sees as an instance of `expo.SharedRef`. `native` is `T` parsed like any other type, but it
77
87
  /// never crosses the boundary itself, so refs in it are not resolved.
78
88
  case sharedRef(native: TypeNode)
79
89
 
80
- /// A spelling the parser doesn't model (generic parameter, tuple, metatype, …), kept verbatim so
90
+ /// A spelling the parser doesn't model (generic parameter, metatype, …), kept verbatim so
81
91
  /// nothing is lost. Has no `typeof`.
82
92
  case unknown(text: String)
83
93
 
@@ -87,7 +97,7 @@ indirect enum TypeNode: Equatable {
87
97
  switch self {
88
98
  case .primitive(_, let jsType):
89
99
  return jsType
90
- case .array, .dictionary, .promise, .sharedRef:
100
+ case .array, .dictionary, .promise, .tuple, .object, .sharedRef:
91
101
  return .object
92
102
  case .ref(_, _, let jsTypeOverride):
93
103
  // An unresolved ref, and every resolved one but an enum, is an object.
@@ -102,6 +112,12 @@ indirect enum TypeNode: Equatable {
102
112
  }
103
113
  }
104
114
 
115
+ /// A property of a `.object` node: its name, and the type of its value.
116
+ struct TypeNodeProperty: Encodable, Equatable {
117
+ let name: String
118
+ let type: TypeNode
119
+ }
120
+
105
121
  extension TypeNode: Encodable {
106
122
  private enum CodingKeys: String, CodingKey {
107
123
  case refKind
@@ -110,7 +126,7 @@ extension TypeNode: Encodable {
110
126
  // The `typeof` category. Spelled `typeof` in JSON (what the consumer reads); the Swift property is
111
127
  // `jsType` since it's the cleaner Swift name.
112
128
  case jsType = "typeof"
113
- case wrapped, element, key, value, parameters, returns, text, native
129
+ case wrapped, element, key, value, parameters, returns, text, native, elements, properties
114
130
  // A closure type's effects, matching `ExportedFunction`'s TS-keyword spellings.
115
131
  case isAsync = "async"
116
132
  case isThrowing = "throws"
@@ -148,6 +164,14 @@ extension TypeNode: Encodable {
148
164
  try container.encode("ref", forKey: .kind)
149
165
  try container.encode(name, forKey: .name)
150
166
  try container.encodeIfPresent(refKind, forKey: .refKind)
167
+ case .tuple(let elements, let name):
168
+ try container.encode("tuple", forKey: .kind)
169
+ try container.encode(elements, forKey: .elements)
170
+ try container.encodeIfPresent(name, forKey: .name)
171
+ case .object(let properties, let name):
172
+ try container.encode("object", forKey: .kind)
173
+ try container.encode(properties, forKey: .properties)
174
+ try container.encodeIfPresent(name, forKey: .name)
151
175
  case .sharedRef(let native):
152
176
  try container.encode("sharedRef", forKey: .kind)
153
177
  try container.encode(native, forKey: .native)
@@ -228,7 +252,22 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
228
252
  }
229
253
  return .ref(name: member.trimmedDescription)
230
254
  }
231
- // Tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
255
+ // `(A, B)` / `(x: A, y: B)`, matching the tuples the macros convert: two or more elements, all
256
+ // labeled or none, without a variadic or `inout` element.
257
+ if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count >= 2,
258
+ tuple.elements.allSatisfy({ $0.ellipsis == nil && $0.inoutKeyword == nil })
259
+ {
260
+ let labels = tuple.elements.compactMap { $0.firstName?.text }
261
+ if labels.isEmpty {
262
+ return .tuple(elements: tuple.elements.map { typeNode(from: $0.type) })
263
+ }
264
+ if labels.count == tuple.elements.count {
265
+ return .object(
266
+ properties: zip(labels, tuple.elements).map { TypeNodeProperty(name: $0, type: typeNode(from: $1.type)) }
267
+ )
268
+ }
269
+ }
270
+ // Other tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
232
271
  return .unknown(text: type.trimmedDescription)
233
272
  }
234
273
 
@@ -316,6 +355,10 @@ extension TypeNode {
316
355
  return "Promise<\(value.spelling)>"
317
356
  case .sharedRef(let native):
318
357
  return "SharedRef<\(native.spelling)>"
358
+ case .tuple(let elements, _):
359
+ return "(\(elements.map(\.spelling).joined(separator: ", ")))"
360
+ case .object(let properties, _):
361
+ return "(\(properties.map { "\($0.name): \($0.type.spelling)" }.joined(separator: ", ")))"
319
362
  case .function(let parameters, let returns, _, _):
320
363
  return "(\(parameters.map { $0.spelling }.joined(separator: ", "))) -> \(returns?.spelling ?? "Void")"
321
364
  case .unknown(let text):
package/build/types.d.ts CHANGED
@@ -61,6 +61,37 @@ export type TypeNode = {
61
61
  name: string;
62
62
  refKind?: 'record' | 'sharedObject' | 'enum' | 'union';
63
63
  }
64
+ /**
65
+ * An unlabeled Swift tuple of two or more elements, `(A, B)`. It crosses as an array with one value per
66
+ * element, like a TypeScript tuple.
67
+ */
68
+ | {
69
+ kind: 'tuple';
70
+ typeof: JSType;
71
+ elements: TypeNode[];
72
+ /**
73
+ * The qualified name of the type alias the tuple was written through, such as `MyModule.Pair`.
74
+ * Absent when the tuple is spelled out.
75
+ */
76
+ name?: string;
77
+ }
78
+ /**
79
+ * An object with a fixed set of properties. A labeled Swift tuple of two or more elements,
80
+ * `(x: A, y: B)`, crosses as one, with one property per label.
81
+ */
82
+ | {
83
+ kind: 'object';
84
+ typeof: JSType;
85
+ properties: {
86
+ name: string;
87
+ type: TypeNode;
88
+ }[];
89
+ /**
90
+ * The qualified name of the type alias the labeled tuple was written through, such as
91
+ * `MyModule.Span`. Absent when the tuple is spelled out.
92
+ */
93
+ name?: string;
94
+ }
64
95
  /**
65
96
  * `SharedRef<T>`: a shared object that JS sees as an instance of `expo.SharedRef`. `native` is the
66
97
  * `T` it holds on the native side. It never reaches JS, so refs in it are not resolved.
@@ -389,4 +420,4 @@ export interface ScanExportsResult {
389
420
  * as a clear error instead of silently misread fields.
390
421
  */
391
422
  export declare const SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
392
- export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 9;
423
+ export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 10;
package/build/types.js CHANGED
@@ -12,4 +12,4 @@ exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = exports.SUPPORTED_SCAN_MODULES_S
12
12
  * as a clear error instead of silently misread fields.
13
13
  */
14
14
  exports.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
15
- exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 9;
15
+ exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 10;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-macros",
3
- "version": "0.24.0",
3
+ "version": "0.26.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.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"
29
+ "@expo/modules-macros-linux-arm64": "0.26.0",
30
+ "@expo/modules-macros-linux-x64": "0.26.0",
31
+ "@expo/modules-macros-win32-arm64": "0.26.0",
32
+ "@expo/modules-macros-win32-x64": "0.26.0"
33
33
  }
34
34
  }