expo-modules-macros 0.21.0 → 0.22.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.
- package/.github/workflows/swift-format.yml +76 -0
- package/AGENTS.md +9 -4
- package/README.md +15 -2
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +3 -3
- package/apple/Package.swift +1 -2
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +113 -26
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +53 -45
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +180 -44
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +163 -42
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +285 -221
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +45 -32
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
- package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +15 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +127 -45
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/types.d.ts +7 -1
- package/package.json +7 -5
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Returns nil if the attribute has no arguments or the first argument is not a string literal.
|
|
6
|
-
*/
|
|
3
|
+
/// Reads the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork".
|
|
4
|
+
/// Returns nil if the attribute has no arguments or the first argument is not a string literal.
|
|
7
5
|
internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
8
6
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
9
7
|
let first = args.first,
|
|
10
8
|
let str = first.expression.as(StringLiteralExprSyntax.self),
|
|
11
|
-
let segment = str.segments.first?.as(StringSegmentSyntax.self)
|
|
9
|
+
let segment = str.segments.first?.as(StringSegmentSyntax.self)
|
|
10
|
+
else {
|
|
12
11
|
return nil
|
|
13
12
|
}
|
|
14
13
|
return segment.content.text
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
|
|
20
|
-
*/
|
|
16
|
+
/// Reads a labeled boolean-literal argument of an attribute, e.g. `@Event(sync: true)` -> true.
|
|
17
|
+
/// Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
|
|
21
18
|
internal func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool? {
|
|
22
19
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
23
20
|
return nil
|
|
@@ -44,7 +41,8 @@ internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) ->
|
|
|
44
41
|
return true
|
|
45
42
|
}
|
|
46
43
|
if let array = arg.expression.as(ArrayExprSyntax.self),
|
|
47
|
-
array.elements.contains(where: { namesOption($0.expression, option) })
|
|
44
|
+
array.elements.contains(where: { namesOption($0.expression, option) })
|
|
45
|
+
{
|
|
48
46
|
return true
|
|
49
47
|
}
|
|
50
48
|
}
|
|
@@ -69,6 +67,64 @@ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
69
67
|
return hasJSOption(attribute, named: "concurrent")
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
/// The `.withAsync("name")` option of a `@JS` attribute, e.g. `@JS(.withAsync("loadAsync"))` or
|
|
71
|
+
/// `@JS("load", [.withAsync("loadAsync")])`, or `nil` when the attribute doesn't list it. When the
|
|
72
|
+
/// option is listed more than once, the first one counts and `@JS` reports the others.
|
|
73
|
+
internal func asyncVariantOption(of attribute: AttributeSyntax) -> FunctionCallExprSyntax? {
|
|
74
|
+
return asyncVariantOptions(of: attribute).first
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Every `.withAsync(...)` option of a `@JS` attribute, in source order. Like `hasJSOption`, it scans
|
|
78
|
+
/// the arguments and the elements of an array literal.
|
|
79
|
+
internal func asyncVariantOptions(of attribute: AttributeSyntax) -> [FunctionCallExprSyntax] {
|
|
80
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
return args.flatMap { arg -> [FunctionCallExprSyntax] in
|
|
84
|
+
if let array = arg.expression.as(ArrayExprSyntax.self) {
|
|
85
|
+
return array.elements.compactMap { asyncOptionCall($0.expression) }
|
|
86
|
+
}
|
|
87
|
+
return asyncOptionCall(arg.expression).map { [$0] } ?? []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The expression as a `.withAsync(...)` call (or `JSOptions.withAsync(...)`), or `nil` for anything
|
|
92
|
+
/// else.
|
|
93
|
+
private func asyncOptionCall(_ expression: ExprSyntax) -> FunctionCallExprSyntax? {
|
|
94
|
+
guard let call = expression.as(FunctionCallExprSyntax.self),
|
|
95
|
+
let member = call.calledExpression.as(MemberAccessExprSyntax.self),
|
|
96
|
+
member.declName.baseName.text == "withAsync"
|
|
97
|
+
else {
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
return call
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The JavaScript name a `.withAsync("name")` option gives the promise-returning variant, with the
|
|
104
|
+
/// token of its string literal, where a name collision is reported. `nil` when the argument isn't a
|
|
105
|
+
/// single plain string literal, which `@JS` reports.
|
|
106
|
+
internal func asyncVariantName(of option: FunctionCallExprSyntax) -> (name: String, token: TokenSyntax)? {
|
|
107
|
+
guard option.arguments.count == 1,
|
|
108
|
+
let argument = option.arguments.first,
|
|
109
|
+
argument.label == nil,
|
|
110
|
+
let literal = argument.expression.as(StringLiteralExprSyntax.self),
|
|
111
|
+
literal.segments.count == 1,
|
|
112
|
+
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
113
|
+
else {
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
return (segment.content.text, segment.content)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// True if the `@JS`-marked declaration lists `.withAsync("name")`. Such a function is left
|
|
120
|
+
/// unstamped, because its promise-returning variant calls it on the concurrent pool.
|
|
121
|
+
internal func hasAsyncVariant(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
122
|
+
guard let attribute = memberAttributes(of: decl).firstAttribute(named: "JS") else {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
return asyncVariantOption(of: attribute) != nil
|
|
126
|
+
}
|
|
127
|
+
|
|
72
128
|
/// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
|
|
73
129
|
/// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
|
|
74
130
|
internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
@@ -129,12 +185,82 @@ internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
|
129
185
|
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
130
186
|
identifier.name.text == "Optional",
|
|
131
187
|
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
132
|
-
case .type(let wrapped) = argument.argument
|
|
188
|
+
case .type(let wrapped) = argument.argument
|
|
189
|
+
{
|
|
133
190
|
return wrapped
|
|
134
191
|
}
|
|
135
192
|
return nil
|
|
136
193
|
}
|
|
137
194
|
|
|
195
|
+
/// The element type of `[T]` or `Array<T>`, or `nil` for any other type.
|
|
196
|
+
internal func arrayElementType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
197
|
+
if let array = type.as(ArrayTypeSyntax.self) {
|
|
198
|
+
return array.element
|
|
199
|
+
}
|
|
200
|
+
guard let arguments = genericArguments(of: type, named: "Array"), arguments.count == 1 else {
|
|
201
|
+
return nil
|
|
202
|
+
}
|
|
203
|
+
return arguments[0]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// The value type of `[String: V]` or `Dictionary<String, V>`, or `nil` for any other type, including a
|
|
207
|
+
/// dictionary with a different key type.
|
|
208
|
+
internal func stringKeyedDictionaryValueType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
209
|
+
if let dictionary = type.as(DictionaryTypeSyntax.self) {
|
|
210
|
+
return isStringType(dictionary.key) ? dictionary.value : nil
|
|
211
|
+
}
|
|
212
|
+
guard let arguments = genericArguments(of: type, named: "Dictionary"), arguments.count == 2 else {
|
|
213
|
+
return nil
|
|
214
|
+
}
|
|
215
|
+
return isStringType(arguments[0]) ? arguments[1] : nil
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// The element type of `Set<T>`, or `nil` for any other type.
|
|
219
|
+
internal func setElementType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
220
|
+
guard let arguments = genericArguments(of: type, named: "Set"), arguments.count == 1 else {
|
|
221
|
+
return nil
|
|
222
|
+
}
|
|
223
|
+
return arguments[0]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// The type arguments of a generic type spelled `<name><…>` (or `Swift.<name><…>`), or `nil` when the
|
|
227
|
+
/// type has another name or no type arguments.
|
|
228
|
+
private func genericArguments(of type: TypeSyntax, named name: String) -> [TypeSyntax]? {
|
|
229
|
+
let clause: GenericArgumentClauseSyntax?
|
|
230
|
+
if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == name {
|
|
231
|
+
clause = identifier.genericArgumentClause
|
|
232
|
+
} else if let member = type.as(MemberTypeSyntax.self),
|
|
233
|
+
member.name.text == name,
|
|
234
|
+
member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
|
|
235
|
+
{
|
|
236
|
+
clause = member.genericArgumentClause
|
|
237
|
+
} else {
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
guard let clause else {
|
|
241
|
+
return nil
|
|
242
|
+
}
|
|
243
|
+
var types: [TypeSyntax] = []
|
|
244
|
+
for argument in clause.arguments {
|
|
245
|
+
guard case .type(let type) = argument.argument else {
|
|
246
|
+
return nil
|
|
247
|
+
}
|
|
248
|
+
types.append(type)
|
|
249
|
+
}
|
|
250
|
+
return types
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// True when the type is `String` or `Swift.String`.
|
|
254
|
+
private func isStringType(_ type: TypeSyntax) -> Bool {
|
|
255
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
256
|
+
return identifier.name.text == "String" && identifier.genericArgumentClause == nil
|
|
257
|
+
}
|
|
258
|
+
if let member = type.as(MemberTypeSyntax.self), member.name.text == "String" {
|
|
259
|
+
return member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
|
|
260
|
+
}
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
138
264
|
/// True when the modifiers make the member type-level (`static` or `class`). A shared object routes
|
|
139
265
|
/// such members to its constructor (JS-static) and instance members to its prototype; the `@JS`
|
|
140
266
|
/// conformance assertion also uses this to emit its peer in the matching metatype context.
|
|
@@ -158,26 +284,23 @@ internal func hasDefaultValue(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
|
158
284
|
return parameter.defaultValue != nil
|
|
159
285
|
}
|
|
160
286
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
JS thread
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
they stay callable from any thread.
|
|
167
|
-
*/
|
|
287
|
+
/// True if the declaration is a `@Event(sync: true)` property. A sync event dispatches inline on the
|
|
288
|
+
/// JS thread instead of scheduling, so `@ExpoModule`/`@SharedObject` stamp it with `@JavaScriptActor`,
|
|
289
|
+
/// making "must be called on the JS thread" a compile-time guarantee at the call site. Async events
|
|
290
|
+
/// (the default) are deliberately not stamped: their `emit` schedules onto the JS thread itself, so
|
|
291
|
+
/// they stay callable from any thread.
|
|
168
292
|
internal func isSyncEventMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
169
293
|
guard let varDecl = decl.as(VariableDeclSyntax.self),
|
|
170
|
-
let attribute = varDecl.attributes.firstAttribute(named: "Event")
|
|
294
|
+
let attribute = varDecl.attributes.firstAttribute(named: "Event")
|
|
295
|
+
else {
|
|
171
296
|
return false
|
|
172
297
|
}
|
|
173
298
|
return boolArgument(of: attribute, label: "sync") == true
|
|
174
299
|
}
|
|
175
300
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
|
|
180
|
-
*/
|
|
301
|
+
/// Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
|
|
302
|
+
/// and returns the type names referenced (e.g. `["Foo", "Bar"]`). Each element must be a
|
|
303
|
+
/// `<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
|
|
181
304
|
internal func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
|
|
182
305
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
183
306
|
return []
|
|
@@ -189,7 +312,8 @@ internal func classListArgument(of attribute: AttributeSyntax, label: String) ->
|
|
|
189
312
|
return array.elements.compactMap { element -> String? in
|
|
190
313
|
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
191
314
|
memberAccess.declName.baseName.text == "self",
|
|
192
|
-
let base = memberAccess.base
|
|
315
|
+
let base = memberAccess.base
|
|
316
|
+
else {
|
|
193
317
|
return nil
|
|
194
318
|
}
|
|
195
319
|
// A bare `CardView.self` has a `DeclReferenceExprSyntax` base; a qualified
|
|
@@ -205,11 +329,9 @@ internal func classListArgument(of attribute: AttributeSyntax, label: String) ->
|
|
|
205
329
|
return []
|
|
206
330
|
}
|
|
207
331
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`Module<Foo>` all match an entry of "Module".
|
|
212
|
-
*/
|
|
332
|
+
/// Returns true if the class's inheritance clause names any of the given identifiers.
|
|
333
|
+
/// Matches by base identifier only, so `Module`, `ExpoModulesCore.Module`, and
|
|
334
|
+
/// `Module<Foo>` all match an entry of "Module".
|
|
213
335
|
internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>) -> Bool {
|
|
214
336
|
guard let inherited = classDecl.inheritanceClause?.inheritedTypes else {
|
|
215
337
|
return false
|
|
@@ -222,10 +344,8 @@ internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>)
|
|
|
222
344
|
return false
|
|
223
345
|
}
|
|
224
346
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
`Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
|
|
228
|
-
*/
|
|
347
|
+
/// Returns the rightmost identifier of a type, e.g. `Foo` for `Foo`, `Foo` for
|
|
348
|
+
/// `Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
|
|
229
349
|
internal func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
230
350
|
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
231
351
|
return identifier.name.text
|
|
@@ -236,10 +356,8 @@ internal func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
|
236
356
|
return nil
|
|
237
357
|
}
|
|
238
358
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
returns false for any other decl kind.
|
|
242
|
-
*/
|
|
359
|
+
/// True if the declaration carries a `@JS` attribute. Works for functions, properties, and inits;
|
|
360
|
+
/// returns false for any other decl kind.
|
|
243
361
|
internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
244
362
|
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
245
363
|
return funcDecl.attributes.firstAttribute(named: "JS") != nil
|
|
@@ -256,6 +374,7 @@ internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
256
374
|
/// Decides whether the macro should stamp `@JavaScriptActor` on a `@JS`-marked member.
|
|
257
375
|
/// The macro defers to the user when they've already chosen an isolation:
|
|
258
376
|
/// - the member opted out with `@JS(.concurrent)`
|
|
377
|
+
/// - the member lists `@JS(.withAsync("name"))`, whose variant runs it on the concurrent pool
|
|
259
378
|
/// - the `nonisolated` modifier is present on the member
|
|
260
379
|
/// - any attribute whose name matches a known global actor (`@MainActor`, `@JavaScriptActor`)
|
|
261
380
|
/// or follows the `*Actor` naming convention is present on the member or its enclosing type
|
|
@@ -265,7 +384,7 @@ internal func shouldStampJavaScriptActor(
|
|
|
265
384
|
on member: DeclSyntaxProtocol,
|
|
266
385
|
enclosedBy enclosing: some DeclGroupSyntax
|
|
267
386
|
) -> Bool {
|
|
268
|
-
if isConcurrentJSMember(member) {
|
|
387
|
+
if isConcurrentJSMember(member) || hasAsyncVariant(member) {
|
|
269
388
|
return false
|
|
270
389
|
}
|
|
271
390
|
|
|
@@ -312,7 +431,7 @@ internal func memberAttributes(of decl: DeclSyntaxProtocol) -> AttributeListSynt
|
|
|
312
431
|
return AttributeListSyntax()
|
|
313
432
|
}
|
|
314
433
|
|
|
315
|
-
|
|
434
|
+
internal func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool {
|
|
316
435
|
guard let attribute = element.as(AttributeSyntax.self) else {
|
|
317
436
|
return false
|
|
318
437
|
}
|
|
@@ -345,7 +464,8 @@ extension AttributeListSyntax {
|
|
|
345
464
|
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
346
465
|
for element in self {
|
|
347
466
|
if let attr = element.as(AttributeSyntax.self),
|
|
348
|
-
attr.attributeName.trimmedDescription == name
|
|
467
|
+
attr.attributeName.trimmedDescription == name
|
|
468
|
+
{
|
|
349
469
|
return attr
|
|
350
470
|
}
|
|
351
471
|
}
|
|
@@ -433,7 +553,8 @@ internal func collectProperties(
|
|
|
433
553
|
let swiftName = ident.identifier.text
|
|
434
554
|
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
435
555
|
// when there's none. `nil` falls back to inference at the use site.
|
|
436
|
-
let valueType =
|
|
556
|
+
let valueType =
|
|
557
|
+
binding.typeAnnotation?.type.trimmedDescription
|
|
437
558
|
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
438
559
|
return JSProperty(
|
|
439
560
|
swiftName: swiftName,
|