expo-modules-macros 0.21.0 → 0.23.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 (41) hide show
  1. package/.github/workflows/linux.yml +8 -5
  2. package/.github/workflows/publish.yml +26 -11
  3. package/.github/workflows/swift-format.yml +79 -0
  4. package/.github/workflows/swift.yml +7 -4
  5. package/.github/workflows/windows.yml +8 -5
  6. package/AGENTS.md +16 -9
  7. package/README.md +20 -5
  8. package/apple/ExpoModulesMacros +0 -0
  9. package/apple/Package.resolved +3 -3
  10. package/apple/Package.swift +1 -2
  11. package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
  12. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +144 -30
  13. package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
  14. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +63 -68
  15. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
  16. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
  17. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
  18. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +184 -48
  19. package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
  20. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +172 -43
  21. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +286 -222
  22. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +49 -56
  23. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
  24. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
  25. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
  26. package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
  27. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
  28. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
  29. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
  30. package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
  31. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +20 -4
  32. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +18 -5
  33. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
  34. package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
  35. package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +28 -8
  36. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
  37. package/build/index.d.ts +1 -1
  38. package/build/index.js +3 -3
  39. package/build/types.d.ts +32 -2
  40. package/build/types.js +1 -1
  41. package/package.json +8 -5
@@ -44,7 +44,11 @@ struct RefIndex {
44
44
  }
45
45
  for sharedObject in surface.sharedObjects {
46
46
  entries[sharedObject.name] = Entry(
47
- name: sharedObject.name, kind: .sharedObject, jsType: .object, jsName: sharedObject.jsName)
47
+ name: sharedObject.name,
48
+ kind: .sharedObject,
49
+ jsType: .object,
50
+ jsName: sharedObject.jsName
51
+ )
48
52
  }
49
53
  for union in surface.unions {
50
54
  // A union crosses as whichever alternative matched, so its category is only meaningful per
@@ -53,7 +57,10 @@ struct RefIndex {
53
57
  }
54
58
  for enumeration in surface.enums {
55
59
  entries[enumeration.name] = Entry(
56
- name: enumeration.name, kind: .enum, jsType: jsType(ofRawType: enumeration.rawType))
60
+ name: enumeration.name,
61
+ kind: .enum,
62
+ jsType: jsType(ofRawType: enumeration.rawType)
63
+ )
57
64
  }
58
65
  }
59
66
 
@@ -105,6 +112,9 @@ extension TypeNode {
105
112
  switch self {
106
113
  case .primitive, .unknown:
107
114
  return self
115
+ case .sharedRef:
116
+ // The native type never reaches JS, so it isn't looked up among the scanned declarations.
117
+ return self
108
118
  case .ref(let name, _, _):
109
119
  guard let entry = index.lookup(name, in: scope) else {
110
120
  return self
@@ -113,7 +123,8 @@ extension TypeNode {
113
123
  return .ref(
114
124
  name: entry.name,
115
125
  refKind: entry.kind,
116
- jsTypeOverride: entry.jsType == .object ? nil : entry.jsType)
126
+ jsTypeOverride: entry.jsType == .object ? nil : entry.jsType
127
+ )
117
128
  case .optional(let wrapped):
118
129
  return .optional(wrapped: wrapped.resolvingRefs(using: index, in: scope))
119
130
  case .array(let element):
@@ -121,7 +132,8 @@ extension TypeNode {
121
132
  case .dictionary(let key, let value):
122
133
  return .dictionary(
123
134
  key: key.resolvingRefs(using: index, in: scope),
124
- value: value.resolvingRefs(using: index, in: scope))
135
+ value: value.resolvingRefs(using: index, in: scope)
136
+ )
125
137
  case .promise(let value):
126
138
  return .promise(value: value.resolvingRefs(using: index, in: scope))
127
139
  case .function(let parameters, let returns, let isAsync, let isThrowing):
@@ -153,7 +165,8 @@ extension ExportedSurface {
153
165
  resolved.events = module.events.map { $0.resolvingRefs(using: index, in: module.name) }
154
166
  resolved.classes = module.classes.map { exportedClass in
155
167
  guard let entry = index.lookup(exportedClass.name, in: module.name),
156
- entry.kind == .sharedObject else {
168
+ entry.kind == .sharedObject
169
+ else {
157
170
  return exportedClass
158
171
  }
159
172
  return ExportedClass(name: entry.name, jsName: entry.jsName)
@@ -1,10 +1,11 @@
1
+ import SwiftParser
2
+ import SwiftSyntax
3
+
1
4
  #if canImport(FoundationEssentials)
2
5
  import FoundationEssentials
3
6
  #else
4
7
  import Foundation
5
8
  #endif
6
- import SwiftParser
7
- import SwiftSyntax
8
9
 
9
10
  extension Scanner {
10
11
  /// Runs `scan-exports` over `paths`, prints the JSON report to stdout, and returns the exit code
@@ -58,7 +59,11 @@ func scanExports(paths: [String]) -> ScanExportsResult {
58
59
  // Refs resolve only once every file has been walked: a type parsed in one file may name a type
59
60
  // declared in another, so the full set of declarations has to exist before any lookup is valid.
60
61
  let surface = ExportedSurface(
61
- modules: modules, sharedObjects: sharedObjects, records: records, enums: enums, unions: unions
62
+ modules: modules,
63
+ sharedObjects: sharedObjects,
64
+ records: records,
65
+ enums: enums,
66
+ unions: unions
62
67
  ).resolvingRefs()
63
68
 
64
69
  return ScanExportsResult(
@@ -100,7 +100,8 @@ final class SurfaceVisitor: SyntaxVisitor {
100
100
  accessLevel: spelledAccessLevel(of: node.modifiers),
101
101
  platforms: platforms,
102
102
  file: file
103
- ))
103
+ )
104
+ )
104
105
  } else if inherits(from: enumerableConformanceName, in: node.inheritanceClause) {
105
106
  let rawType = rawValueType(of: node.inheritanceClause)
106
107
  enums.append(
@@ -111,7 +112,8 @@ final class SurfaceVisitor: SyntaxVisitor {
111
112
  accessLevel: spelledAccessLevel(of: node.modifiers),
112
113
  platforms: platforms,
113
114
  file: file
114
- ))
115
+ )
116
+ )
115
117
  }
116
118
  // An enum is often a namespace for other types, so its body is searched too.
117
119
  return .visitChildren
@@ -156,7 +158,8 @@ final class SurfaceVisitor: SyntaxVisitor {
156
158
  accessLevel: accessLevel,
157
159
  platforms: platforms,
158
160
  file: file
159
- ))
161
+ )
162
+ )
160
163
  return
161
164
  }
162
165
 
@@ -167,13 +170,15 @@ final class SurfaceVisitor: SyntaxVisitor {
167
170
  name: qualified,
168
171
  jsName: stringArgument(of: attribute) ?? name,
169
172
  initializer: initializer,
173
+ sharedRef: sharedRefSuperclass(of: node),
170
174
  functions: functions,
171
175
  properties: properties,
172
176
  events: events,
173
177
  accessLevel: accessLevel,
174
178
  platforms: platforms,
175
179
  file: file
176
- ))
180
+ )
181
+ )
177
182
  return
178
183
  }
179
184
 
@@ -185,7 +190,8 @@ final class SurfaceVisitor: SyntaxVisitor {
185
190
  accessLevel: accessLevel,
186
191
  platforms: platforms,
187
192
  file: file
188
- ))
193
+ )
194
+ )
189
195
  }
190
196
  }
191
197
 
@@ -214,7 +220,8 @@ final class SurfaceVisitor: SyntaxVisitor {
214
220
 
215
221
  if let varDecl = decl.as(VariableDeclSyntax.self),
216
222
  varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) == nil,
217
- let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue) {
223
+ let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue)
224
+ {
218
225
  events.append(contentsOf: makeEvents(varDecl: varDecl, attribute: attribute, platforms: platforms))
219
226
  continue
220
227
  }
@@ -224,7 +231,8 @@ final class SurfaceVisitor: SyntaxVisitor {
224
231
  }
225
232
 
226
233
  if let initDecl = decl.as(InitializerDeclSyntax.self),
227
- initDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) != nil {
234
+ initDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) != nil
235
+ {
228
236
  // At most one `@JS init`; keep the first if a malformed source has more (the macro errors).
229
237
  if initializer == nil {
230
238
  initializer = ExportedInitializer(
@@ -237,13 +245,15 @@ final class SurfaceVisitor: SyntaxVisitor {
237
245
  }
238
246
 
239
247
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
240
- let attribute = funcDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) {
241
- functions.append(makeFunction(funcDecl: funcDecl, attribute: attribute, platforms: platforms))
248
+ let attribute = funcDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue)
249
+ {
250
+ functions.append(contentsOf: makeFunctions(funcDecl: funcDecl, attribute: attribute, platforms: platforms))
242
251
  continue
243
252
  }
244
253
 
245
254
  if let varDecl = decl.as(VariableDeclSyntax.self),
246
- let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) {
255
+ let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue)
256
+ {
247
257
  properties.append(contentsOf: makeProperties(varDecl: varDecl, attribute: attribute, platforms: platforms))
248
258
  }
249
259
  }
@@ -256,10 +266,13 @@ final class SurfaceVisitor: SyntaxVisitor {
256
266
  /// reporting one would describe a surface that does not exist. `@JS` on the same property is
257
267
  /// caught by the caller, which reaches the `@JS` branch first.
258
268
  private func makeEvents(
259
- varDecl: VariableDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
269
+ varDecl: VariableDeclSyntax,
270
+ attribute: AttributeSyntax,
271
+ platforms: [ExportedPlatform]
260
272
  ) -> [ExportedEvent] {
261
273
  guard varDecl.bindingSpecifier.tokenKind != .keyword(.let),
262
- !isTypeLevel(varDecl.modifiers) else {
274
+ !isTypeLevel(varDecl.modifiers)
275
+ else {
263
276
  return []
264
277
  }
265
278
  let override = stringArgument(of: attribute)
@@ -274,7 +287,8 @@ final class SurfaceVisitor: SyntaxVisitor {
274
287
  binding.accessorBlock == nil,
275
288
  let functionType = underlyingFunctionType(of: binding.typeAnnotation?.type),
276
289
  isVoidEventReturn(functionType.returnClause.type),
277
- functionType.parameters.count <= 1 else {
290
+ functionType.parameters.count <= 1
291
+ else {
278
292
  continue
279
293
  }
280
294
  let name = ident.identifier.identifierName
@@ -286,38 +300,57 @@ final class SurfaceVisitor: SyntaxVisitor {
286
300
  isSync: isSync,
287
301
  accessLevel: spelledAccessLevel(of: varDecl.modifiers),
288
302
  platforms: platforms
289
- ))
303
+ )
304
+ )
290
305
  }
291
306
  return result
292
307
  }
293
308
 
294
- /// Builds an `ExportedFunction` from a `@JS func`: JS-name fallback, parameters, a `Void` return as
295
- /// `nil`, and the effect/static flags.
296
- private func makeFunction(
297
- funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
298
- ) -> ExportedFunction {
309
+ /// Builds the `ExportedFunction` entries for a `@JS func`: JS-name fallback, parameters, a `Void`
310
+ /// return as `nil`, and the effect/static flags. A synchronous function with `.withAsync("name")`
311
+ /// gets a second, `async` entry under that name, for the promise-returning variant the macro binds.
312
+ private func makeFunctions(
313
+ funcDecl: FunctionDeclSyntax,
314
+ attribute: AttributeSyntax,
315
+ platforms: [ExportedPlatform]
316
+ ) -> [ExportedFunction] {
299
317
  let effects = funcDecl.signature.effectSpecifiers
300
318
  let returnType = funcDecl.signature.returnClause?.type
301
- return ExportedFunction(
302
- name: funcDecl.name.identifierName,
303
- jsName: stringArgument(of: attribute) ?? funcDecl.name.identifierName,
304
- parameters: parameters(of: funcDecl.signature.parameterClause),
305
- returns: isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) },
306
- isAsync: effects?.asyncSpecifier != nil,
307
- isThrowing: effects?.throwsClause?.throwsSpecifier != nil,
308
- isStatic: isTypeLevel(funcDecl.modifiers),
309
- accessLevel: spelledAccessLevel(of: funcDecl.modifiers),
310
- platforms: platforms
311
- )
319
+ let isAsync = effects?.asyncSpecifier != nil
320
+ let exportedParameters = parameters(of: funcDecl.signature.parameterClause)
321
+ let returns = isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) }
322
+ let makeFunction = { (jsName: String, isAsync: Bool) in
323
+ ExportedFunction(
324
+ name: funcDecl.name.identifierName,
325
+ jsName: jsName,
326
+ parameters: exportedParameters,
327
+ returns: returns,
328
+ isAsync: isAsync,
329
+ isThrowing: effects?.throwsClause?.throwsSpecifier != nil,
330
+ isStatic: isTypeLevel(funcDecl.modifiers),
331
+ accessLevel: spelledAccessLevel(of: funcDecl.modifiers),
332
+ platforms: platforms
333
+ )
334
+ }
335
+ let function = makeFunction(stringArgument(of: attribute) ?? funcDecl.name.identifierName, isAsync)
336
+ // The macro binds no variant for an `async` function: it reports the option there instead.
337
+ guard !isAsync, let variantName = asyncVariantName(of: attribute) else {
338
+ return [function]
339
+ }
340
+ return [function, makeFunction(variantName, true)]
312
341
  }
313
342
 
314
343
  /// Builds the `ExportedProperty` entries for a `@JS var`/`let`. One declaration can introduce several
315
344
  /// bindings (`var a, b: Int`), so this returns an array. The value type is the annotation, else the
316
345
  /// literal default's inferred type, else `nil`.
317
346
  private func makeProperties(
318
- varDecl: VariableDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
347
+ varDecl: VariableDeclSyntax,
348
+ attribute: AttributeSyntax,
349
+ platforms: [ExportedPlatform]
319
350
  ) -> [ExportedProperty] {
320
351
  let isLet = varDecl.bindingSpecifier.tokenKind == .keyword(.let)
352
+ // A `lazy var` is read-only from JS, like a `let`: the macro binds it getter-only.
353
+ let isLazy = varDecl.modifiers.contains { $0.name.tokenKind == .keyword(.lazy) }
321
354
  let isStatic = isTypeLevel(varDecl.modifiers)
322
355
  let override = stringArgument(of: attribute)
323
356
  var result: [ExportedProperty] = []
@@ -333,13 +366,14 @@ final class SurfaceVisitor: SyntaxVisitor {
333
366
  name: name,
334
367
  jsName: override ?? name,
335
368
  type: type,
336
- isSettable: isSettable(binding: binding, isLet: isLet),
369
+ isSettable: !isLazy && isSettable(binding: binding, isLet: isLet),
337
370
  isStatic: isStatic,
338
371
  // Only a stored property has an initializer; a computed one has none to report.
339
372
  defaultValue: binding.initializer?.value.trimmedDescription,
340
373
  accessLevel: spelledAccessLevel(of: varDecl.modifiers),
341
374
  platforms: platforms
342
- ))
375
+ )
376
+ )
343
377
  }
344
378
  return result
345
379
  }
@@ -380,7 +414,8 @@ final class SurfaceVisitor: SyntaxVisitor {
380
414
  // `@Record` reads only the properties written directly in the body, so `#if` is not looked into.
381
415
  for member in members {
382
416
  guard let varDecl = member.decl.as(VariableDeclSyntax.self),
383
- !isExcludedRecordModifier(varDecl.modifiers) else {
417
+ !isExcludedRecordModifier(varDecl.modifiers)
418
+ else {
384
419
  continue
385
420
  }
386
421
  let platforms = platformResolver.platforms(of: varDecl)
@@ -406,7 +441,8 @@ final class SurfaceVisitor: SyntaxVisitor {
406
441
  defaultValue: binding.initializer?.value.trimmedDescription,
407
442
  accessLevel: spelledAccessLevel(of: varDecl.modifiers),
408
443
  platforms: platforms
409
- ))
444
+ )
445
+ )
410
446
  }
411
447
  }
412
448
  return properties
@@ -440,7 +476,8 @@ final class SurfaceVisitor: SyntaxVisitor {
440
476
  name: name,
441
477
  rawValue: writtenRawValue(of: element) ?? derivedStringRawValue(for: name, rawType: rawType),
442
478
  platforms: platforms
443
- ))
479
+ )
480
+ )
444
481
  }
445
482
  }
446
483
  return cases
@@ -467,12 +504,17 @@ final class SurfaceVisitor: SyntaxVisitor {
467
504
  for element in caseDecl.elements {
468
505
  guard let parameters = element.parameterClause?.parameters,
469
506
  parameters.count == 1, let parameter = parameters.first,
470
- parameter.defaultValue == nil else {
507
+ parameter.defaultValue == nil
508
+ else {
471
509
  continue
472
510
  }
473
511
  result.append(
474
512
  ExportedUnionMember(
475
- name: element.name.identifierName, type: typeNode(from: parameter.type), platforms: platforms))
513
+ name: element.name.identifierName,
514
+ type: typeNode(from: parameter.type),
515
+ platforms: platforms
516
+ )
517
+ )
476
518
  }
477
519
  }
478
520
  return result
@@ -508,7 +550,8 @@ final class SurfaceVisitor: SyntaxVisitor {
508
550
  }
509
551
  guard let item = node.parent?.as(MemberBlockItemSyntax.self),
510
552
  let parent = declaration(owning: item),
511
- let scope = enclosingTypes(of: parent) else {
553
+ let scope = enclosingTypes(of: parent)
554
+ else {
512
555
  return nil
513
556
  }
514
557
  if let decl = parent.as(ExtensionDeclSyntax.self) {
@@ -633,7 +676,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
633
676
  return underlyingFunctionType(of: attributed.baseType)
634
677
  }
635
678
  if let tuple = type.as(TupleTypeSyntax.self),
636
- tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil {
679
+ tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
680
+ {
637
681
  return underlyingFunctionType(of: element.type)
638
682
  }
639
683
  return type.as(FunctionTypeSyntax.self)
@@ -643,7 +687,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
643
687
  /// accepts neither `Swift.Void` nor a parenthesized `(Void)`, so it would drop valid events.
644
688
  private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
645
689
  if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
646
- let element = tuple.elements.first, element.firstName == nil {
690
+ let element = tuple.elements.first, element.firstName == nil
691
+ {
647
692
  return isVoidEventReturn(element.type)
648
693
  }
649
694
  let text = type.trimmedDescription
@@ -664,6 +709,89 @@ private func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool?
664
709
  return nil
665
710
  }
666
711
 
712
+ /// The name in a `@JS` attribute's `.withAsync("name")` option, in the arguments or inside an array
713
+ /// literal (`@JS("load", [.withAsync("loadAsync")])`), or `nil` when the option is absent or its
714
+ /// argument isn't a plain string literal. Mirrors `asyncVariantOption` and `asyncVariantName` in the
715
+ /// macros.
716
+ private func asyncVariantName(of attribute: AttributeSyntax) -> String? {
717
+ guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else {
718
+ return nil
719
+ }
720
+ let expressions = arguments.flatMap { argument -> [ExprSyntax] in
721
+ if let array = argument.expression.as(ArrayExprSyntax.self) {
722
+ return array.elements.map(\.expression)
723
+ }
724
+ return [argument.expression]
725
+ }
726
+ for expression in expressions {
727
+ guard let call = expression.as(FunctionCallExprSyntax.self),
728
+ let member = call.calledExpression.as(MemberAccessExprSyntax.self),
729
+ member.declName.baseName.text == "withAsync"
730
+ else {
731
+ continue
732
+ }
733
+ guard call.arguments.count == 1,
734
+ let argument = call.arguments.first,
735
+ argument.label == nil,
736
+ let literal = argument.expression.as(StringLiteralExprSyntax.self),
737
+ literal.segments.count == 1,
738
+ let segment = literal.segments.first?.as(StringSegmentSyntax.self)
739
+ else {
740
+ return nil
741
+ }
742
+ return segment.content.text
743
+ }
744
+ return nil
745
+ }
746
+
747
+ /// The `SharedRef<T>` a class names as its superclass, or `nil` when it names another one or none.
748
+ /// Swift requires the superclass to come first in the inheritance clause, so only that entry is read.
749
+ private func sharedRefSuperclass(of node: some DeclGroupSyntax) -> ExportedSharedRef? {
750
+ guard let superclass = node.inheritanceClause?.inheritedTypes.first?.type,
751
+ case .sharedRef(let native) = typeNode(from: superclass)
752
+ else {
753
+ return nil
754
+ }
755
+ return ExportedSharedRef(native: native, nativeRefType: nativeRefType(in: node.memberBlock.members))
756
+ }
757
+
758
+ /// The value of a `nativeRefType` override that returns a plain string literal: `"image"` in
759
+ /// `override var nativeRefType: String { "image" }`, also with `return` or inside a `get` block.
760
+ /// `unknown`, core's default, when the class declares no `nativeRefType`, and `nil` when its getter is
761
+ /// anything else.
762
+ private func nativeRefType(in members: MemberBlockItemListSyntax) -> String? {
763
+ let binding = members.lazy
764
+ .compactMap { $0.decl.as(VariableDeclSyntax.self) }
765
+ .filter { !isTypeLevel($0.modifiers) }
766
+ .flatMap { $0.bindings }
767
+ .first { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "nativeRefType" }
768
+ guard let binding else {
769
+ return "unknown"
770
+ }
771
+ let statements: CodeBlockItemListSyntax?
772
+ switch binding.accessorBlock?.accessors {
773
+ case .getter(let body):
774
+ statements = body
775
+ case .accessors(let accessors):
776
+ statements = accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.get) }?.body?.statements
777
+ case nil:
778
+ statements = nil
779
+ }
780
+ guard let statements, statements.count == 1, let item = statements.first?.item else {
781
+ return nil
782
+ }
783
+ let expression = item.as(ReturnStmtSyntax.self)?.expression ?? item.as(ExprSyntax.self)
784
+ guard let literal = expression?.as(StringLiteralExprSyntax.self),
785
+ literal.segments.count == 1,
786
+ let segment = literal.segments.first?.as(StringSegmentSyntax.self)
787
+ else {
788
+ return nil
789
+ }
790
+ // An escape would need real unescaping, so it's treated as unreadable, as in `writtenRawValue(of:)`.
791
+ let content = segment.content.text
792
+ return content.contains("\\") ? nil : content
793
+ }
794
+
667
795
  private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
668
796
  modifiers.contains {
669
797
  $0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
@@ -721,7 +849,8 @@ private func writtenRawValue(of element: EnumCaseElementSyntax) -> String? {
721
849
  return value.trimmedDescription
722
850
  }
723
851
  guard literal.segments.count == 1,
724
- let segment = literal.segments.first?.as(StringSegmentSyntax.self) else {
852
+ let segment = literal.segments.first?.as(StringSegmentSyntax.self)
853
+ else {
725
854
  return nil
726
855
  }
727
856
  // The segment's text is the literal's content with its delimiters already stripped, so a plain
@@ -777,7 +906,8 @@ private let knownNonRawValueProtocols: Set<String> = [
777
906
  func rawValueType(of clause: InheritanceClauseSyntax?) -> TypeNode? {
778
907
  guard let first = clause?.inheritedTypes.first?.type,
779
908
  let trailing = first.trimmedDescription.split(separator: ".").last.map(String.init),
780
- !knownNonRawValueProtocols.contains(trailing) else {
909
+ !knownNonRawValueProtocols.contains(trailing)
910
+ else {
781
911
  return nil
782
912
  }
783
913
  return typeNode(from: first)
@@ -801,7 +931,8 @@ extension AttributeListSyntax {
801
931
  fileprivate func firstAttribute(named name: String) -> AttributeSyntax? {
802
932
  for element in self {
803
933
  if let attribute = element.as(AttributeSyntax.self),
804
- attribute.attributeName.trimmedDescription == name {
934
+ attribute.attributeName.trimmedDescription == name
935
+ {
805
936
  return attribute
806
937
  }
807
938
  }
@@ -814,14 +945,16 @@ extension AttributeListSyntax {
814
945
  /// an array literal. A local copy of the macros' helper (the macro target can't be imported here).
815
946
  private func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
816
947
  guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
817
- let array = args.first(where: { $0.label?.text == label })?.expression.as(ArrayExprSyntax.self) else {
948
+ let array = args.first(where: { $0.label?.text == label })?.expression.as(ArrayExprSyntax.self)
949
+ else {
818
950
  return []
819
951
  }
820
952
  return array.elements.compactMap { element in
821
953
  guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
822
954
  memberAccess.declName.baseName.text == "self",
823
955
  let base = memberAccess.base,
824
- base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self) else {
956
+ base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self)
957
+ else {
825
958
  return nil
826
959
  }
827
960
  return base.trimmedDescription
@@ -8,8 +8,8 @@ import SwiftSyntax
8
8
  /// never silently dropped.
9
9
 
10
10
  /// The runtime category mirroring JavaScript's `typeof`. A coarse companion to `TypeNode.kind`: every
11
- /// structured object kind (array, dictionary, promise, ref) reports `object`. `symbol` is included for
12
- /// completeness; the scanner doesn't produce it.
11
+ /// structured object kind (array, dictionary, promise, ref, sharedRef) reports `object`. `symbol` is
12
+ /// included for completeness; the scanner doesn't produce it.
13
13
  enum JSType: String, Encodable {
14
14
  case undefined
15
15
  case object
@@ -72,6 +72,11 @@ indirect enum TypeNode: Equatable {
72
72
  /// node parsed at a use site has never seen.
73
73
  case ref(name: String, refKind: RefKind? = nil, jsTypeOverride: JSType? = nil)
74
74
 
75
+ /// `SharedRef<T>` (or `ExpoModulesCore.SharedRef<T>`): a shared object holding a native `T`, which
76
+ /// JS sees as an instance of `expo.SharedRef`. `native` is `T` parsed like any other type, but it
77
+ /// never crosses the boundary itself, so refs in it are not resolved.
78
+ case sharedRef(native: TypeNode)
79
+
75
80
  /// A spelling the parser doesn't model (generic parameter, tuple, metatype, …), kept verbatim so
76
81
  /// nothing is lost. Has no `typeof`.
77
82
  case unknown(text: String)
@@ -82,7 +87,7 @@ indirect enum TypeNode: Equatable {
82
87
  switch self {
83
88
  case .primitive(_, let jsType):
84
89
  return jsType
85
- case .array, .dictionary, .promise:
90
+ case .array, .dictionary, .promise, .sharedRef:
86
91
  return .object
87
92
  case .ref(_, _, let jsTypeOverride):
88
93
  // An unresolved ref, and every resolved one but an enum, is an object.
@@ -105,7 +110,7 @@ extension TypeNode: Encodable {
105
110
  // The `typeof` category. Spelled `typeof` in JSON (what the consumer reads); the Swift property is
106
111
  // `jsType` since it's the cleaner Swift name.
107
112
  case jsType = "typeof"
108
- case wrapped, element, key, value, parameters, returns, text
113
+ case wrapped, element, key, value, parameters, returns, text, native
109
114
  // A closure type's effects, matching `ExportedFunction`'s TS-keyword spellings.
110
115
  case isAsync = "async"
111
116
  case isThrowing = "throws"
@@ -143,6 +148,9 @@ extension TypeNode: Encodable {
143
148
  try container.encode("ref", forKey: .kind)
144
149
  try container.encode(name, forKey: .name)
145
150
  try container.encodeIfPresent(refKind, forKey: .refKind)
151
+ case .sharedRef(let native):
152
+ try container.encode("sharedRef", forKey: .kind)
153
+ try container.encode(native, forKey: .native)
146
154
  case .unknown(let text):
147
155
  try container.encode("unknown", forKey: .kind)
148
156
  try container.encode(text, forKey: .text)
@@ -201,15 +209,23 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
201
209
  }
202
210
  // `(T)`: one unlabeled element in parentheses is the same type as `T`, as in `((Int) -> Void)?`.
203
211
  if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
204
- let element = tuple.elements.first, element.firstName == nil {
212
+ let element = tuple.elements.first, element.firstName == nil
213
+ {
205
214
  return typeNode(from: element.type)
206
215
  }
207
216
  // A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
208
217
  if let identifier = type.as(IdentifierTypeSyntax.self) {
209
218
  return nominalNode(name: identifier.name.identifierName, generics: identifier.genericArgumentClause)
210
219
  }
211
- // A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling.
220
+ // A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling, except core's own
221
+ // `ExpoModulesCore.SharedRef<T>`, which is the same type as a bare `SharedRef<T>`.
212
222
  if let member = type.as(MemberTypeSyntax.self) {
223
+ if member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "ExpoModulesCore",
224
+ member.name.text == "SharedRef",
225
+ let generics = member.genericArgumentClause
226
+ {
227
+ return nominalNode(name: "SharedRef", generics: generics)
228
+ }
213
229
  return .ref(name: member.trimmedDescription)
214
230
  }
215
231
  // Tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
@@ -217,8 +233,8 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
217
233
  }
218
234
 
219
235
  /// A nominal type's node. The standard generic wrappers normalize to their sugared node
220
- /// (`Optional<T>`/`Array<T>`/`Dictionary<K,V>`/`Promise<T>`); a bare name is a primitive when known,
221
- /// else a ref; any other generic is a ref under its full spelling.
236
+ /// (`Optional<T>`/`Array<T>`/`Dictionary<K,V>`/`Promise<T>`) and `SharedRef<T>` to its own node; a
237
+ /// bare name is a primitive when known, else a ref; any other generic is a ref under its full spelling.
222
238
  private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -> TypeNode {
223
239
  guard let generics else {
224
240
  if let jsType = primitiveJSTypes[name] {
@@ -237,6 +253,8 @@ private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -
237
253
  return .dictionary(key: arguments[0], value: arguments[1])
238
254
  case ("Promise", 1):
239
255
  return .promise(value: arguments[0])
256
+ case ("SharedRef", 1):
257
+ return .sharedRef(native: arguments[0])
240
258
  default:
241
259
  // An unmodeled generic (`Set<Int>`, `Either<A, B>`, …). Keep the whole spelling as a ref so the
242
260
  // name and its arguments survive for the generator to interpret or flag.
@@ -296,6 +314,8 @@ extension TypeNode {
296
314
  return "[\(key.spelling): \(value.spelling)]"
297
315
  case .promise(let value):
298
316
  return "Promise<\(value.spelling)>"
317
+ case .sharedRef(let native):
318
+ return "SharedRef<\(native.spelling)>"
299
319
  case .function(let parameters, let returns, _, _):
300
320
  return "(\(parameters.map { $0.spelling }.joined(separator: ", "))) -> \(returns?.spelling ?? "Void")"
301
321
  case .unknown(let text):
@@ -1,9 +1,10 @@
1
+ import SwiftParser
2
+
1
3
  #if canImport(FoundationEssentials)
2
4
  import FoundationEssentials
3
5
  #else
4
6
  import Foundation
5
7
  #endif
6
- import SwiftParser
7
8
 
8
9
  /// The scanner's public entry point. Argument parsing, subcommand dispatch, and usage text live in
9
10
  /// the CLI target; this just runs a command and writes its JSON report to stdout.
@@ -122,7 +123,12 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
122
123
  var platformsByDetection: [String: [String]] = [:]
123
124
  for platform in platformUniverse {
124
125
  let configuration = ScanBuildConfiguration(platform: platform.condition, defines: defines)
125
- let visitor = DetectionVisitor(file: file, tree: tree, detectedMacros: [.expoModule], configuration: configuration)
126
+ let visitor = DetectionVisitor(
127
+ file: file,
128
+ tree: tree,
129
+ detectedMacros: [.expoModule],
130
+ configuration: configuration
131
+ )
126
132
  visitor.walk(tree)
127
133
  for detection in visitor.detections {
128
134
  platformsByDetection[detectionKey(detection), default: []].append(platform.reported)
package/build/index.d.ts CHANGED
@@ -43,7 +43,7 @@ export declare class ScannerSchemaVersionError extends Error {
43
43
  * `bin` directory.
44
44
  * - Windows and Linux: `ExpoModulesMacros.exe` or `ExpoModulesMacros` from the package for the
45
45
  * current platform and architecture (see `platformBinaryPackageName`). When that package isn't
46
- * installed, for example in this repository, it falls back to the executable that `npm run build`
46
+ * installed, for example in this repository, it falls back to the executable that `pnpm run build`
47
47
  * writes to `platforms/<platform>-<arch>`.
48
48
  *
49
49
  * The path isn't checked here; running a missing binary fails with a `ScannerError` that says how to