expo-modules-macros 0.21.0 → 0.22.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.
Files changed (34) hide show
  1. package/.github/workflows/swift-format.yml +76 -0
  2. package/AGENTS.md +9 -4
  3. package/README.md +15 -2
  4. package/apple/ExpoModulesMacros +0 -0
  5. package/apple/Package.resolved +3 -3
  6. package/apple/Package.swift +1 -2
  7. package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
  8. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +113 -26
  9. package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
  10. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +53 -45
  11. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
  12. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
  13. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
  14. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +180 -44
  15. package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
  16. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +163 -42
  17. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +285 -221
  18. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +45 -32
  19. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
  20. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
  21. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
  22. package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
  23. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
  24. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
  25. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
  26. package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
  27. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +3 -2
  28. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +15 -5
  29. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
  30. package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +127 -45
  31. package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +2 -1
  32. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
  33. package/build/types.d.ts +7 -1
  34. package/package.json +7 -5
@@ -54,7 +54,8 @@ public struct EventMacro: AccessorMacro {
54
54
  // through the getter, so the body never has to spell the payload type. A sync event calls
55
55
  // `emitSync` (inline dispatch, JS thread only); the default calls the scheduling `emit`.
56
56
  let emitMethod = event.isSync ? "emitSync" : "emit"
57
- let closure = event.hasPayload
57
+ let closure =
58
+ event.hasPayload
58
59
  ? "{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload) }"
59
60
  : "{ [weak self] in self?.\(emitMethod)(event: \"\(event.jsName)\") }"
60
61
  return [
@@ -85,9 +86,10 @@ extension EventMacro: PeerMacro {
85
86
  // and asserting the enclosing type by its spelled name so the conformance error names the
86
87
  // user's type rather than 'Self'.
87
88
  let name = event.swiftName
88
- let payload = event.payloadType.flatMap(assertableBoundaryType)
89
+ let payload = event.payloadType.flatMap { assertableBoundaryType($0, constraint: jsConvertibleProtocolName) }
89
90
  let owner = enclosingTypeName(in: context) ?? "Self"
90
- let helper = payload != nil
91
+ let helper =
92
+ payload != nil
91
93
  ? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
92
94
  : "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
93
95
  let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
@@ -132,7 +134,8 @@ private func validatedEvent(
132
134
  }
133
135
  if varDecl.attributes.firstAttribute(named: "JS") != nil {
134
136
  throw MacroExpansionErrorMessage(
135
- "@Event and @JS cannot be combined on the same property; an event is exposed to JS on its own, so remove one of the attributes")
137
+ "@Event and @JS cannot be combined on the same property; an event is exposed to JS on its own, so remove one of the attributes"
138
+ )
136
139
  }
137
140
  // The compiler also rejects accessor macros on a `let`, but with a generic message; this one says
138
141
  // what to do instead and carries the fix-it doing it. The synthesized property is getter-only, so
@@ -142,32 +145,40 @@ private func validatedEvent(
142
145
  }
143
146
  if varDecl.modifiers.contains(where: isTypeLevelModifier) {
144
147
  throw MacroExpansionErrorMessage(
145
- "@Event must be an instance property; events are emitted from a module or shared object instance.")
148
+ "@Event must be an instance property; events are emitted from a module or shared object instance."
149
+ )
146
150
  }
147
151
  guard varDecl.bindings.count == 1, let binding = varDecl.bindings.first,
148
- let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
152
+ let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
153
+ else {
149
154
  throw MacroExpansionErrorMessage(
150
- "@Event must be applied to a single named property; declare each event separately")
155
+ "@Event must be applied to a single named property; declare each event separately"
156
+ )
151
157
  }
152
158
  if binding.initializer != nil {
153
159
  throw MacroExpansionErrorMessage(
154
- "@Event property cannot have an initial value; the macro synthesizes the closure")
160
+ "@Event property cannot have an initial value; the macro synthesizes the closure"
161
+ )
155
162
  }
156
163
  if binding.accessorBlock != nil {
157
164
  throw MacroExpansionErrorMessage(
158
- "@Event property cannot declare its own accessors; the macro synthesizes the getter")
165
+ "@Event property cannot declare its own accessors; the macro synthesizes the getter"
166
+ )
159
167
  }
160
168
  guard let functionType = underlyingFunctionType(of: binding.typeAnnotation?.type) else {
161
169
  throw MacroExpansionErrorMessage(
162
- "@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'")
170
+ "@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'"
171
+ )
163
172
  }
164
173
  guard isVoidReturn(functionType.returnClause.type) else {
165
174
  throw MacroExpansionErrorMessage(
166
- "@Event function type must return 'Void'; an event dispatches to JS and has no return value")
175
+ "@Event function type must return 'Void'; an event dispatches to JS and has no return value"
176
+ )
167
177
  }
168
178
  guard functionType.parameters.count <= 1 else {
169
179
  throw MacroExpansionErrorMessage(
170
- "@Event function type takes at most one payload parameter; combine multiple values into a single record")
180
+ "@Event function type takes at most one payload parameter; combine multiple values into a single record"
181
+ )
171
182
  }
172
183
  // The payload is encoded to JavaScript, and a native closure can't be encoded.
173
184
  if let payloadClosure = functionType.parameters.first.flatMap({ firstFunctionType(in: $0.type) }) {
@@ -177,7 +188,8 @@ private func validatedEvent(
177
188
  message: EventDiagnosticMessage(
178
189
  "@Event payload can't hold a closure: the payload is encoded to JavaScript, and a native closure can't be encoded.",
179
190
  id: "event-closure-payload"
180
- ))
191
+ )
192
+ )
181
193
  ])
182
194
  }
183
195
 
@@ -321,7 +333,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
321
333
  return underlyingFunctionType(of: attributed.baseType)
322
334
  }
323
335
  if let tuple = type.as(TupleTypeSyntax.self),
324
- tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil {
336
+ tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
337
+ {
325
338
  return underlyingFunctionType(of: element.type)
326
339
  }
327
340
  return type.as(FunctionTypeSyntax.self)
@@ -334,7 +347,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
334
347
  private func isVoidReturn(_ type: TypeSyntax) -> Bool {
335
348
  // Peel single-element, unlabeled parentheses: `(Void)` and `(())` are the same type as their content.
336
349
  if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
337
- let element = tuple.elements.first, element.firstName == nil {
350
+ let element = tuple.elements.first, element.firstName == nil
351
+ {
338
352
  return isVoidReturn(element.type)
339
353
  }
340
354
  let text = type.trimmedDescription
@@ -2,34 +2,32 @@ import SwiftSyntax
2
2
  import SwiftSyntaxBuilder
3
3
  import SwiftSyntaxMacros
4
4
 
5
- /**
6
- Macro applied to a module class. It plays three roles, each implemented in its own
7
- extension below:
8
-
9
- - `MemberMacro`: binds `@JS`-marked members directly into the module's JS object via the
10
- synthesized `_decorateModule`, and emits the resolved module name as a non-optional
11
- `_jsName` static (no `Name(…)` DSL element). It also synthesizes a
12
- framework-internal `_synthesizedDefinition()` returning `[AnyDefinition]` (now carrying
13
- only nested `classes` entries) that `expo-modules-core` merges into the module's
14
- definition. When the class doesn't already inherit `Module`/`BaseModule`, it also
15
- synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
16
- - `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
17
- `@ModuleDefinitionBuilder` on a `definition()` method.
18
- - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
19
-
20
- Inheriting from `Module` is therefore optional — a class can carry any superclass (or
21
- none) and still be a module, because everything inheritance used to provide is synthesized.
22
-
23
- @ExpoModule
24
- public final class MyModule {
25
- public func definition() -> ModuleDefinition {
26
- Name("MyModule")
27
- }
28
-
29
- @JS
30
- func greet(name: String) -> String { "Hi, \(name)" }
31
- }
32
- */
5
+ /// Macro applied to a module class. It plays three roles, each implemented in its own
6
+ /// extension below:
7
+ ///
8
+ /// - `MemberMacro`: binds `@JS`-marked members directly into the module's JS object via the
9
+ /// synthesized `_decorateModule`, and emits the resolved module name as a non-optional
10
+ /// `_jsName` static (no `Name(…)` DSL element). It also synthesizes a
11
+ /// framework-internal `_synthesizedDefinition()` returning `[AnyDefinition]` (now carrying
12
+ /// only nested `classes` entries) that `expo-modules-core` merges into the module's
13
+ /// definition. When the class doesn't already inherit `Module`/`BaseModule`, it also
14
+ /// synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
15
+ /// - `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
16
+ /// `@ModuleDefinitionBuilder` on a `definition()` method.
17
+ /// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
18
+ ///
19
+ /// Inheriting from `Module` is therefore optional — a class can carry any superclass (or
20
+ /// none) and still be a module, because everything inheritance used to provide is synthesized.
21
+ ///
22
+ /// @ExpoModule
23
+ /// public final class MyModule {
24
+ /// public func definition() -> ModuleDefinition {
25
+ /// Name("MyModule")
26
+ /// }
27
+ ///
28
+ /// @JS
29
+ /// func greet(name: String) -> String { "Hi, \(name)" }
30
+ /// }
33
31
  public struct ExpoModuleMacro: MemberMacro {
34
32
  public static func expansion(
35
33
  of node: AttributeSyntax,
@@ -62,13 +60,19 @@ public struct ExpoModuleMacro: MemberMacro {
62
60
  let decl = member.decl
63
61
 
64
62
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
65
- let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
66
- functions.append(JSFunction(funcDecl: funcDecl, attribute: attribute))
63
+ let attribute = funcDecl.attributes.firstAttribute(named: "JS")
64
+ {
65
+ let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
66
+ functions.append(function)
67
+ if let variant = function.asyncVariant {
68
+ functions.append(variant)
69
+ }
67
70
  continue
68
71
  }
69
72
 
70
73
  if let varDecl = decl.as(VariableDeclSyntax.self),
71
- let attribute = varDecl.attributes.firstAttribute(named: "JS") {
74
+ let attribute = varDecl.attributes.firstAttribute(named: "JS")
75
+ {
72
76
  properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
73
77
  }
74
78
  }
@@ -154,12 +158,14 @@ extension ExpoModuleMacro: MemberAttributeMacro {
154
158
 
155
159
  // `@JS` sync members run on the JS thread; stamp `@JavaScriptActor` so isolation is
156
160
  // checked at compile time. Skipped when the member already chose an isolation
157
- // (`async`, `nonisolated`, or another global actor) — see `shouldStampJavaScriptActor`.
161
+ // (`nonisolated` or another global actor), or when `.concurrent` or `.withAsync("name")` runs
162
+ // it on the concurrent pool. See `shouldStampJavaScriptActor`.
158
163
  // `@Event(sync: true)` members get the stamp too: a sync event dispatches inline, so the
159
164
  // isolation forces its call site onto the JS thread. Async events (the default) are
160
165
  // deliberately left unstamped — their `emit` schedules onto the JS thread itself.
161
166
  if memberHasJSAttribute(member) || isSyncEventMember(member),
162
- shouldStampJavaScriptActor(on: member, enclosedBy: declaration) {
167
+ shouldStampJavaScriptActor(on: member, enclosedBy: declaration)
168
+ {
163
169
  attributes.append("@JavaScriptActor")
164
170
  }
165
171
 
@@ -174,7 +180,8 @@ extension ExpoModuleMacro: MemberAttributeMacro {
174
180
  // they already wrote `@ModuleDefinitionBuilder` themselves, which would otherwise be
175
181
  // a duplicate attribute.
176
182
  if isModuleDefinitionFunction(member),
177
- !memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") }) {
183
+ !memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") })
184
+ {
178
185
  attributes.append("@ModuleDefinitionBuilder")
179
186
  }
180
187
 
@@ -186,7 +193,8 @@ private func isModuleDefinitionFunction(_ member: some DeclSyntaxProtocol) -> Bo
186
193
  guard let funcDecl = member.as(FunctionDeclSyntax.self),
187
194
  funcDecl.name.text == "definition",
188
195
  funcDecl.signature.parameterClause.parameters.isEmpty,
189
- let returnType = funcDecl.signature.returnClause?.type else {
196
+ let returnType = funcDecl.signature.returnClause?.type
197
+ else {
190
198
  return false
191
199
  }
192
200
  return baseIdentifier(of: returnType) == "ModuleDefinition"
@@ -215,7 +223,8 @@ extension ExpoModuleMacro: ExtensionMacro {
215
223
  // These base types already supply `AnyModule`; emitting a second conformance here
216
224
  // would be redundant and error.
217
225
  if let classDecl = declaration.as(ClassDeclSyntax.self),
218
- inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"]) {
226
+ inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
227
+ {
219
228
  return []
220
229
  }
221
230
  let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
@@ -232,7 +241,8 @@ private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String)
232
241
  }
233
242
  for binding in varDecl.bindings {
234
243
  if let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
235
- identifier.identifier.text == name {
244
+ identifier.identifier.text == name
245
+ {
236
246
  return true
237
247
  }
238
248
  }
@@ -240,14 +250,12 @@ private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String)
240
250
  return false
241
251
  }
242
252
 
243
- /**
244
- True if the class declares an initializer that satisfies the `init(appContext:)` protocol
245
- requirement: a single parameter labeled `appContext` whose type is written as `AppContext`
246
- (or `…​.AppContext`). Both must match — the label is part of the requirement, so
247
- `init(c: AppContext)` does *not* satisfy it (we still synthesize ours), and the type guards
248
- against a same-labeled init of an unrelated type. A macro can't resolve types, so this is a
249
- syntactic match on the spelled name.
250
- */
253
+ /// True if the class declares an initializer that satisfies the `init(appContext:)` protocol
254
+ /// requirement: a single parameter labeled `appContext` whose type is written as `AppContext`
255
+ /// (or `…​.AppContext`). Both must match — the label is part of the requirement, so
256
+ /// `init(c: AppContext)` does *not* satisfy it (we still synthesize ours), and the type guards
257
+ /// against a same-labeled init of an unrelated type. A macro can't resolve types, so this is a
258
+ /// syntactic match on the spelled name.
251
259
  private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
252
260
  for member in classDecl.memberBlock.members {
253
261
  guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else {
@@ -34,7 +34,8 @@ public struct OptimizedFunctionAttachedMacro: PeerMacro {
34
34
  ) throws -> [DeclSyntax] {
35
35
  guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
36
36
  throw MacroExpansionErrorMessage(
37
- "@OptimizedFunction can only be attached to function declarations")
37
+ "@OptimizedFunction can only be attached to function declarations"
38
+ )
38
39
  }
39
40
 
40
41
  let functionName = funcDecl.name.text
@@ -178,72 +178,13 @@ internal func isAnyType(_ type: some SyntaxProtocol) -> Bool {
178
178
  if let member = type.as(MemberTypeSyntax.self),
179
179
  member.name.text == "Any",
180
180
  member.genericArgumentClause == nil,
181
- let base = member.baseType.as(IdentifierTypeSyntax.self) {
181
+ let base = member.baseType.as(IdentifierTypeSyntax.self)
182
+ {
182
183
  return base.name.text == "Swift"
183
184
  }
184
185
  return false
185
186
  }
186
187
 
187
- /// The element type of `[T]` or `Array<T>`, or `nil` for any other type.
188
- private func arrayElementType(_ type: TypeSyntax) -> TypeSyntax? {
189
- if let array = type.as(ArrayTypeSyntax.self) {
190
- return array.element
191
- }
192
- guard let arguments = genericArguments(of: type, named: "Array"), arguments.count == 1 else {
193
- return nil
194
- }
195
- return arguments[0]
196
- }
197
-
198
- /// The value type of `[String: V]` or `Dictionary<String, V>`, or `nil` for any other type, including a
199
- /// dictionary with a different key type.
200
- private func stringKeyedDictionaryValueType(_ type: TypeSyntax) -> TypeSyntax? {
201
- if let dictionary = type.as(DictionaryTypeSyntax.self) {
202
- return isStringType(dictionary.key) ? dictionary.value : nil
203
- }
204
- guard let arguments = genericArguments(of: type, named: "Dictionary"), arguments.count == 2 else {
205
- return nil
206
- }
207
- return isStringType(arguments[0]) ? arguments[1] : nil
208
- }
209
-
210
- /// The type arguments of a generic type spelled `<name><…>` (or `Swift.<name><…>`), or `nil` when the
211
- /// type has another name or no type arguments.
212
- private func genericArguments(of type: TypeSyntax, named name: String) -> [TypeSyntax]? {
213
- let clause: GenericArgumentClauseSyntax?
214
- if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == name {
215
- clause = identifier.genericArgumentClause
216
- } else if let member = type.as(MemberTypeSyntax.self),
217
- member.name.text == name,
218
- member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift" {
219
- clause = member.genericArgumentClause
220
- } else {
221
- return nil
222
- }
223
- guard let clause else {
224
- return nil
225
- }
226
- var types: [TypeSyntax] = []
227
- for argument in clause.arguments {
228
- guard case .type(let type) = argument.argument else {
229
- return nil
230
- }
231
- types.append(type)
232
- }
233
- return types
234
- }
235
-
236
- /// True when the type is `String` or `Swift.String`.
237
- private func isStringType(_ type: TypeSyntax) -> Bool {
238
- if let identifier = type.as(IdentifierTypeSyntax.self) {
239
- return identifier.name.text == "String" && identifier.genericArgumentClause == nil
240
- }
241
- if let member = type.as(MemberTypeSyntax.self), member.name.text == "String" {
242
- return member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
243
- }
244
- return false
245
- }
246
-
247
188
  /// Walks a type looking for `Any` (or `Swift.Any`) anywhere inside it.
248
189
  private final class AnyTypeFinder: SyntaxVisitor {
249
190
  var found = false
@@ -23,7 +23,8 @@ internal struct JSConstructor {
23
23
  guard arguments.count == \(parameters.count) else {
24
24
  throw Exceptions.ArgumentsRangeMismatch((functionName: "\(typeName)", received: arguments.count, required: \(parameters.count), maximum: \(parameters.count)))
25
25
  }
26
- """)
26
+ """
27
+ )
27
28
 
28
29
  var callArguments: [String] = []
29
30
  for (index, parameter) in parameters.enumerated() {
@@ -32,7 +33,8 @@ internal struct JSConstructor {
32
33
  type: parameter.type,
33
34
  into: "arg\(index)",
34
35
  from: "arguments.unownedValue(at: \(index))"
35
- ))
36
+ )
37
+ )
36
38
 
37
39
  let label = parameter.firstName.text
38
40
  callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
@@ -40,7 +42,8 @@ internal struct JSConstructor {
40
42
 
41
43
  lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
42
44
 
43
- return lines
45
+ return
46
+ lines
44
47
  .flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
45
48
  .map { indent + $0 }
46
49
  .joined(separator: "\n")