expo-modules-macros 0.22.0 → 0.23.1

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.
@@ -43,14 +43,17 @@ import SwiftSyntaxMacros
43
43
  /// (`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
44
44
  /// values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
45
45
  /// user modules without any internal core symbols. Every property type must therefore conform to both
46
- /// `AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`.
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.
47
49
  ///
48
50
  /// A property whose type mentions `Any` (`Any?`, `[String: Any]?`, `[String: [Any]]`, …) can't conform to
49
- /// the JS codable protocols. On the JS-value paths it converts through the `JavaScriptValue` free-form
50
- /// methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and the matching `encodeAny…`
51
- /// for `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic `decodeAny(_:as:in:)` /
52
- /// `encodeAny(_:in:)` for any other type. Such a property only needs `AnyArgument` for the dictionary
53
- /// paths, and a bare `Any` needs nothing, since the dictionary paths pass it through unchanged.
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.
54
57
  ///
55
58
  /// A record class must be `final` and have no inheritance clause. The synthesized factories construct
56
59
  /// `Self` through the memberwise init, which only a final class can do without a `required` init, and
@@ -87,23 +90,16 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
87
90
 
88
91
  var members: [DeclSyntax] = []
89
92
 
90
- // A single never-called member that makes the compiler verify each property type is convertible
91
- // both ways (the JS-value paths call `decode`/`encode`; the native-`Any` dictionary paths call
92
- // the dynamic-type API). Each property keeps its own named assertion inside, so the compiler's
93
- // conformance diagnostic names the offending property (see `typeConformanceAssertions`). Emitted
94
- // first so that, for a non-conforming type, this clear "requires that '…' conform to '…'" error
95
- // is reported ahead of the noisier "no member 'decode'"/"getDynamicType" errors from the
96
- // conversion code below.
93
+ // A single never-called member that makes the compiler verify each property type converts both
94
+ // ways (the JS-value paths call `decode`/`encode`). Each property keeps its own named assertion
95
+ // inside, so the compiler's conformance diagnostic names the offending property (see
96
+ // `typeConformanceAssertions`). Emitted first so that, for a non-conforming type, this clear
97
+ // "requires that '…' conform to '…'" error is reported ahead of the noisier "no member 'decode'"
98
+ // errors from the conversion code below.
97
99
  let assertions = properties.map { property in
98
- // A bare `Any` can't conform to anything and needs no conformance, since every path converts it
99
- // without one.
100
- ConformanceAssertion(
101
- name: property.name,
102
- types: property.freeForm?.isBareAny == true ? [] : [property.type],
103
- constraint: property.freeForm != nil ? jsConvertibleProtocolName : nil
104
- )
100
+ ConformanceAssertion(name: property.name, types: property.freeForm == nil ? [property.type] : [])
105
101
  }
106
- if let assertionMember = typeConformanceAssertions(for: assertions, constraint: recordFieldProtocolName) {
102
+ if let assertionMember = typeConformanceAssertions(for: assertions, constraint: javaScriptCodableProtocolName) {
107
103
  members.append(assertionMember)
108
104
  }
109
105
 
@@ -499,7 +495,7 @@ private func javaScriptCodableWitnesses(
499
495
  [" let object = runtime.createObject()"]
500
496
  + lookupLines
501
497
  + objectWriteLines(properties: properties, of: "value", cachesPropNames: cachesPropNames)
502
- + [" return object.asValue()"]
498
+ + [" return object.takeValue()"]
503
499
  return [
504
500
  """
505
501
  @JavaScriptActor
@@ -2,9 +2,10 @@ import SwiftSyntax
2
2
  import SwiftSyntaxBuilder
3
3
  import SwiftSyntaxMacros
4
4
 
5
- /// Member macro applied to a `SharedObject` subclass. Scans the class body for
6
- /// declarations marked with `@JS` and synthesizes `_synthesizedClassDefinition()`,
7
- /// returning a `ClassDefinition` ready to drop into a module's `Class { ... }` slot.
5
+ /// Member macro applied to a `SharedObject` subclass, direct or not (a `SharedRef<T>` subclass too).
6
+ /// Scans the class body for declarations marked with `@JS` and binds them directly through overrides of
7
+ /// the `SharedObject` hooks core calls when it builds the JavaScript class. The class's JavaScript name
8
+ /// is emitted as a `_jsName` override.
8
9
  ///
9
10
  /// @SharedObject
10
11
  /// final class Cache: SharedObject {
@@ -18,8 +19,8 @@ import SwiftSyntaxMacros
18
19
  /// var size: Int { 42 }
19
20
  /// }
20
21
  ///
21
- /// The companion `@ExpoModule(classes: [Cache.self])` wires the resulting
22
- /// definition into the module's exposed surface.
22
+ /// The companion `@ExpoModule(classes: [Cache.self])` registers the class with the module, which
23
+ /// exposes it in its JavaScript object.
23
24
  public struct SharedObjectMacro: MemberMacro {
24
25
  public static func expansion(
25
26
  of node: AttributeSyntax,
@@ -31,26 +32,19 @@ public struct SharedObjectMacro: MemberMacro {
31
32
  throw MacroExpansionErrorMessage("@SharedObject can only be applied to a class")
32
33
  }
33
34
 
34
- guard inheritsFromSharedObject(classDecl) else {
35
- throw MacroExpansionErrorMessage(
36
- "@SharedObject class must inherit from SharedObject. Add `: SharedObject` to the class declaration."
37
- )
38
- }
39
-
35
+ // The superclass isn't checked here: the macro sees only this declaration, so it can't tell an
36
+ // indirect subclass (`class Thumbnail: ImageRef`) from an unrelated class. The compiler reports a
37
+ // class that isn't a `SharedObject` subclass on the `_jsName` override, which is always emitted.
40
38
  let typeName = classDecl.name.text
41
39
  let jsName = jsNameArgument(of: node) ?? withoutBackticks(typeName)
42
40
 
43
41
  // `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
44
- // the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`
45
- // rather than described with a `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so
46
- // they're collected here instead of appended to the `Class` block. The block keeps only non-`@JS`
47
- // definitions (none are collected today), so it's empty when every member is `@JS`.
42
+ // the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`.
48
43
  //
49
44
  // Members split by the `static`/`class` modifier onto two different JS objects: instance members
50
45
  // decorate the prototype (receiver recovered from JS `this`), static members decorate the
51
46
  // constructor (called on the metatype). A JS instance and static member may share a name without
52
47
  // colliding: they live on different objects.
53
- let entries: [String] = []
54
48
  var instanceFunctions: [JSFunction] = []
55
49
  var instanceProperties: [JSProperty] = []
56
50
  var staticFunctions: [JSFunction] = []
@@ -100,18 +94,10 @@ public struct SharedObjectMacro: MemberMacro {
100
94
  diagnoseJSNameCollisions(functions: instanceFunctions, properties: instanceProperties, in: context)
101
95
  diagnoseJSNameCollisions(functions: staticFunctions, properties: staticProperties, in: context)
102
96
 
103
- let lines = entries.map { " \($0)" }.joined(separator: "\n")
104
- let body =
105
- entries.isEmpty
106
- ? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
107
- : " return Class(\"\(jsName)\", \(typeName).self) {\n\(lines)\n }"
108
-
97
+ // The resolved JS class name, read by core when it builds the class. An `override` of the
98
+ // `SharedObject` class property, so core reads it through the concrete type's metatype.
109
99
  var emitted: [DeclSyntax] = [
110
- """
111
- public static func _synthesizedClassDefinition() -> ClassDefinition {
112
- \(raw: body)
113
- }
114
- """
100
+ "public override class var _jsName: String { \"\(raw: jsName)\" }"
115
101
  ]
116
102
 
117
103
  // Direct JSI binding, split by which JS object each member decorates:
@@ -170,9 +156,3 @@ extension SharedObjectMacro: MemberAttributeMacro {
170
156
  return ["@JavaScriptActor"]
171
157
  }
172
158
  }
173
-
174
- // MARK: - Inheritance check
175
-
176
- private func inheritsFromSharedObject(_ classDecl: ClassDeclSyntax) -> Bool {
177
- return inheritsFromAny(classDecl, names: ["SharedObject"])
178
- }
@@ -1,9 +1,8 @@
1
1
  import SwiftSyntax
2
2
  import SwiftSyntaxBuilder
3
3
 
4
- /// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
5
- /// eventual rename (this is a placeholder name) is a single edit, and shared by the macros whose
6
- /// generated conversions go through the dynamic-type API (`@Record`, `@Event`).
4
+ /// The protocol of the 1.0 dynamic-type API (`getDynamicType()`). `@Event` asserts payload types
5
+ /// against it.
7
6
  internal let jsConvertibleProtocolName = "AnyArgument"
8
7
 
9
8
  /// The constraint a `@JS` boundary type must satisfy, by direction: an argument (and a settable
@@ -15,23 +14,10 @@ internal let jsConvertibleProtocolName = "AnyArgument"
15
14
  internal let javaScriptDecodableProtocolName = "JavaScriptDecodable"
16
15
  internal let javaScriptEncodableProtocolName = "JavaScriptEncodable"
17
16
 
18
- /// The constraint a `@Record` property type must satisfy: it must be both `AnyArgument` (the
19
- /// `from(dictionary:)` / `toDictionary(appContext:)` paths still convert native `Any` through the
20
- /// dynamic-type API) and `JavaScriptDecodable & JavaScriptEncodable` (the `from(object:)` /
21
- /// `toObject(appContext:)` paths convert JS values through `decode`/`encode`). A record field has to
22
- /// support both directions, so the assertion requires the intersection.
23
- ///
24
- /// The exception is a property whose type mentions `Any` (`[String: Any]?`, `[Any]`, …): it can't
25
- /// conform to the JS codable protocols, so the JS-value paths convert it through the `JavaScriptValue`
26
- /// free-form methods, and it's asserted against `jsConvertibleProtocolName` alone for the dictionary
27
- /// paths.
28
- internal let recordFieldProtocolName = "AnyArgument & JavaScriptDecodable & JavaScriptEncodable"
29
-
30
- /// The constraint a `@Union` case payload type must satisfy: the union decodes by trying each payload's
31
- /// `decode` and encodes through the matching payload's `encode`, so every alternative must convert in
32
- /// both directions. Unlike a `@Record` field, a union never crosses the native-`Any` dictionary path, so
33
- /// `AnyArgument` is not required.
34
- internal let unionPayloadProtocolName = "JavaScriptDecodable & JavaScriptEncodable"
17
+ /// The constraint a `@Record` property type and a `@Union` case payload type must satisfy. A record
18
+ /// decodes and encodes each property, and a union decodes by trying each payload's `decode` and
19
+ /// encodes through the matching payload's `encode`, so both convert in both directions.
20
+ internal let javaScriptCodableProtocolName = "JavaScriptDecodable & JavaScriptEncodable"
35
21
 
36
22
  /// The protocol a type must conform to for `self.emit(event:…)` to resolve; core conforms
37
23
  /// `BaseModule` and `SharedObject` to it. Asserted by `@Event` so attaching it to a type that can't
@@ -52,20 +38,15 @@ internal struct ConformanceAssertion {
52
38
  /// Declared types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
53
39
  /// conditional conformances transitively constrain the elements.
54
40
  let types: [String]
55
- /// The constraint for this member's types, overriding the one passed to `typeConformanceAssertions`.
56
- /// `@Record` uses it to assert only `AnyArgument` for a property that mentions `Any`, which can't be
57
- /// `JavaScriptDecodable & JavaScriptEncodable`.
58
- var constraint: String? = nil
59
41
  }
60
42
 
61
43
  /// A directional conformance-assertion peer for a `@JS` member: a never-called `private func` whose
62
44
  /// body asserts each argument type is `JavaScriptDecodable` and the return/getter type is
63
45
  /// `JavaScriptEncodable`, matching how the generated binding converts each (decode on the way in,
64
- /// encode on the way out). The check is a single member-named nested helper with one `A0…` generic
65
- /// parameter per asserted argument and a single `Return` parameter for the value, called once with
66
- /// every type's metatype; naming the helper after the member puts the member in the compiler's
67
- /// diagnostic, and the per-slot constraint reports the offending type against the protocol for *its*
68
- /// direction.
46
+ /// encode on the way out). The check is a single member-named nested helper with one opaque metatype
47
+ /// parameter per asserted type, called once with every type's metatype; naming the helper after the
48
+ /// member puts the member in the compiler's diagnostic, and the per-parameter constraint reports the
49
+ /// offending type against the protocol for *its* direction.
69
50
  ///
70
51
  /// `decodableTypes` are the argument types (and a settable property's value type); `encodableType` is
71
52
  /// the single return type (or a property's value type on read), or `nil` when there's none.
@@ -87,31 +68,18 @@ internal func directionalConformanceAssertion(
87
68
  return nil
88
69
  }
89
70
 
90
- // Build the generic parameter list and the matching call arguments in lockstep: one `A0…` slot
91
- // constrained `JavaScriptDecodable` per argument, one `E0…` slot constrained `JavaScriptEncodable`
92
- // per closure-argument parameter, and a single `Return` slot constrained `JavaScriptEncodable` for
93
- // the return/getter value (there's only ever one, so it isn't indexed). Every slot suppresses
94
- // `Copyable`, so a non-copyable boundary type such as `JavaScriptValue` can fill it.
95
- var parameters: [String] = []
96
- var typeParameters: [String] = []
97
- var arguments: [String] = []
98
- for (index, type) in decodables.enumerated() {
99
- parameters.append("A\(index): \(javaScriptDecodableProtocolName) & ~Copyable")
100
- typeParameters.append("_: A\(index).Type")
101
- arguments.append("\(type).self")
102
- }
103
- for (index, type) in callbackArguments.enumerated() {
104
- parameters.append("E\(index): \(javaScriptEncodableProtocolName) & ~Copyable")
105
- typeParameters.append("_: E\(index).Type")
106
- arguments.append("\(type).self")
107
- }
108
- if let encodable {
109
- parameters.append("Return: \(javaScriptEncodableProtocolName) & ~Copyable")
110
- typeParameters.append("_: Return.Type")
111
- arguments.append("\(encodable).self")
112
- }
113
-
114
- let helper = "func \(name)<\(parameters.joined(separator: ", "))>(\(typeParameters.joined(separator: ", "))) {}"
71
+ // Arguments are constrained `JavaScriptDecodable`, and closure-argument parameters and the
72
+ // return/getter value `JavaScriptEncodable`. Every parameter suppresses `Copyable`, so a
73
+ // non-copyable boundary type such as `JavaScriptValue` can fill it.
74
+ let encodables = callbackArguments + [encodable].compactMap { $0 }
75
+ let decodableParameter = opaqueMetatypeParameter(constraint: "\(javaScriptDecodableProtocolName) & ~Copyable")
76
+ let encodableParameter = opaqueMetatypeParameter(constraint: "\(javaScriptEncodableProtocolName) & ~Copyable")
77
+ let parameters =
78
+ Array(repeating: decodableParameter, count: decodables.count)
79
+ + Array(repeating: encodableParameter, count: encodables.count)
80
+ let arguments = (decodables + encodables).map { "\($0).self" }
81
+
82
+ let helper = "func \(name)(\(parameters.joined(separator: ", "))) {}"
115
83
  let call = "\(name)(\(arguments.joined(separator: ", ")))"
116
84
  let staticKeyword = isStatic ? "static " : ""
117
85
  let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
@@ -132,9 +100,9 @@ internal func directionalConformanceAssertion(
132
100
  /// caller emits nothing.
133
101
  internal func typeConformanceAssertions(
134
102
  for assertions: [ConformanceAssertion],
135
- constraint: String = jsConvertibleProtocolName
103
+ constraint: String
136
104
  ) -> DeclSyntax? {
137
- let bodies = assertions.compactMap { conformanceAssertionBody($0, constraint: $0.constraint ?? constraint) }
105
+ let bodies = assertions.compactMap { conformanceAssertionBody($0, constraint: constraint) }
138
106
  guard !bodies.isEmpty else {
139
107
  return nil
140
108
  }
@@ -186,25 +154,33 @@ private func setsConform(to constraint: String) -> Bool {
186
154
  }
187
155
  }
188
156
 
189
- /// The assertion's body fragment: a nested generic helper named after the member, plus one call per
190
- /// distinct type that needs an assertion. Nesting the helper keeps the constraint entirely local — no
191
- /// shared symbol, nothing to collide, nothing left in the type's namespace — and naming it after the
192
- /// member puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every
193
- /// type was known to conform or the list was empty.
157
+ /// The assertion's body fragment: a nested helper named after the member, plus one call per distinct
158
+ /// type that needs an assertion. Nesting the helper keeps the constraint entirely local — no shared
159
+ /// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
160
+ /// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
161
+ /// known to conform or the list was empty.
194
162
  private func conformanceAssertionBody(
195
163
  _ assertion: ConformanceAssertion,
196
- constraint: String = jsConvertibleProtocolName
164
+ constraint: String
197
165
  ) -> String? {
198
166
  let distinct = distinctAssertableTypes(assertion.types, constraint: constraint)
199
167
  guard !distinct.isEmpty else {
200
168
  return nil
201
169
  }
202
170
 
203
- var lines = ["func \(assertion.name)<T: \(constraint)>(_: T.Type) {}"]
171
+ var lines = ["func \(assertion.name)(\(opaqueMetatypeParameter(constraint: constraint))) {}"]
204
172
  lines.append(contentsOf: distinct.map { "\(assertion.name)(\($0).self)" })
205
173
  return lines.joined(separator: "\n")
206
174
  }
207
175
 
176
+ /// An unlabeled parameter that takes the metatype of any type satisfying `constraint`, for an
177
+ /// assertion helper.
178
+ internal func opaqueMetatypeParameter(constraint: String) -> String {
179
+ // A named generic parameter would shadow a generic parameter of the same name on an enclosing
180
+ // type, which is an error. An opaque parameter declares no name.
181
+ return "_: (some \(constraint)).Type"
182
+ }
183
+
208
184
  /// The distinct types to assert against `constraint` from a list, mapping each through
209
185
  /// `assertableBoundaryType` (which drops what needs no assertion) and deduping in first-seen order.
210
186
  private func distinctAssertableTypes(_ types: [String], constraint: String) -> [String] {
@@ -57,7 +57,7 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
57
57
  // reports the clear "requires that '…' conform to …" error ahead of the noisier "no member
58
58
  // 'decode'"/"'encode'" errors from the conversion code below.
59
59
  let assertions = union.cases.map { ConformanceAssertion(name: $0.name, types: [$0.payloadType]) }
60
- if let assertionMember = typeConformanceAssertions(for: assertions, constraint: unionPayloadProtocolName) {
60
+ if let assertionMember = typeConformanceAssertions(for: assertions, constraint: javaScriptCodableProtocolName) {
61
61
  members.append(assertionMember)
62
62
  }
63
63
 
@@ -293,6 +293,9 @@ struct ExportedSharedObject: Encodable, Equatable {
293
293
  /// if a malformed source has more. Not named `constructor`, which every JS object inherits.
294
294
  var initializer: ExportedInitializer?
295
295
 
296
+ /// What the class inherits from `SharedRef<T>`, or `nil` when its superclass isn't `SharedRef`.
297
+ let sharedRef: ExportedSharedRef?
298
+
296
299
  var functions: [ExportedFunction]
297
300
  var properties: [ExportedProperty]
298
301
  var events: [ExportedEvent]
@@ -305,6 +308,17 @@ struct ExportedSharedObject: Encodable, Equatable {
305
308
  let file: String
306
309
  }
307
310
 
311
+ /// The `SharedRef<T>` superclass of a shared object. Only the superclass written on the class is read,
312
+ /// so a class that inherits `SharedRef` through another subclass has none.
313
+ struct ExportedSharedRef: Encodable, Equatable {
314
+ /// The `T` in `SharedRef<T>`: the native type the object holds, which JS never sees.
315
+ let native: TypeNode
316
+
317
+ /// The `nativeRefType` JS reads to tell refs apart, for example `image`. `unknown` (core's default)
318
+ /// when the class doesn't override it, and `nil` when the override isn't a plain string literal.
319
+ let nativeRefType: String?
320
+ }
321
+
308
322
  /// A `@Record` type and its properties (data only: no functions, accessors, or constructor).
309
323
  struct ExportedRecord: Encodable, Equatable {
310
324
  /// The Swift type name (struct or class), qualified with the types it is nested in
@@ -448,8 +462,9 @@ struct ExportedSurface: Encodable, Equatable {
448
462
  /// an enum ref's `typeof`; version 6 added `classes` to modules; version 7 reports nested types under
449
463
  /// qualified names and gives a resolved ref the name of its declaration; version 8 reports
450
464
  /// declarations in `#if` blocks, adds `platforms` (with deprecations), `accessLevel`, and
451
- /// `defaultValue`, and replaces `constructorParameters` with `initializer`.
452
- let scanExportsSchemaVersion = 8
465
+ /// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
466
+ /// `sharedRef` type kind and the `sharedRef` of a shared object.
467
+ let scanExportsSchemaVersion = 9
453
468
 
454
469
  /// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
455
470
  /// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
@@ -112,6 +112,9 @@ extension TypeNode {
112
112
  switch self {
113
113
  case .primitive, .unknown:
114
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
115
118
  case .ref(let name, _, _):
116
119
  guard let entry = index.lookup(name, in: scope) else {
117
120
  return self
@@ -170,6 +170,7 @@ final class SurfaceVisitor: SyntaxVisitor {
170
170
  name: qualified,
171
171
  jsName: stringArgument(of: attribute) ?? name,
172
172
  initializer: initializer,
173
+ sharedRef: sharedRefSuperclass(of: node),
173
174
  functions: functions,
174
175
  properties: properties,
175
176
  events: events,
@@ -348,6 +349,8 @@ final class SurfaceVisitor: SyntaxVisitor {
348
349
  platforms: [ExportedPlatform]
349
350
  ) -> [ExportedProperty] {
350
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) }
351
354
  let isStatic = isTypeLevel(varDecl.modifiers)
352
355
  let override = stringArgument(of: attribute)
353
356
  var result: [ExportedProperty] = []
@@ -363,7 +366,7 @@ final class SurfaceVisitor: SyntaxVisitor {
363
366
  name: name,
364
367
  jsName: override ?? name,
365
368
  type: type,
366
- isSettable: isSettable(binding: binding, isLet: isLet),
369
+ isSettable: !isLazy && isSettable(binding: binding, isLet: isLet),
367
370
  isStatic: isStatic,
368
371
  // Only a stored property has an initializer; a computed one has none to report.
369
372
  defaultValue: binding.initializer?.value.trimmedDescription,
@@ -741,6 +744,54 @@ private func asyncVariantName(of attribute: AttributeSyntax) -> String? {
741
744
  return nil
742
745
  }
743
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
+
744
795
  private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
745
796
  modifiers.contains {
746
797
  $0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
@@ -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)
@@ -209,8 +217,15 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
209
217
  if let identifier = type.as(IdentifierTypeSyntax.self) {
210
218
  return nominalNode(name: identifier.name.identifierName, generics: identifier.genericArgumentClause)
211
219
  }
212
- // 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>`.
213
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
+ }
214
229
  return .ref(name: member.trimmedDescription)
215
230
  }
216
231
  // Tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
@@ -218,8 +233,8 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
218
233
  }
219
234
 
220
235
  /// A nominal type's node. The standard generic wrappers normalize to their sugared node
221
- /// (`Optional<T>`/`Array<T>`/`Dictionary<K,V>`/`Promise<T>`); a bare name is a primitive when known,
222
- /// 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.
223
238
  private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -> TypeNode {
224
239
  guard let generics else {
225
240
  if let jsType = primitiveJSTypes[name] {
@@ -238,6 +253,8 @@ private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -
238
253
  return .dictionary(key: arguments[0], value: arguments[1])
239
254
  case ("Promise", 1):
240
255
  return .promise(value: arguments[0])
256
+ case ("SharedRef", 1):
257
+ return .sharedRef(native: arguments[0])
241
258
  default:
242
259
  // An unmodeled generic (`Set<Int>`, `Either<A, B>`, …). Keep the whole spelling as a ref so the
243
260
  // name and its arguments survive for the generator to interpret or flag.
@@ -297,6 +314,8 @@ extension TypeNode {
297
314
  return "[\(key.spelling): \(value.spelling)]"
298
315
  case .promise(let value):
299
316
  return "Promise<\(value.spelling)>"
317
+ case .sharedRef(let native):
318
+ return "SharedRef<\(native.spelling)>"
300
319
  case .function(let parameters, let returns, _, _):
301
320
  return "(\(parameters.map { $0.spelling }.joined(separator: ", "))) -> \(returns?.spelling ?? "Void")"
302
321
  case .unknown(let text):
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
package/build/index.js CHANGED
@@ -75,7 +75,7 @@ function platformBinaryPackageName() {
75
75
  * `bin` directory.
76
76
  * - Windows and Linux: `ExpoModulesMacros.exe` or `ExpoModulesMacros` from the package for the
77
77
  * current platform and architecture (see `platformBinaryPackageName`). When that package isn't
78
- * installed, for example in this repository, it falls back to the executable that `npm run build`
78
+ * installed, for example in this repository, it falls back to the executable that `pnpm run build`
79
79
  * writes to `platforms/<platform>-<arch>`.
80
80
  *
81
81
  * The path isn't checked here; running a missing binary fails with a `ScannerError` that says how to
@@ -97,7 +97,7 @@ function getScannerBinaryPath() {
97
97
  /** The message for a scanner binary that doesn't exist at `binaryPath`. */
98
98
  function missingBinaryMessage(binaryPath) {
99
99
  if (process.platform === 'darwin') {
100
- return `The scanner binary is missing at ${binaryPath}. Run \`npm run build\` in this package to build it.`;
100
+ return `The scanner binary is missing at ${binaryPath}. Run \`pnpm run build\` in this package to build it.`;
101
101
  }
102
102
  const machines = platformPackageMachines[process.platform];
103
103
  if (machines === undefined) {
@@ -106,7 +106,7 @@ function missingBinaryMessage(binaryPath) {
106
106
  return (`The scanner binary is missing at ${binaryPath}. It comes from the optional package ` +
107
107
  `${platformBinaryPackageName()}, which your package manager installs only on ${machines}. ` +
108
108
  'Reinstall dependencies without skipping optional ones (for example without `--omit=optional`), ' +
109
- 'or run `npm run build` in this package to build it locally.');
109
+ 'or run `pnpm run build` in this package to build it locally.');
110
110
  }
111
111
  /**
112
112
  * Runs a scanner subcommand and parses its JSON report.