expo-modules-macros 0.25.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.
@@ -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.25.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.25.0",
30
- "@expo/modules-macros-linux-x64": "0.25.0",
31
- "@expo/modules-macros-win32-arm64": "0.25.0",
32
- "@expo/modules-macros-win32-x64": "0.25.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
  }