expo-modules-macros 0.27.1 → 0.29.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 (44) hide show
  1. package/.github/workflows/linux.yml +3 -7
  2. package/.github/workflows/publish.yml +2 -3
  3. package/.github/workflows/swift.yml +7 -13
  4. package/.github/workflows/windows.yml +3 -10
  5. package/AGENTS.md +10 -7
  6. package/Package.swift +110 -0
  7. package/README.md +31 -8
  8. package/apple/ExpoModulesMacros +0 -0
  9. package/apple/Sources/ExpoModulesCLI/CLI.swift +1 -1
  10. package/apple/Sources/{ExpoModulesMacros → ExpoModulesCLI/Commands}/Expand/MacroExpander.swift +1 -1
  11. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +21 -4
  12. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +96 -0
  13. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +3 -2
  14. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +42 -11
  15. package/apple/Sources/ExpoModulesMacros/main.swift +6 -0
  16. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModuleMacro.swift +12 -6
  17. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/RecordMacro.swift +29 -71
  18. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/SharedObjectMacro.swift +30 -3
  19. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/UnionMacro.swift +6 -4
  20. package/apple/Sources/ExpoModulesMacrosCore/PluginServer.swift +40 -0
  21. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/ProvidedMacros.swift +7 -7
  22. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/DecorateModuleBuilder.swift +44 -9
  23. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSConstructor.swift +15 -2
  24. package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/MacroHelpers.swift +2 -3
  25. package/apple/Sources/ExpoModulesMacrosCore/Support/SendableConformance.swift +36 -0
  26. package/apple/Sources/ExpoModulesMacrosTool/main.swift +27 -0
  27. package/build/types.d.ts +11 -3
  28. package/package.json +5 -5
  29. package/apple/Package.swift +0 -66
  30. package/apple/Sources/ExpoModulesMacros/Plugin.swift +0 -36
  31. /package/{apple/Package.resolved → Package.resolved} +0 -0
  32. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/EventMacro.swift +0 -0
  33. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModulesOptimizedMacro.swift +0 -0
  34. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoViewMacro.swift +0 -0
  35. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/JSMacro.swift +0 -0
  36. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ViewPropsMacro.swift +0 -0
  37. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/ClosureArguments.swift +0 -0
  38. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/FreeFormTypes.swift +0 -0
  39. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSNameCollisions.swift +0 -0
  40. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/OptimizedFunctionHelpers.swift +0 -0
  41. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/Receiver.swift +0 -0
  42. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TupleTypes.swift +0 -0
  43. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TypeAliases.swift +0 -0
  44. /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TypeConformanceAssertion.swift +0 -0
@@ -183,7 +183,9 @@ final class SurfaceVisitor: SyntaxVisitor {
183
183
  events: events,
184
184
  accessLevel: accessLevel,
185
185
  platforms: platforms,
186
- file: file
186
+ file: file,
187
+ superclass: sharedObjectSuperclass(of: node),
188
+ ownNativeRefType: declaredNativeRefType(in: members)
187
189
  )
188
190
  )
189
191
  return
@@ -759,21 +761,50 @@ private func sharedRefSuperclass(of node: some DeclGroupSyntax) -> ExportedShare
759
761
  else {
760
762
  return nil
761
763
  }
762
- return ExportedSharedRef(native: native, nativeRefType: nativeRefType(in: node.memberBlock.members))
764
+ return ExportedSharedRef(
765
+ native: native,
766
+ nativeRefType: declaredNativeRefType(in: node.memberBlock.members) ?? "unknown"
767
+ )
768
+ }
769
+
770
+ /// The superclass a `@SharedObject` class names, without generic arguments (`Outer.ImageRef`), or `nil`
771
+ /// when it names none, `SharedObject`, or `SharedRef`. The first entry of the inheritance clause is read,
772
+ /// since Swift requires the superclass to come first. It may also be a protocol, which then names no
773
+ /// scanned shared object.
774
+ private func sharedObjectSuperclass(of node: some DeclGroupSyntax) -> String? {
775
+ guard let type = node.inheritanceClause?.inheritedTypes.first?.type,
776
+ let spelling = spellingWithoutGenericArguments(of: type)
777
+ else {
778
+ return nil
779
+ }
780
+ let name = spelling.split(separator: ".").last.map(String.init)
781
+ return name == "SharedObject" || name == "SharedRef" ? nil : spelling
782
+ }
783
+
784
+ /// A simple or qualified type name as written, without backticks or generic arguments, or `nil` for any
785
+ /// other kind of type.
786
+ private func spellingWithoutGenericArguments(of type: TypeSyntax) -> String? {
787
+ if let identifier = type.as(IdentifierTypeSyntax.self) {
788
+ return identifier.name.text.filter { $0 != "`" }
789
+ }
790
+ if let member = type.as(MemberTypeSyntax.self), let base = spellingWithoutGenericArguments(of: member.baseType) {
791
+ return base + "." + member.name.text.filter { $0 != "`" }
792
+ }
793
+ return nil
763
794
  }
764
795
 
765
- /// The value of a `nativeRefType` override that returns a plain string literal: `"image"` in
766
- /// `override var nativeRefType: String { "image" }`, also with `return` or inside a `get` block.
767
- /// `unknown`, core's default, when the class declares no `nativeRefType`, and `nil` when its getter is
768
- /// anything else.
769
- private func nativeRefType(in members: MemberBlockItemListSyntax) -> String? {
796
+ /// The value of the class's `nativeRefType` override when it returns a plain string literal: `"image"`
797
+ /// in `override var nativeRefType: String { "image" }`, also with `return` or inside a `get` block.
798
+ /// `.none` when the class declares no `nativeRefType` (core's default is `unknown`), and `.some(nil)`
799
+ /// when its getter is anything else.
800
+ private func declaredNativeRefType(in members: MemberBlockItemListSyntax) -> String?? {
770
801
  let binding = members.lazy
771
802
  .compactMap { $0.decl.as(VariableDeclSyntax.self) }
772
803
  .filter { !isTypeLevel($0.modifiers) }
773
804
  .flatMap { $0.bindings }
774
805
  .first { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "nativeRefType" }
775
806
  guard let binding else {
776
- return "unknown"
807
+ return .none
777
808
  }
778
809
  let statements: CodeBlockItemListSyntax?
779
810
  switch binding.accessorBlock?.accessors {
@@ -785,18 +816,18 @@ private func nativeRefType(in members: MemberBlockItemListSyntax) -> String? {
785
816
  statements = nil
786
817
  }
787
818
  guard let statements, statements.count == 1, let item = statements.first?.item else {
788
- return nil
819
+ return .some(nil)
789
820
  }
790
821
  let expression = item.as(ReturnStmtSyntax.self)?.expression ?? item.as(ExprSyntax.self)
791
822
  guard let literal = expression?.as(StringLiteralExprSyntax.self),
792
823
  literal.segments.count == 1,
793
824
  let segment = literal.segments.first?.as(StringSegmentSyntax.self)
794
825
  else {
795
- return nil
826
+ return .some(nil)
796
827
  }
797
828
  // An escape would need real unescaping, so it's treated as unreadable, as in `writtenRawValue(of:)`.
798
829
  let content = segment.content.text
799
- return content.contains("\\") ? nil : content
830
+ return .some(content.contains("\\") ? nil : content)
800
831
  }
801
832
 
802
833
  private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
@@ -0,0 +1,6 @@
1
+ import ExpoModulesMacrosCore
2
+
3
+ // The plugin executable that SwiftPM builds for the packages that depend on the
4
+ // `ExpoModulesMacrosPlugin` product. It only serves the macros, so it doesn't link the CLI; the
5
+ // binary that this package ships is `ExpoModulesMacrosTool`, which does both.
6
+ try runPluginServer()
@@ -13,7 +13,8 @@ import SwiftSyntaxMacros
13
13
  /// synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
14
14
  /// - `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
15
15
  /// `@ModuleDefinitionBuilder` on a `definition()` method.
16
- /// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
16
+ /// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it, and an
17
+ /// `@unchecked Sendable` conformance when the class isn't `Sendable` yet.
17
18
  ///
18
19
  /// Inheriting from `Module` is therefore optional — a class can carry any superclass (or
19
20
  /// none) and still be a module, because everything inheritance used to provide is synthesized.
@@ -219,15 +220,20 @@ extension ExpoModuleMacro: ExtensionMacro {
219
220
  guard !protocols.isEmpty else {
220
221
  return []
221
222
  }
223
+ var extensions: [ExtensionDeclSyntax] = []
222
224
  // These base types already supply `AnyModule`; emitting a second conformance here
223
225
  // would be redundant and error.
224
- if let classDecl = declaration.as(ClassDeclSyntax.self),
225
- inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
226
+ if protocols.contains(where: { $0.trimmedDescription == "AnyModule" }),
227
+ let classDecl = declaration.as(ClassDeclSyntax.self),
228
+ !inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
226
229
  {
227
- return []
230
+ let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
231
+ extensions.append(conformance.cast(ExtensionDeclSyntax.self))
232
+ }
233
+ if let sendable = uncheckedSendableExtension(for: type, of: declaration, conformingTo: protocols) {
234
+ extensions.append(sendable)
228
235
  }
229
- let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
230
- return [conformance.cast(ExtensionDeclSyntax.self)]
236
+ return extensions
231
237
  }
232
238
  }
233
239
 
@@ -11,8 +11,9 @@ import SwiftSyntaxMacros
11
11
  /// - `decode(_:in:)` and `encode(_:in:)` — the fast paths, reading each property straight off a JS
12
12
  /// object and writing it straight into one,
13
13
  /// - `from(object:appContext:)` and `toObject(appContext:)`, which forward to `decode` and `encode`,
14
- /// - `from(dictionary:appContext:)` and `toDictionary(appContext:)` — converting each property from and
15
- /// to a `[String: Any]` dictionary.
14
+ /// - `toDictionary(appContext:)`, which puts each property into a `[String: Any]` dictionary,
15
+ /// - `from(dictionary:appContext:)`, which throws `RecordDictionaryConversionException`: a record
16
+ /// converts only from JavaScript values, so it can't be created from a native dictionary.
16
17
  ///
17
18
  /// The type is auto-conformed to `Record` (a core protocol whose requirements are this surface); the
18
19
  /// synthesized methods override `Record`'s reflection-based defaults, and the synthesized `decode` and
@@ -37,23 +38,16 @@ import SwiftSyntaxMacros
37
38
  /// have static stored properties. The macro can't tell that an extension extends a generic type, so a
38
39
  /// record declared in one doesn't compile.
39
40
  ///
40
- /// The JS-value paths convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
41
- /// (`decode(_:in:)` / `encode(_:in:)`); the native-`Any` dictionary paths still go through the
42
- /// public dynamic-type API — `T.getDynamicType()` plus `cast(_:appContext:)` / `convertToJS(_:appContext:)`
43
- /// (`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
44
- /// values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
45
- /// user modules without any internal core symbols. Every property type must therefore conform to both
46
- /// `AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`. The conformance assertion checks only
47
- /// the JS codable protocols; the compiler reports a missing `AnyArgument` at the `getDynamicType()`
48
- /// call.
41
+ /// Properties convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
42
+ /// (`decode(_:in:)` / `encode(_:in:)`), spelled with `public` symbols so the synthesized code compiles
43
+ /// inside user modules without any internal core symbols. Every property type must conform to
44
+ /// `JavaScriptDecodable & JavaScriptEncodable`, which the conformance assertion checks.
49
45
  ///
50
46
  /// A property whose type mentions `Any` (`Any?`, `[String: Any]?`, `[String: [Any]]`, …) can't conform to
51
- /// the JS codable protocols, so it isn't asserted. On the JS-value paths it converts through the
52
- /// `JavaScriptValue` free-form methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and
53
- /// the matching `encodeAny…` for `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic
54
- /// `decodeAny(_:as:in:)` / `encodeAny(_:in:)` for any other type. Such a property only needs
55
- /// `AnyArgument` for the dictionary paths, and a bare `Any` needs nothing, since the dictionary paths
56
- /// pass it through unchanged.
47
+ /// the JS codable protocols, so it isn't asserted. It converts through the `JavaScriptValue` free-form
48
+ /// methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and the matching `encodeAny…` for
49
+ /// `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic `decodeAny(_:as:in:)` /
50
+ /// `encodeAny(_:in:)` for any other type.
57
51
  ///
58
52
  /// A record class must be `final` and have no inheritance clause. The synthesized factories construct
59
53
  /// `Self` through the memberwise init, which only a final class can do without a `required` init, and
@@ -112,7 +106,7 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
112
106
  members.append(memberwiseInit(properties: properties))
113
107
  }
114
108
  members.append(fromJSObjectFactory())
115
- members.append(fromDictionaryFactory(properties: properties))
109
+ members.append(fromDictionaryFactory())
116
110
  members.append(toDictionaryMethod(properties: properties))
117
111
  members.append(toObjectMethod())
118
112
  if cachesPropNames {
@@ -152,8 +146,10 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
152
146
  return []
153
147
  }
154
148
 
149
+ // `nonisolated` keeps the conformance usable on the JavaScript thread when the module builds with a
150
+ // default isolation, which would otherwise isolate it to the main actor.
155
151
  let ext: DeclSyntax = """
156
- extension \(type.trimmed): Record {}
152
+ extension \(type.trimmed): nonisolated Record {}
157
153
  """
158
154
  guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
159
155
  return []
@@ -171,8 +167,7 @@ private struct RecordProperty {
171
167
  /// every identifier position in the generated code needs them.
172
168
  let name: String
173
169
  /// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
174
- /// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` (and, on the
175
- /// dictionary paths, `getDynamicType()`) conversions.
170
+ /// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` conversions.
176
171
  let type: String
177
172
  /// The default-value expression verbatim (`0`, `""`, `[]`), or `nil` when the property has none.
178
173
  /// Inlined into the memberwise init and the factories' omitted-property branch so the synthesized
@@ -304,13 +299,13 @@ private func defaultInit(properties: [RecordProperty]) -> DeclSyntax? {
304
299
  }
305
300
  if properties.contains(where: { $0.isRequired }) {
306
301
  return """
307
- public init() {
308
- fatalError("\\(Self.self) has required properties and cannot be created with init(); construct it through the @Record-synthesized from(dictionary:) or from(object:) factories")
302
+ nonisolated public init() {
303
+ fatalError("\\(Self.self) has required properties and cannot be created with init(); create it from JavaScript or with its memberwise initializer")
309
304
  }
310
305
  """
311
306
  }
312
307
  return """
313
- public init() {
308
+ nonisolated public init() {
314
309
  }
315
310
  """
316
311
  }
@@ -333,7 +328,7 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
333
328
  let signature = params.joined(separator: ", ")
334
329
 
335
330
  return """
336
- public init(\(raw: signature)) {
331
+ nonisolated public init(\(raw: signature)) {
337
332
  \(raw: assignments)
338
333
  }
339
334
  """
@@ -355,13 +350,13 @@ private func fromJSObjectFactory() -> DeclSyntax {
355
350
  """
356
351
  }
357
352
 
358
- /// `from(dictionary:appContext:)` — same as the JS path but reads from `[String: Any]` and
359
- /// uses the `Any?` cast overload; a missing key is `dictionary[key] == nil`.
360
- private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
361
- let body = factoryBody(properties: properties, readLines: dictionaryReadLines(properties: properties))
353
+ /// `from(dictionary:appContext:)`, a `Record` requirement. A record converts only from JavaScript values,
354
+ /// through `decode(_:in:)`, so it throws. Converting each property from a native `Any` would need the
355
+ /// dynamic types of the 1.0 conversion API.
356
+ private func fromDictionaryFactory() -> DeclSyntax {
362
357
  return """
363
- public static func from(dictionary: [String: Any], appContext: AppContext) throws -> Self {
364
- \(raw: body)
358
+ nonisolated public static func from(dictionary: [String: Any], appContext: AppContext) throws -> Self {
359
+ throw RecordDictionaryConversionException(String(describing: Self.self))
365
360
  }
366
361
  """
367
362
  }
@@ -413,44 +408,7 @@ private func jsObjectReadLines(properties: [RecordProperty], cachesPropNames: Bo
413
408
  return lines
414
409
  }
415
410
 
416
- /// Per-property read statements for the dictionary factory, each producing a `let field<index>` from the
417
- /// raw dictionary value in `entry<index>`. A bare `Any` property has no dynamic type to cast through, and
418
- /// the dictionary value already is an `Any`, so it's read unchanged.
419
- private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
420
- var lines: [String] = []
421
- for (index, property) in properties.enumerated() {
422
- let field = fieldLocal(index)
423
- let entry = "entry\(index)"
424
- let exprType = expressionType(property.type)
425
- let isBareAny = property.freeForm?.isBareAny == true
426
- let cast: String
427
- if isBareAny {
428
- cast = entry
429
- } else if tupleConversion(of: property.type) != nil {
430
- // A type that holds a tuple has no dynamic type, so the dictionary value must already be of the
431
- // property's type.
432
- cast =
433
- "try { () throws -> \(exprType) in guard let value = \(entry) as? \(exprType) else { throw Conversions.ConvertingException<\(exprType)>(\(entry)) }; return value }()"
434
- } else {
435
- cast = "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
436
- }
437
- lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
438
- if property.isRequired {
439
- lines.append(" guard let \(entry) else {")
440
- lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
441
- lines.append(" }")
442
- lines.append(" let \(field) = \(cast)")
443
- } else if property.isOptional {
444
- lines.append(" let \(field): \(property.type) = (\(entry) == nil || \(entry)! is NSNull) ? nil : \(cast)")
445
- } else {
446
- lines.append(" let \(field) = \(entry) == nil ? \(property.defaultValue!) : \(isBareAny ? entry + "!" : cast)")
447
- }
448
- }
449
- return lines
450
- }
451
-
452
- /// `toDictionary(appContext:)` — converts each property back to a JS-compatible value via the
453
- /// dynamic type and assembles a `[String: Any]`.
411
+ /// `toDictionary(appContext:)`, which puts each property into a `[String: Any]` as it is.
454
412
  private func toDictionaryMethod(properties: [RecordProperty]) -> DeclSyntax {
455
413
  var lines = [" var dictionary: [String: Any] = [:]"]
456
414
  for property in properties {
@@ -458,7 +416,7 @@ private func toDictionaryMethod(properties: [RecordProperty]) -> DeclSyntax {
458
416
  }
459
417
  lines.append(" return dictionary")
460
418
  return """
461
- public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
419
+ nonisolated public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
462
420
  \(raw: lines.joined(separator: "\n"))
463
421
  }
464
422
  """
@@ -578,7 +536,7 @@ private func propNamesClass(properties: [RecordProperty]) -> DeclSyntax {
578
536
  " self.\(propNameMember(index)) = JavaScriptPropNameID(runtime, string: \"\(property.key)\")"
579
537
  }
580
538
  return """
581
- private final class \(raw: propNamesTypeName) {
539
+ nonisolated private final class \(raw: propNamesTypeName) {
582
540
  static let _key = JavaScriptRuntime.Cache.Key<\(raw: propNamesTypeName)>()
583
541
 
584
542
  \(raw: storedProperties.joined(separator: "\n"))
@@ -21,6 +21,9 @@ import SwiftSyntaxMacros
21
21
  ///
22
22
  /// The companion `@ExpoModule(classes: [Cache.self])` registers the class with the module, which
23
23
  /// exposes it in its JavaScript object.
24
+ ///
25
+ /// As an extension macro, it adds an `@unchecked Sendable` conformance when the class isn't
26
+ /// `Sendable` yet.
24
27
  public struct SharedObjectMacro: MemberMacro {
25
28
  public static func expansion(
26
29
  of node: AttributeSyntax,
@@ -38,6 +41,11 @@ public struct SharedObjectMacro: MemberMacro {
38
41
  let typeName = classDecl.name.text
39
42
  let jsName = jsNameArgument(of: node) ?? withoutBackticks(typeName)
40
43
 
44
+ // A superclass other than `SharedObject` or `SharedRef` may be another `@SharedObject` class, so the
45
+ // decorator phases call `super` to bind its `@JS` members too. The base classes' phases bind nothing.
46
+ let callsSuper = !inheritsFromAny(classDecl, names: ["SharedObject", "SharedRef"])
47
+ let isFinal = classDecl.modifiers.contains { $0.name.tokenKind == .keyword(.final) }
48
+
41
49
  // `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
42
50
  // the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`.
43
51
  //
@@ -117,7 +125,9 @@ public struct SharedObjectMacro: MemberMacro {
117
125
  phase: .prototype,
118
126
  functions: instanceFunctions,
119
127
  properties: instanceProperties,
120
- typeName: typeName
128
+ typeName: typeName,
129
+ callsSuper: callsSuper,
130
+ isFinal: isFinal
121
131
  )
122
132
  )
123
133
  }
@@ -127,12 +137,14 @@ public struct SharedObjectMacro: MemberMacro {
127
137
  phase: .constructor,
128
138
  functions: staticFunctions,
129
139
  properties: staticProperties,
130
- typeName: typeName
140
+ typeName: typeName,
141
+ callsSuper: callsSuper,
142
+ isFinal: isFinal
131
143
  )
132
144
  )
133
145
  }
134
146
  if let constructor {
135
- emitted.append(constructor.buildConstructor(typeName: typeName))
147
+ emitted.append(constructor.buildConstructor(typeName: typeName, isFinal: isFinal))
136
148
  }
137
149
 
138
150
  // The conformance assertions of the `@JS` members, which `@JS` leaves to this macro (see
@@ -168,3 +180,18 @@ extension SharedObjectMacro: MemberAttributeMacro {
168
180
  return ["@JavaScriptActor"]
169
181
  }
170
182
  }
183
+
184
+ extension SharedObjectMacro: ExtensionMacro {
185
+ public static func expansion(
186
+ of node: AttributeSyntax,
187
+ attachedTo declaration: some DeclGroupSyntax,
188
+ providingExtensionsOf type: some TypeSyntaxProtocol,
189
+ conformingTo protocols: [TypeSyntax],
190
+ in context: some MacroExpansionContext
191
+ ) throws -> [ExtensionDeclSyntax] {
192
+ guard let sendable = uncheckedSendableExtension(for: type, of: declaration, conformingTo: protocols) else {
193
+ return []
194
+ }
195
+ return [sendable]
196
+ }
197
+ }
@@ -98,7 +98,9 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
98
98
  return []
99
99
  }
100
100
 
101
- let conformances = missing.map { $0.trimmedDescription }.joined(separator: ", ")
101
+ // `nonisolated` keeps the conformances usable on the JavaScript thread when the module builds with
102
+ // a default isolation, which would otherwise isolate them to the main actor.
103
+ let conformances = missing.map { "nonisolated \($0.trimmedDescription)" }.joined(separator: ", ")
102
104
  let ext: DeclSyntax = """
103
105
  extension \(type.trimmed): \(raw: conformances) {}
104
106
  """
@@ -298,7 +300,7 @@ private func decodableKindsProperty(union: UnionType) -> DeclSyntax {
298
300
  // optimizer folds it to a constant after specialization, the same as a chain of `union(_:)` calls.
299
301
  let expression = kinds.count == 1 ? kinds[0] : "[\(kinds.joined(separator: ", "))]"
300
302
  return """
301
- public static var decodableKinds: JavaScriptValueKinds {
303
+ nonisolated public static var decodableKinds: JavaScriptValueKinds {
302
304
  return \(raw: expression)
303
305
  }
304
306
  """
@@ -367,7 +369,7 @@ private func accessorMethods(union: UnionType) -> [DeclSyntax] {
367
369
  let mismatch =
368
370
  "(unionName: \"\(union.name)\", received: _payloadTypeName, expected: [\"\(unionCase.payloadType)\"])"
369
371
  return """
370
- public func `as`(_ type: \(raw: payloadType).Type) throws -> \(raw: payloadType) {
372
+ nonisolated public func `as`(_ type: \(raw: payloadType).Type) throws -> \(raw: payloadType) {
371
373
  if case \(raw: unionCase.pattern) = self {
372
374
  return payload
373
375
  }
@@ -389,7 +391,7 @@ private func payloadTypeNameProperty(union: UnionType) -> DeclSyntax {
389
391
  lines.append(" }")
390
392
  let body = lines.joined(separator: "\n")
391
393
  return """
392
- private var _payloadTypeName: String {
394
+ nonisolated private var _payloadTypeName: String {
393
395
  \(raw: body)
394
396
  }
395
397
  """
@@ -0,0 +1,40 @@
1
+ @_spi(PluginMessage) import SwiftCompilerPluginMessageHandling
2
+ import SwiftSyntaxMacros
3
+
4
+ /// The module name that `expo-modules-core` passes to `#externalMacro`, and the name of the macro
5
+ /// target that SwiftPM loads as the plugin.
6
+ private let pluginModuleName = "ExpoModulesMacros"
7
+
8
+ /// Runs the compiler plugin server on stdin and stdout until the compiler closes the connection.
9
+ ///
10
+ /// `CompilerPlugin.main()` finds a macro by comparing `String(reflecting:)` of each type with
11
+ /// `<module>.<type>` from `#externalMacro`. The macro types live in this module, not in a module
12
+ /// named `ExpoModulesMacros`, so both executables that serve the plugin (the SwiftPM macro target and
13
+ /// the shipped tool) use this provider, which matches the type name under the plugin module name.
14
+ public func runPluginServer() throws {
15
+ let connection = try StandardIOMessageConnection()
16
+ let listener = CompilerPluginMessageListener(connection: connection, provider: ProvidedMacroProvider())
17
+ try listener.main()
18
+ }
19
+
20
+ private struct ProvidedMacroProvider: PluginProvider {
21
+ let macros: [String: Macro.Type] = Dictionary(
22
+ uniqueKeysWithValues: ProvidedMacro.all.map { (String(describing: $0.type), $0.type) }
23
+ )
24
+
25
+ func resolveMacro(moduleName: String, typeName: String) throws -> Macro.Type {
26
+ guard moduleName == pluginModuleName, let type = macros[typeName] else {
27
+ throw UnknownMacroError(moduleName: moduleName, typeName: typeName)
28
+ }
29
+ return type
30
+ }
31
+ }
32
+
33
+ private struct UnknownMacroError: Error, CustomStringConvertible {
34
+ let moduleName: String
35
+ let typeName: String
36
+
37
+ var description: String {
38
+ "macro implementation type '\(moduleName).\(typeName)' could not be found in the Expo Modules macro plugin"
39
+ }
40
+ }
@@ -3,21 +3,21 @@ import SwiftSyntaxMacros
3
3
 
4
4
  /// A macro that the plugin provides, with the attribute name and the extension conformances of its
5
5
  /// declaration in `expo-modules-core`.
6
- struct ProvidedMacro {
7
- let name: String
8
- let type: Macro.Type
9
- var conformances: [TypeSyntax] = []
6
+ public struct ProvidedMacro {
7
+ public let name: String
8
+ public let type: Macro.Type
9
+ public var conformances: [TypeSyntax] = []
10
10
 
11
11
  /// Every macro of the plugin. The compiler gets the types from `providingMacros`, and `expand`
12
12
  /// matches the attributes by name and passes the same conformances as the compiler, so both read
13
13
  /// this list.
14
- static var all: [ProvidedMacro] {
14
+ public static var all: [ProvidedMacro] {
15
15
  [
16
16
  ProvidedMacro(name: "OptimizedFunction", type: OptimizedFunctionAttachedMacro.self),
17
17
  ProvidedMacro(name: "JS", type: JSMacro.self),
18
18
  ProvidedMacro(name: "Event", type: EventMacro.self),
19
- ProvidedMacro(name: "ExpoModule", type: ExpoModuleMacro.self, conformances: ["AnyModule"]),
20
- ProvidedMacro(name: "SharedObject", type: SharedObjectMacro.self),
19
+ ProvidedMacro(name: "ExpoModule", type: ExpoModuleMacro.self, conformances: ["AnyModule", "Sendable"]),
20
+ ProvidedMacro(name: "SharedObject", type: SharedObjectMacro.self, conformances: ["Sendable"]),
21
21
  ProvidedMacro(name: "Record", type: RecordMacro.self, conformances: ["Record"]),
22
22
  ProvidedMacro(
23
23
  name: "Union",
@@ -398,7 +398,10 @@ internal struct JSProperty {
398
398
  /// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
399
399
  /// function bindings: a module captures `self` strong, a shared object captures nothing of the
400
400
  /// instance. Getter and setter are gated independently.
401
- func decorateStatements(object: String, receiver: Receiver) -> String {
401
+ ///
402
+ /// With `configurable`, the accessor can be redefined, so a subclass of a shared object class can
403
+ /// replace it with its own member of the same JS name. A `lazy var` is always configurable.
404
+ func decorateStatements(object: String, receiver: Receiver, configurable: Bool = false) -> String {
402
405
  let descriptorName = derivedIdentifier(from: swiftName, suffix: "Descriptor")
403
406
  let callee = receiver.callee
404
407
  // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
@@ -409,8 +412,8 @@ internal struct JSProperty {
409
412
 
410
413
  lines.append("let \(descriptorName) = runtime.createObject()")
411
414
  lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
412
- if isLazy {
413
- // The getter redefines the property on `this`, which needs a configurable accessor when `this`
415
+ if isLazy || configurable {
416
+ // A lazy getter redefines the property on `this`, which needs a configurable accessor when `this`
414
417
  // is the object that holds it (a module).
415
418
  lines.append("\(descriptorName).setProperty(\"configurable\", value: true)")
416
419
  }
@@ -523,14 +526,30 @@ private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversio
523
526
  /// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
524
527
  /// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
525
528
  /// metatype for a static member).
529
+ ///
530
+ /// `configurableProperties` makes the property accessors configurable. `replacesInherited` first
531
+ /// redefines the name of each function as a writable data property: the object may already hold an
532
+ /// inherited accessor with that name, which an assignment would call instead of replacing.
526
533
  private func decorateBody(
527
534
  functions: [JSFunction],
528
535
  properties: [JSProperty],
529
536
  object: String,
530
- receiver: Receiver
537
+ receiver: Receiver,
538
+ configurableProperties: Bool = false,
539
+ replacesInherited: Bool = false
531
540
  ) -> String {
532
- let functionBody = functions.map { $0.decorateStatements(object: object, receiver: receiver) }
533
- let propertyBody = properties.map { $0.decorateStatements(object: object, receiver: receiver) }
541
+ let functionBody = functions.map { function in
542
+ let binding = function.decorateStatements(object: object, receiver: receiver)
543
+ guard replacesInherited else {
544
+ return binding
545
+ }
546
+ let redefine =
547
+ " \(object).defineProperty(\"\(function.jsName)\", descriptor: .init(configurable: true, enumerable: true, writable: true))"
548
+ return redefine + "\n" + binding
549
+ }
550
+ let propertyBody = properties.map {
551
+ $0.decorateStatements(object: object, receiver: receiver, configurable: configurableProperties)
552
+ }
534
553
  return (functionBody + propertyBody).joined(separator: "\n")
535
554
  }
536
555
 
@@ -568,11 +587,21 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
568
587
  /// constructor). The constructor *object* the static members decorate is distinct from the `@JS init`,
569
588
  /// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
570
589
  /// is emitted only when the type has a member for it.
590
+ ///
591
+ /// With `callsSuper`, the phase first calls the superclass's phase, so a subclass of another
592
+ /// `@SharedObject` class also gets the inherited `@JS` members. Core builds each JavaScript class on
593
+ /// the base `SharedObject` class, not on the JavaScript class of the superclass, so the inherited
594
+ /// members have to be bound on the subclass's own objects. They are bound first, so a subclass member
595
+ /// with the same JavaScript name replaces the inherited one. For that, the property accessors of a
596
+ /// class that isn't `final` are configurable, and a subclass redefines the name of each of its functions
597
+ /// before it binds the function.
571
598
  internal func buildDecorateSharedObjectPhase(
572
599
  phase: Phase,
573
600
  functions: [JSFunction],
574
601
  properties: [JSProperty],
575
- typeName: String
602
+ typeName: String,
603
+ callsSuper: Bool,
604
+ isFinal: Bool
576
605
  ) -> DeclSyntax {
577
606
  let receiver: Receiver =
578
607
  phase == .constructor
@@ -582,12 +611,18 @@ internal func buildDecorateSharedObjectPhase(
582
611
  functions: functions,
583
612
  properties: properties,
584
613
  object: phase.rawValue,
585
- receiver: receiver
614
+ receiver: receiver,
615
+ configurableProperties: !isFinal,
616
+ replacesInherited: callsSuper
586
617
  )
618
+ let superCall =
619
+ callsSuper
620
+ ? " try super._decorateSharedObject(\(phase.rawValue): \(phase.rawValue), in: runtime)\n"
621
+ : ""
587
622
  return """
588
623
  @JavaScriptActor
589
624
  public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
590
- \(raw: body)
625
+ \(raw: superCall)\(raw: body)
591
626
  }
592
627
  """
593
628
  }
@@ -51,11 +51,24 @@ internal struct JSConstructor {
51
51
 
52
52
  /// Builds an instance from the JS arguments. Overrides the base `SharedObject` class method; returns
53
53
  /// the concrete instance, which promotes to the base `SharedObject?` return type.
54
- func buildConstructor(typeName: String) -> DeclSyntax {
54
+ ///
55
+ /// A subclass without a `@JS init` of its own inherits this override, and would construct an instance
56
+ /// of this class instead of the subclass. Unless the class is `final`, the override returns `nil` when
57
+ /// called on a subclass, so core reports that the subclass has no JavaScript constructor.
58
+ func buildConstructor(typeName: String, isFinal: Bool) -> DeclSyntax {
59
+ let subclassGuard =
60
+ isFinal
61
+ ? ""
62
+ : """
63
+ guard self == \(typeName).self else {
64
+ return nil
65
+ }
66
+
67
+ """
55
68
  return """
56
69
  @JavaScriptActor
57
70
  public override class func _constructSharedObject(this: borrowing JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
58
- \(raw: bodyStatements(typeName: typeName, indent: " "))
71
+ \(raw: subclassGuard)\(raw: bodyStatements(typeName: typeName, indent: " "))
59
72
  }
60
73
  """
61
74
  }
@@ -479,8 +479,7 @@ extension AttributeListSyntax {
479
479
  }
480
480
  }
481
481
 
482
- /// A type spelled so it's valid in expression position (before `.decode`/`.encode`, before
483
- /// `.getDynamicType()`, or after `as!`). Implicitly-unwrapped optionals (`T!`) are only allowed in
482
+ /// A type spelled so it's valid in expression position (before `.decode`/`.encode`). Implicitly-unwrapped optionals (`T!`) are only allowed in
484
483
  /// type-annotation position, so a trailing `!` is rewritten to `?` (`T!` and `T?` are both
485
484
  /// `Optional<T>`, which the conversion layer treats identically). Other type spellings pass through
486
485
  /// unchanged.
@@ -525,7 +524,7 @@ internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of value
525
524
  /// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
526
525
  /// backticks in every identifier position, but a name JavaScript sees (a JS member name, an event
527
526
  /// name, a record key) is the bare name.
528
- internal func withoutBackticks(_ name: String) -> String {
527
+ public func withoutBackticks(_ name: String) -> String {
529
528
  var name = Substring(name)
530
529
  while name.first == "`" {
531
530
  name.removeFirst()