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.
- package/.github/workflows/linux.yml +8 -5
- package/.github/workflows/publish.yml +26 -11
- package/.github/workflows/swift-format.yml +4 -1
- package/.github/workflows/swift.yml +7 -4
- package/.github/workflows/windows.yml +8 -5
- package/AGENTS.md +12 -10
- package/README.md +7 -5
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +32 -5
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +5 -4
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +14 -27
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +4 -4
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +9 -1
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +18 -22
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +13 -33
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +39 -63
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +1 -1
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +17 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +3 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +52 -1
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +26 -7
- package/build/index.d.ts +1 -1
- package/build/index.js +3 -3
- package/build/types.d.ts +25 -1
- package/build/types.js +1 -1
- package/package.json +6 -5
|
@@ -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
|
|
50
|
-
/// methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and
|
|
51
|
-
/// for `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic
|
|
52
|
-
/// `encodeAny(_:in:)` for any other type. Such a property only needs
|
|
53
|
-
/// paths, and a bare `Any` needs nothing, since the dictionary paths
|
|
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
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
6
|
-
/// declarations marked with `@JS` and
|
|
7
|
-
///
|
|
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])`
|
|
22
|
-
///
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
5
|
-
///
|
|
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
|
|
19
|
-
///
|
|
20
|
-
///
|
|
21
|
-
|
|
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
|
|
65
|
-
/// parameter per asserted
|
|
66
|
-
///
|
|
67
|
-
///
|
|
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
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
103
|
+
constraint: String
|
|
136
104
|
) -> DeclSyntax? {
|
|
137
|
-
let bodies = assertions.compactMap { conformanceAssertionBody($0, 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
|
|
190
|
-
///
|
|
191
|
-
///
|
|
192
|
-
///
|
|
193
|
-
///
|
|
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
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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
|
|
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>`)
|
|
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 `
|
|
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 `
|
|
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 \`
|
|
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 `
|
|
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.
|