expo-modules-macros 0.32.0 → 0.34.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 (24) hide show
  1. package/.github/workflows/macos.yml +16 -3
  2. package/AGENTS.md +7 -3
  3. package/Package.swift +10 -1
  4. package/README.md +3 -3
  5. package/apple/ExpoModulesMacros +0 -0
  6. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +124 -5
  7. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +2 -1
  8. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ResolveRefs.swift +76 -17
  9. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +15 -8
  10. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +206 -21
  11. package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +14 -6
  12. package/apple/Sources/ExpoModulesCLI/Core/Detection.swift +6 -0
  13. package/apple/Sources/ExpoModulesCLI/Core/DetectionVisitor.swift +41 -2
  14. package/apple/Sources/ExpoModulesCLI/Core/SourceScan.swift +3 -2
  15. package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +64 -30
  16. package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +174 -2
  17. package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +176 -46
  18. package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
  19. package/apple/Sources/ExpoModulesMacrosCore/Support/DecorateModuleBuilder.swift +17 -3
  20. package/apple/Sources/ExpoModulesMacrosCore/Support/JSConstructor.swift +11 -2
  21. package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +36 -5
  22. package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
  23. package/build/types.d.ts +58 -3
  24. package/package.json +5 -5
@@ -39,6 +39,10 @@ import SwiftSyntaxMacros
39
39
  /// event without an initial value an empty closure (`{}`, `{ _ in }`, ...) and leaves the value
40
40
  /// props at their initial values. It is never generated when the type declares its own `init()`.
41
41
  /// Like any initializer in the struct body, the generated one removes the memberwise initializer.
42
+ /// - `_resetViewProp(_:in:)`: sets one value prop back to its default, by assigning the property's
43
+ /// initial value or `nil`, so a reset evaluates only that prop's default. It is a static member of
44
+ /// the struct body, because an initial value can name a static member without `Self.` or a member of
45
+ /// an enclosing type, which an instance method or the extension can't.
42
46
  ///
43
47
  /// An event's function type must return `Void`, because JavaScript can't return a value to it.
44
48
  ///
@@ -47,7 +51,16 @@ import SwiftSyntaxMacros
47
51
  /// - `allProps`: every bit set, the changed set on the first application.
48
52
  /// - `propSet(for:)`: one name's bit, for folding raw changed keys into the mask.
49
53
  /// - `_decode(_:from:in:)`: assigns one value prop from a JavaScript value. `null` and `undefined`
50
- /// reset it to its value in `Self()`, and any other value decodes through `JavaScriptDecodable`.
54
+ /// reset it with `_resetViewProp(_:in:)`, and any other value decodes through
55
+ /// `JavaScriptDecodable`.
56
+ ///
57
+ /// `_decode` and `_resetViewProp` run on the JavaScript actor. The other generated types and members
58
+ /// and the conformance are `nonisolated`, so core can use them on any thread also when the module
59
+ /// builds with a default isolation, such as `-default-isolation MainActor`. The props' own
60
+ /// declarations still take that isolation, so a struct with an event prop, a value prop whose type
61
+ /// isn't `Sendable`, or a computed prop needs `nonisolated` on its declaration, and the author's
62
+ /// `init()` must be `nonisolated` too. So must a conformance that the author declares
63
+ /// (`: nonisolated AnyViewProps`).
51
64
  ///
52
65
  /// Author-facing shape — no conformance to spell out:
53
66
  ///
@@ -83,6 +96,9 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
83
96
  if let initializer = defaultInitializer(props: props) {
84
97
  members.append(initializer)
85
98
  }
99
+ if let reset = resetFunction(valueProps: props.valueProps) {
100
+ members.append(reset)
101
+ }
86
102
  return members
87
103
  }
88
104
 
@@ -120,29 +136,33 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
120
136
  ? "0"
121
137
  : "0b" + String(repeating: "1", count: props.valueProps.count)
122
138
 
123
- // With no value prop there's nothing to switch over, so return the empty set.
139
+ // With no value prop, `PropName` has no case, so the function can't be called and its body is
140
+ // empty, like the one of `_decode`. Swift reports any statement in it, also an empty `switch`, as
141
+ // "will never be executed". The body starts with a line break, so the empty one closes on the
142
+ // next line.
124
143
  let propSetBody: String
125
144
  if props.valueProps.isEmpty {
126
- propSetBody = " return []"
145
+ propSetBody = ""
127
146
  } else {
128
147
  let cases = props.valueProps
129
148
  .map { " case .\($0.name):\n return .\($0.name)" }
130
149
  .joined(separator: "\n")
131
- propSetBody = " switch name {\n\(cases)\n }"
150
+ propSetBody = "\n switch name {\n\(cases)\n }"
132
151
  }
133
152
 
134
153
  let decodeBody = decodeFunctionBody(valueProps: props.valueProps)
135
154
 
136
- let conformanceClause = alreadyConforms ? "" : ": \(viewPropsProtocolName)"
155
+ // `nonisolated` keeps the conformance and its members usable on the JavaScript thread when the
156
+ // module builds with a default isolation, which would otherwise isolate them to the main actor.
157
+ let conformanceClause = alreadyConforms ? "" : ": nonisolated \(viewPropsProtocolName)"
137
158
  let ext: DeclSyntax = """
138
159
  extension \(type.trimmed)\(raw: conformanceClause) {
139
- public static let allProps = PropSet(rawValue: \(raw: allPropsMask))
160
+ nonisolated public static let allProps = PropSet(rawValue: \(raw: allPropsMask))
140
161
 
141
162
  /// `@inlinable` so core's raw-key fold can inline the lookup across the module boundary:
142
163
  /// it runs once per changed key per props batch.
143
164
  @inlinable
144
- public static func propSet(for name: PropName) -> PropSet {
145
- \(raw: propSetBody)
165
+ nonisolated public static func propSet(for name: PropName) -> PropSet {\(raw: propSetBody)
146
166
  }
147
167
 
148
168
  @JavaScriptActor
@@ -488,9 +508,11 @@ private struct ViewProp {
488
508
  /// The property's declared type, verbatim, or the type of its literal initial value. `_decode`
489
509
  /// doesn't spell it: the decoded type is inferred from the assignment.
490
510
  let type: String
491
- /// Whether the prop is computed with a `mutating get`, which `_decode` can only read from a
511
+ /// Whether the prop is computed with a `mutating get`, which `_resetViewProp` can only read from a
492
512
  /// mutable value.
493
513
  let hasMutatingGetter: Bool
514
+ /// What `_resetViewProp` assigns when React removes the prop. Events aren't reset.
515
+ let defaultValue: PropDefault
494
516
 
495
517
  /// The name with any escaping backticks removed: the JS-visible key, so the enum's raw value and
496
518
  /// `_eventNames` both read `"default"`, never `` "`default`" ``.
@@ -499,6 +521,49 @@ private struct ViewProp {
499
521
  }
500
522
  }
501
523
 
524
+ /// The value that a value prop goes back to when React removes it. Each one equals the prop's value
525
+ /// in `Self()`.
526
+ private enum PropDefault {
527
+ /// The initial value of a stored property, as written. Evaluating it again gives the value that
528
+ /// `Self()` gives the property.
529
+ case initialValue(ExprSyntax)
530
+ /// The implicit `nil` of a `var` whose type is written as `T?` or `T!`.
531
+ case implicitNil
532
+ /// The prop's value in a new `Self()`, when the macro can't tell it from the declaration: the
533
+ /// struct declares its own `init()`, which can set any value, the prop is computed, the property
534
+ /// has an attribute, such as a property wrapper, whose value after `init(wrappedValue:)` can differ
535
+ /// from its initial value, or the initial value has a `#` expression, such as `#function` or
536
+ /// `#line`, whose value depends on where it's evaluated.
537
+ case newInstance
538
+ }
539
+
540
+ /// The default of a property with the given binding, for a struct that does or doesn't declare its own
541
+ /// `init()`.
542
+ private func propDefault(
543
+ of binding: PatternBindingSyntax,
544
+ in varDecl: VariableDeclSyntax,
545
+ declaredType: TypeSyntax?,
546
+ declaresUsableInitializer: Bool
547
+ ) -> PropDefault {
548
+ if declaresUsableInitializer || !isStoredBinding(binding) || !varDecl.attributes.isEmpty {
549
+ return .newInstance
550
+ }
551
+ if let initializer = binding.initializer {
552
+ // A macro expansion, also a literal such as `#function`, can depend on where it's evaluated, so
553
+ // a copy in `_resetViewProp` could give another value.
554
+ let hasPoundExpression = initializer.value.tokens(viewMode: .sourceAccurate).contains {
555
+ $0.tokenKind == .pound
556
+ }
557
+ return hasPoundExpression ? .newInstance : .initialValue(initializer.value)
558
+ }
559
+ if varDecl.bindingSpecifier.tokenKind == .keyword(.var), let declaredType, hasImplicitNilDefault(declaredType) {
560
+ return .implicitNil
561
+ }
562
+ // A value prop without a default is reported, so this is only reached for an event, which isn't
563
+ // reset.
564
+ return .newInstance
565
+ }
566
+
502
567
  /// An event prop declared without an initial value, which the generated `init()` assigns.
503
568
  private struct EventDefault {
504
569
  /// The property name as written, backticks included.
@@ -664,7 +729,17 @@ private func validatedViewProps(
664
729
  // Only stored storage takes an initial value; a computed prop with a setter needs none.
665
730
  let needsInitialValue =
666
731
  binding.initializer == nil && isStoredBinding(binding) && !declaresUsableInitializer
667
- let prop = ViewProp(name: name, type: resolvedType, hasMutatingGetter: hasMutatingGetter(binding))
732
+ let prop = ViewProp(
733
+ name: name,
734
+ type: resolvedType,
735
+ hasMutatingGetter: hasMutatingGetter(binding),
736
+ defaultValue: propDefault(
737
+ of: binding,
738
+ in: varDecl,
739
+ declaredType: declaredType,
740
+ declaresUsableInitializer: declaresUsableInitializer
741
+ )
742
+ )
668
743
  if let eventFunctionType {
669
744
  // An event is dispatched to JavaScript, which can't hand a value back to the caller.
670
745
  if !returnsVoid(eventFunctionType) {
@@ -861,17 +936,6 @@ private func isVoidType(_ type: TypeSyntax) -> Bool {
861
936
  return ["Void", "Swift.Void"].contains(type.trimmedDescription)
862
937
  }
863
938
 
864
- /// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
865
- /// label would make it a tuple, which Swift rejects with a single element anyway.
866
- private func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
867
- guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
868
- only.firstName == nil
869
- else {
870
- return type
871
- }
872
- return withoutParentheses(only.type)
873
- }
874
-
875
939
  /// True if the binding is computed with a `mutating get`.
876
940
  private func hasMutatingGetter(_ binding: PatternBindingSyntax) -> Bool {
877
941
  guard case .accessors(let accessors) = binding.accessorBlock?.accessors else {
@@ -937,6 +1001,10 @@ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax?
937
1001
  /// Emitted even when empty, so a props type with only events still conforms. An enum with no cases
938
1002
  /// can't declare a raw type, so the empty one conforms to `RawRepresentable` by hand, with `String`
939
1003
  /// as its `RawValue` like the others.
1004
+ ///
1005
+ /// The enum is `@frozen`, so the `switch` in the `@inlinable` `propSet(for:)` compiles in a module
1006
+ /// built with library evolution, where it would otherwise need a case for unknown values. The cases
1007
+ /// and their order are already part of the ABI, as the bits of `PropSet`.
940
1008
  private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
941
1009
  // An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
942
1010
  // the wire key visible in the generated source and independent of that implicit rule.
@@ -950,7 +1018,7 @@ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
950
1018
  .joined(separator: "\n")
951
1019
  if valueProps.isEmpty {
952
1020
  return """
953
- public enum PropName: RawRepresentable, CaseIterable {
1021
+ @frozen nonisolated public enum PropName: RawRepresentable, CaseIterable {
954
1022
  public init?(rawValue: String) {
955
1023
  return nil
956
1024
  }
@@ -963,7 +1031,7 @@ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
963
1031
  """
964
1032
  }
965
1033
  return """
966
- public enum PropName: String, CaseIterable {
1034
+ @frozen nonisolated public enum PropName: String, CaseIterable {
967
1035
  \(raw: cases)
968
1036
  }
969
1037
  """
@@ -981,7 +1049,7 @@ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
981
1049
 
982
1050
  if valueProps.isEmpty {
983
1051
  return """
984
- public struct PropSet: OptionSet, Sendable {
1052
+ nonisolated public struct PropSet: OptionSet, Sendable {
985
1053
  public let rawValue: UInt64
986
1054
 
987
1055
  public init(rawValue: UInt64) {
@@ -991,7 +1059,7 @@ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
991
1059
  """
992
1060
  }
993
1061
  return """
994
- public struct PropSet: OptionSet, Sendable {
1062
+ nonisolated public struct PropSet: OptionSet, Sendable {
995
1063
  public let rawValue: UInt64
996
1064
 
997
1065
  public init(rawValue: UInt64) {
@@ -1009,31 +1077,23 @@ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
1009
1077
  private func eventNamesConstant(eventProps: [ViewProp]) -> DeclSyntax {
1010
1078
  let names = eventProps.map { "\"\($0.wireName)\"" }.joined(separator: ", ")
1011
1079
  return """
1012
- public static let _eventNames: [String] = [\(raw: names)]
1080
+ nonisolated public static let _eventNames: [String] = [\(raw: names)]
1013
1081
  """
1014
1082
  }
1015
1083
 
1016
1084
  /// The body of `_decode`, which core calls once per changed prop. React sends a removed prop as
1017
- /// `null`, so `null` and `undefined` reset the prop to its value in a fresh `Self()`; any other value
1018
- /// decodes through `JavaScriptDecodable`, with the type inferred from the assignment. The helper is a
1019
- /// static member of `AnyViewProps`, reached through `Self.`, so a prop named `_decodeViewProp`
1020
- /// doesn't shadow it. The checks run
1021
- /// once, before the switch, so each case is a single assignment. Members are reached through `self.`
1022
- /// so that a prop named like a parameter (`value`, `runtime`) isn't shadowed by it.
1085
+ /// `null`, so `null` and `undefined` reset the prop with `_resetViewProp`; any other value decodes
1086
+ /// through `JavaScriptDecodable`, with the type inferred from the assignment. `_decodeViewProp` is a
1087
+ /// static member of `AnyViewProps`, reached through `Self.`, so a prop with the same name doesn't
1088
+ /// shadow it. The checks run once, before the switch, so each case is a single assignment. Members
1089
+ /// are reached through `self.` so that a prop named like a parameter (`value`, `runtime`) isn't
1090
+ /// shadowed by it.
1023
1091
  ///
1024
1092
  /// With no value prop the body is empty: `PropName` is uninhabited, so the function is never called.
1025
- ///
1026
- /// `defaults` is a `var` only when a prop has a `mutating get`, which can't be read from a constant.
1027
- /// Otherwise it's a `let`, so the common case gets no "never mutated" warning.
1028
1093
  private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
1029
1094
  if valueProps.isEmpty {
1030
1095
  return ""
1031
1096
  }
1032
- let defaultsSpecifier = valueProps.contains(where: \.hasMutatingGetter) ? "var" : "let"
1033
- let resetCases =
1034
- valueProps
1035
- .map { " case .\($0.name):\n self.\($0.name) = defaults.\($0.name)" }
1036
- .joined(separator: "\n")
1037
1097
  let decodeCases =
1038
1098
  valueProps
1039
1099
  .map { " case .\($0.name):\n self.\($0.name) = try Self._decodeViewProp(value, in: runtime)" }
@@ -1043,10 +1103,7 @@ private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
1043
1103
  return """
1044
1104
 
1045
1105
  if value.isNull() || value.isUndefined() {
1046
- \(defaultsSpecifier) defaults = Self()
1047
- switch name {
1048
- \(resetCases)
1049
- }
1106
+ Self._resetViewProp(name, in: &self)
1050
1107
  return
1051
1108
  }
1052
1109
  switch name {
@@ -1055,6 +1112,79 @@ private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
1055
1112
  """
1056
1113
  }
1057
1114
 
1115
+ /// `_resetViewProp(_:in:)`, which sets one value prop back to its default when React removes it. Each
1116
+ /// case assigns the prop's initial value or `nil`, so a reset evaluates only that prop's default. A
1117
+ /// prop whose default the macro can't tell from the declaration reads it from a new `Self()`.
1118
+ ///
1119
+ /// A static function of the struct body resolves names as a property initializer does: a static
1120
+ /// member without `Self.` and a member of an enclosing type, which `_decode` in the extension can't
1121
+ /// name. The parameters are `_name` and `_props`, with a number after the name when an initial value
1122
+ /// uses it, so they don't hide a declaration that an initial value names.
1123
+ ///
1124
+ /// A default argument such as `#function` or `#line` of a function that an initial value calls gets
1125
+ /// the location of `_resetViewProp`, which the macro can't see.
1126
+ ///
1127
+ /// `nil` with no value prop: `PropName` is uninhabited, so `_decode` never resets.
1128
+ private func resetFunction(valueProps: [ViewProp]) -> DeclSyntax? {
1129
+ if valueProps.isEmpty {
1130
+ return nil
1131
+ }
1132
+ let usedNames = Set(
1133
+ valueProps.flatMap { prop -> [String] in
1134
+ guard case .initialValue(let expression) = prop.defaultValue else {
1135
+ return []
1136
+ }
1137
+ return expression.tokens(viewMode: .sourceAccurate).compactMap { token in
1138
+ guard case .identifier(let text) = token.tokenKind else {
1139
+ return nil
1140
+ }
1141
+ return withoutBackticks(text)
1142
+ }
1143
+ }
1144
+ )
1145
+ let nameParameter = unusedName("_name", in: usedNames)
1146
+ let propsParameter = unusedName("_props", in: usedNames)
1147
+ let cases =
1148
+ valueProps
1149
+ .map { prop -> String in
1150
+ let assignment: String
1151
+ switch prop.defaultValue {
1152
+ case .initialValue(let expression):
1153
+ assignment = "\(propsParameter).\(prop.name) = \(expression.trimmedDescription)"
1154
+ case .implicitNil:
1155
+ assignment = "\(propsParameter).\(prop.name) = nil"
1156
+ case .newInstance where prop.hasMutatingGetter:
1157
+ // A mutating getter can't be read from a constant. The local is scoped to its case, where
1158
+ // no initial value is copied.
1159
+ assignment = "var _defaults = Self()\n \(propsParameter).\(prop.name) = _defaults.\(prop.name)"
1160
+ case .newInstance:
1161
+ assignment = "\(propsParameter).\(prop.name) = Self().\(prop.name)"
1162
+ }
1163
+ return " case .\(prop.name):\n \(assignment)"
1164
+ }
1165
+ .joined(separator: "\n")
1166
+ return """
1167
+ @JavaScriptActor
1168
+ static func _resetViewProp(_ \(raw: nameParameter): PropName, in \(raw: propsParameter): inout Self) {
1169
+ switch \(raw: nameParameter) {
1170
+ \(raw: cases)
1171
+ }
1172
+ }
1173
+ """
1174
+ }
1175
+
1176
+ /// `base`, or `base` with the lowest number after it that gives a name not in `names`.
1177
+ private func unusedName(_ base: String, in names: Set<String>) -> String {
1178
+ if !names.contains(base) {
1179
+ return base
1180
+ }
1181
+ var number = 1
1182
+ while names.contains("\(base)\(number)") {
1183
+ number += 1
1184
+ }
1185
+ return "\(base)\(number)"
1186
+ }
1187
+
1058
1188
  /// The public `init()` that `AnyViewProps` requires, when Swift's own can't satisfy it: it gives each
1059
1189
  /// event prop without an initial value an empty closure, and leaves the value props at their initial
1060
1190
  /// values. `nil` when the type declares a usable `init()`, or when Swift's `init()` is enough (see
@@ -1065,7 +1195,7 @@ private func defaultInitializer(props: ViewPropsModel) -> DeclSyntax? {
1065
1195
  }
1066
1196
  if props.eventDefaults.isEmpty {
1067
1197
  return """
1068
- public init() {
1198
+ nonisolated public init() {
1069
1199
  }
1070
1200
  """
1071
1201
  }
@@ -1073,7 +1203,7 @@ private func defaultInitializer(props: ViewPropsModel) -> DeclSyntax? {
1073
1203
  .map { " self.\($0.name) = \($0.emptyClosure)" }
1074
1204
  .joined(separator: "\n")
1075
1205
  return """
1076
- public init() {
1206
+ nonisolated public init() {
1077
1207
  \(raw: assignments)
1078
1208
  }
1079
1209
  """
@@ -25,7 +25,7 @@ public struct ProvidedMacro {
25
25
  conformances: ["JavaScriptDecodable", "JavaScriptEncodable"]
26
26
  ),
27
27
  ProvidedMacro(name: "ViewProps", type: ViewPropsMacro.self, conformances: ["AnyViewProps"]),
28
- ProvidedMacro(name: "ExpoView", type: ExpoViewMacro.self),
28
+ ProvidedMacro(name: "ExpoView", type: ExpoViewMacro.self, conformances: ["AnyNativeView"]),
29
29
  ]
30
30
  }
31
31
  }
@@ -17,10 +17,17 @@ import SwiftSyntax
17
17
  /// A synchronous function marked `@JS(.withAsync("name"))` also binds a promise-returning variant
18
18
  /// under that name (see `asyncVariant`). It decodes and encodes like an `async` function, and calls
19
19
  /// the function on the concurrent pool.
20
+ ///
21
+ /// A synchronous function can take the JavaScript runtime as its last parameter,
22
+ /// `in runtime: borrowing JavaScriptRuntime`. JavaScript doesn't see it: the binding passes the runtime
23
+ /// of the call.
20
24
  internal struct JSFunction {
21
25
  let swiftName: String
22
26
  private(set) var jsName: String
27
+ /// The parameters that take JavaScript arguments, without the runtime parameter.
23
28
  let parameters: [FunctionParameterSyntax]
29
+ /// The label of the parameter that takes the JavaScript runtime, or `nil` when the function has none.
30
+ private let runtimeLabel: String?
24
31
  /// The declared return type as written, or `nil` when the function returns `Void`/nothing.
25
32
  let returnType: String?
26
33
  /// How the return value encodes when its type mentions `Any`, or `nil` for any other type.
@@ -39,7 +46,9 @@ internal struct JSFunction {
39
46
  self.swiftName = funcDecl.name.text
40
47
  self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
41
48
  self.nameToken = funcDecl.name
42
- self.parameters = Array(funcDecl.signature.parameterClause.parameters)
49
+ let declaredParameters = funcDecl.signature.parameterClause.parameters
50
+ self.parameters = declaredParameters.filter { !isRuntimeParameter($0) }
51
+ self.runtimeLabel = declaredParameters.first(where: isRuntimeParameter)?.firstName.text
43
52
 
44
53
  let declaredReturnType = funcDecl.signature.returnClause?.type
45
54
  self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
@@ -264,6 +273,9 @@ internal struct JSFunction {
264
273
  }
265
274
  callArguments.append(label == "_" ? value : "\(label): \(value)")
266
275
  }
276
+ if let runtimeLabel {
277
+ callArguments.append(runtimeLabel == "_" ? "runtime" : "\(runtimeLabel): runtime")
278
+ }
267
279
  return callArguments.joined(separator: ", ")
268
280
  }
269
281
 
@@ -338,12 +350,14 @@ internal struct JSFunction {
338
350
  // returns nothing binds through the `Void` overload (`JavaScriptRuntime.VoidFunctionClosure`)
339
351
  // instead, which leaves JS's result as `undefined`. A call through it is faster than through a
340
352
  // result-slot closure that writes nothing. The closure declares `-> Void` because a body of several
341
- // statements without a `return` is otherwise ambiguous between the overloads.
353
+ // statements without a `return` is otherwise ambiguous between the overloads. A function that takes
354
+ // the runtime binds through the result-slot overload even when it returns nothing, so it gets the
355
+ // runtime as the closure's parameter instead of capturing the decorator's.
342
356
  let captures = receiver.captureClause
343
357
  let parameters =
344
358
  isAsync
345
359
  ? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
346
- : returnType == nil
360
+ : returnType == nil && runtimeLabel == nil
347
361
  ? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) -> Void"
348
362
  : resultSlotClosureParameters
349
363
 
@@ -4,12 +4,18 @@ import SwiftSyntax
4
4
  /// have a single constructor). Instead of a `Constructor { … }` DSL entry, the macro synthesizes an
5
5
  /// override of `SharedObject._constructSharedObject(...)` that decodes the JS arguments and returns a
6
6
  /// fresh instance; unlike the method/property bindings it produces the native instance rather than
7
- /// recovering one.
7
+ /// recovering one. Like a function, it can take the JavaScript runtime as its last parameter.
8
8
  internal struct JSConstructor {
9
+ /// The parameters that take JavaScript arguments, without the runtime parameter.
9
10
  let parameters: [FunctionParameterSyntax]
11
+ /// The label of the parameter that takes the JavaScript runtime, or `nil` when the initializer has
12
+ /// none.
13
+ private let runtimeLabel: String?
10
14
 
11
15
  init(initDecl: InitializerDeclSyntax) {
12
- self.parameters = Array(initDecl.signature.parameterClause.parameters)
16
+ let declaredParameters = initDecl.signature.parameterClause.parameters
17
+ self.parameters = declaredParameters.filter { !isRuntimeParameter($0) }
18
+ self.runtimeLabel = declaredParameters.first(where: isRuntimeParameter)?.firstName.text
13
19
  }
14
20
 
15
21
  /// The body statements, indented with `indent`: arity guard, per-argument decode through
@@ -39,6 +45,9 @@ internal struct JSConstructor {
39
45
  let label = parameter.firstName.text
40
46
  callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
41
47
  }
48
+ if let runtimeLabel {
49
+ callArguments.append(runtimeLabel == "_" ? "runtime" : "\(runtimeLabel): runtime")
50
+ }
42
51
 
43
52
  lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
44
53
 
@@ -111,9 +111,11 @@ internal func hasAsyncVariant(_ decl: DeclSyntaxProtocol) -> Bool {
111
111
  return asyncVariantOption(of: attribute) != nil
112
112
  }
113
113
 
114
- /// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
115
- /// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
114
+ /// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`, also in
115
+ /// parentheses (`(T?)`), which Swift treats as the same type. Used to decide argument requiredness (an
116
+ /// optional parameter may be omitted) and record-field nullability.
116
117
  internal func isOptionalType(_ type: TypeSyntax) -> Bool {
118
+ let type = withoutParentheses(type)
117
119
  if let attributed = type.as(AttributedTypeSyntax.self) {
118
120
  return isOptionalType(attributed.baseType)
119
121
  }
@@ -126,6 +128,17 @@ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
126
128
  return false
127
129
  }
128
130
 
131
+ /// True when the parameter takes the JavaScript runtime instead of a JavaScript argument: its type,
132
+ /// without specifiers, is written as `JavaScriptRuntime` (or `….JavaScriptRuntime`), or as an optional
133
+ /// of it. The binding passes the runtime of the call, and JavaScript doesn't see the parameter. `@JS`
134
+ /// requires it to be the last parameter, labeled `in` or `runtime`, `borrowing` and not optional (see
135
+ /// `diagnoseRuntimeParameter`). A macro can't resolve types, so a typealias or a subclass of
136
+ /// `JavaScriptRuntime` isn't detected.
137
+ internal func isRuntimeParameter(_ parameter: FunctionParameterSyntax) -> Bool {
138
+ let type = SpecifiedType(parameter.type).type
139
+ return baseIdentifier(of: optionalWrappedType(type) ?? type) == "JavaScriptRuntime"
140
+ }
141
+
129
142
  /// A parameter's type split from the specifiers written in front of it (`borrowing`, `consuming`,
130
143
  /// `sending`, `inout`, `isolated`, ...). The specifiers aren't part of the type: the decode call and
131
144
  /// the conformance assertion need the bare type (`borrowing Point.decode(...)` isn't valid Swift), and
@@ -159,6 +172,17 @@ internal struct SpecifiedType {
159
172
  /// a fresh decoded value satisfies either way.
160
173
  internal let ownershipSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
161
174
 
175
+ /// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
176
+ /// label would make it a tuple, which Swift rejects with a single element anyway.
177
+ internal func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
178
+ guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
179
+ only.firstName == nil
180
+ else {
181
+ return type
182
+ }
183
+ return withoutParentheses(only.type)
184
+ }
185
+
162
186
  /// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
163
187
  /// optional.
164
188
  internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
@@ -607,8 +631,9 @@ internal func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool
607
631
 
608
632
  /// True if the type's inheritance clause already lists a protocol with the given name. Matches
609
633
  /// either the bare identifier (`Record`) or a qualified member access ending in the name
610
- /// (`ExpoModulesCore.Record`). Used by the extension macros to skip a conformance the author already
611
- /// spelled out. Works for `struct`, `class`, and `enum` declarations; any other declaration kind has no
634
+ /// (`ExpoModulesCore.Record`), also with `nonisolated` or an attribute such as `@preconcurrency`
635
+ /// before it. Used by the extension macros to skip a conformance the author already spelled out.
636
+ /// Works for `struct`, `class`, and `enum` declarations; any other declaration kind has no
612
637
  /// inheritance clause to read and reports `false`.
613
638
  internal func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
614
639
  let inheritanceClause: InheritanceClauseSyntax?
@@ -624,5 +649,11 @@ internal func inheritsProtocol(named name: String, in declaration: some DeclGrou
624
649
  guard let inherited = inheritanceClause?.inheritedTypes else {
625
650
  return false
626
651
  }
627
- return inherited.contains { baseIdentifier(of: $0.type) == name }
652
+ return inherited.contains { entry in
653
+ var type = entry.type
654
+ while let attributed = type.as(AttributedTypeSyntax.self) {
655
+ type = attributed.baseType
656
+ }
657
+ return baseIdentifier(of: type) == name
658
+ }
628
659
  }
@@ -0,0 +1 @@
1
+ // Empty on purpose: this target only carries the dependency on the macro target (see Package.swift).
package/build/types.d.ts CHANGED
@@ -368,6 +368,58 @@ export interface ExportedUnion extends ExportedAccess, ExportedAvailability {
368
368
  members: ExportedUnionMember[];
369
369
  file: string;
370
370
  }
371
+ /**
372
+ * One value prop of a `@ViewProps` struct: a stored property that isn't function-typed. It has no
373
+ * `required`: native code resets a prop that React leaves out to its default, so JS can always omit
374
+ * one.
375
+ */
376
+ export interface ExportedViewProp extends ExportedAccess, ExportedAvailability {
377
+ name: string;
378
+ type: TypeNode;
379
+ /** Optional-typed: true exactly when `type` is an `optional` node. */
380
+ optional: boolean;
381
+ /** The initial value as Swift source text, absent when none is written. */
382
+ defaultValue?: string;
383
+ }
384
+ /**
385
+ * One event of a `@ViewProps` struct: a function-typed stored property. Its name is the React prop
386
+ * name as written, with no `on` prefix stripped, so it has no `jsName`.
387
+ */
388
+ export interface ExportedViewEvent extends ExportedAccess, ExportedAvailability {
389
+ /** The Swift property name, e.g. `onTap`. */
390
+ name: string;
391
+ /**
392
+ * The type of the function's single parameter, absent when it takes none (`() -> Void`) or more
393
+ * than one.
394
+ */
395
+ payload?: TypeNode;
396
+ }
397
+ /**
398
+ * An `@ExpoView` class with the props and events of the `@ViewProps` struct in its generic argument.
399
+ * It is reported on its own rather than under a module.
400
+ */
401
+ export interface ExportedView extends ExportedAccess, ExportedAvailability {
402
+ /** The Swift class name. Only a view declared at file scope is reported. */
403
+ name: string;
404
+ /** The name the view registers under. `@ExpoView` takes no name, so it is always the class name. */
405
+ jsName: string;
406
+ /**
407
+ * The props type: the generic argument of `@ExpoView<CardProps>`, as written without backticks
408
+ * (`ExternalProps<Int>`), or the qualified name of the scanned `@ViewProps` struct it names
409
+ * (`CardView.Props`).
410
+ */
411
+ propsType: string;
412
+ /**
413
+ * The value props of the `@ViewProps` struct, in declaration order. Absent when the scan declares
414
+ * no struct named `propsType` on a platform of the view, so unknown props differ from no props. A
415
+ * struct declared in several `#if` branches contributes the props of each declaration whose
416
+ * platforms overlap the view's, and each prop keeps its own `platforms`.
417
+ */
418
+ props?: ExportedViewProp[];
419
+ /** The events of the `@ViewProps` struct, in declaration order. Absent when `props` is absent. */
420
+ events?: ExportedViewEvent[];
421
+ file: string;
422
+ }
371
423
  /** The exported types grouped by kind. */
372
424
  export interface ExportedSurface {
373
425
  modules: ExportedModule[];
@@ -375,6 +427,7 @@ export interface ExportedSurface {
375
427
  records: ExportedRecord[];
376
428
  enums: ExportedEnum[];
377
429
  unions: ExportedUnion[];
430
+ views: ExportedView[];
378
431
  }
379
432
  /** A `#if` condition the scan couldn't answer statically. */
380
433
  export interface ScanWarning {
@@ -392,14 +445,14 @@ export interface ScanStats {
392
445
  durationMs: number;
393
446
  }
394
447
  /**
395
- * One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, or a `@SharedObject`
396
- * class in `sharedObjects`.
448
+ * One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, a `@SharedObject`
449
+ * class in `sharedObjects`, or an `@ExpoView` class in `views`.
397
450
  */
398
451
  export interface ScannedClass {
399
452
  name: string;
400
453
  /**
401
454
  * The resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override, else the class
402
- * name.
455
+ * name. A view's JS name is its class name.
403
456
  */
404
457
  jsName: string;
405
458
  /**
@@ -422,6 +475,8 @@ export interface ScanModulesResult {
422
475
  modules: ScannedClass[];
423
476
  /** The top-level `@SharedObject` classes, with the same platform rules as `modules`. */
424
477
  sharedObjects: ScannedClass[];
478
+ /** The top-level `@ExpoView` classes, with the same platform rules as `modules`. */
479
+ views: ScannedClass[];
425
480
  warnings: ScanWarning[];
426
481
  stats: ScanStats;
427
482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-macros",
3
- "version": "0.32.0",
3
+ "version": "0.34.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.32.0",
30
- "@expo/modules-macros-linux-x64": "0.32.0",
31
- "@expo/modules-macros-win32-arm64": "0.32.0",
32
- "@expo/modules-macros-win32-x64": "0.32.0"
29
+ "@expo/modules-macros-linux-arm64": "0.34.0",
30
+ "@expo/modules-macros-linux-x64": "0.34.0",
31
+ "@expo/modules-macros-win32-arm64": "0.34.0",
32
+ "@expo/modules-macros-win32-x64": "0.34.0"
33
33
  }
34
34
  }