expo-modules-macros 0.27.0 → 0.28.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/README.md +10 -4
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +21 -4
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +96 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +3 -2
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +42 -11
- package/apple/Sources/ExpoModulesMacros/Macros/ExpoModuleMacro.swift +12 -6
- package/apple/Sources/ExpoModulesMacros/Macros/RecordMacro.swift +29 -71
- package/apple/Sources/ExpoModulesMacros/Macros/SharedObjectMacro.swift +30 -3
- package/apple/Sources/ExpoModulesMacros/Macros/UnionMacro.swift +6 -4
- package/apple/Sources/ExpoModulesMacros/ProvidedMacros.swift +2 -2
- package/apple/Sources/ExpoModulesMacros/Support/DecorateModuleBuilder.swift +52 -12
- package/apple/Sources/ExpoModulesMacros/Support/JSConstructor.swift +15 -2
- package/apple/Sources/ExpoModulesMacros/Support/MacroHelpers.swift +1 -2
- package/apple/Sources/ExpoModulesMacros/Support/SendableConformance.swift +36 -0
- package/build/types.d.ts +11 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -24,13 +24,15 @@ This package lists them as optional dependencies, and their `os`, `cpu` and (on
|
|
|
24
24
|
|
|
25
25
|
# Macros
|
|
26
26
|
|
|
27
|
-
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none.
|
|
28
|
-
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. The macro also checks that every type crossing the boundary is convertible in the direction it travels. Outside a module or shared object, a bad type is reported on the author's own declaration. In an `@ExpoModule` or `@SharedObject` class, the class macro makes the check, since only it sees the type aliases in the class body, so the error is reported on the class and names the member and its argument labels, such as `local function 'move(_:by:returning:)' requires that 'Point' conform to 'JavaScriptEncodable'`. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol
|
|
27
|
+
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none. It adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet (see [Sendable conformance](#sendable-conformance)).
|
|
28
|
+
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. The macro also checks that every type crossing the boundary is convertible in the direction it travels. Outside a module or shared object, a bad type is reported on the author's own declaration. In an `@ExpoModule` or `@SharedObject` class, the class macro makes the check, since only it sees the type aliases in the class body, so the error is reported on the class and names the member and its argument labels, such as `local function 'move(_:by:returning:)' requires that 'Point' conform to 'JavaScriptEncodable'`. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol, the error points into the generated code, so mark the function `nonisolated`. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one.
|
|
29
29
|
- **`@Event(_ name: String? = nil, sync: Bool = false)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. `sync: true` dispatches inline instead of asynchronously.
|
|
30
|
-
- **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses.
|
|
31
|
-
- **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
30
|
+
- **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses. A subclass of another `@SharedObject` class also gets the `@JS` members of that class, and its own members replace inherited members with the same JS name. A subclass that isn't `final` can't be constructed from JavaScript through the `@JS init` of its superclass, so give each subclass that JavaScript constructs its own `@JS init`. Like `@ExpoModule`, it adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet.
|
|
31
|
+
- **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. A record converts only from and to JavaScript values: its `from(dictionary:appContext:)` throws `RecordDictionaryConversionException`, so a record can't be created from a native `[String: Any]`. A `@Union` can be a record property. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
32
32
|
- **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first.
|
|
33
33
|
|
|
34
|
+
The members and conformances that `@Record` and `@Union` generate are `nonisolated`, so they also work in a module that builds with a default isolation, such as `-default-isolation MainActor`. The stored properties of a record still take that isolation. A class record, or a struct record with a property whose type isn't `Sendable`, then needs `nonisolated` on its declaration, for example `@Record nonisolated final class Options`.
|
|
35
|
+
|
|
34
36
|
A tuple can cross the boundary as a `@JS` argument, result or property, a `@Record` property, a `@Union` payload, an `@Event` payload, or a parameter or result of a closure argument that isn't `async`. An unlabeled tuple such as `(Int, String)` maps to an array with one value per element, and a labeled tuple such as `(x: Double, y: Double)` to an object with one property per label. A tuple must have two or more elements, all labeled or none, and each element must be convertible. Tuples nest in other tuples, optionals, arrays and `String`-keyed dictionaries. `@ExpoModule` and `@SharedObject` also resolve a type alias of a tuple, such as `typealias Span = (start: Int, end: Int)`, when the class body declares it. A macro sees only the declaration it is attached to, so an alias declared anywhere else doesn't resolve.
|
|
35
37
|
|
|
36
38
|
How that looks in a module:
|
|
@@ -64,6 +66,10 @@ final class Cache: SharedObject {
|
|
|
64
66
|
|
|
65
67
|
The author-facing documentation for each macro lives next to its declaration in `expo-modules-core`, in `ios/Core/ExpoModulesMacros.swift`. This repository holds the implementations.
|
|
66
68
|
|
|
69
|
+
## Sendable conformance
|
|
70
|
+
|
|
71
|
+
Native code calls a module or shared object from more than one thread: a `.withAsync` variant runs on the concurrent thread pool, and events and closure arguments can be called from any thread. So the generated code needs the class to be `Sendable`. A subclass of `Module` or `SharedObject` can't be checked `Sendable`, because those base classes aren't, so `@ExpoModule` and `@SharedObject` add `extension MyModule: @unchecked Sendable {}`. The compiler doesn't check the class's mutable state then: keep state that more than one thread uses behind a lock or an actor. The macros add nothing when the class already declares `Sendable` or `@unchecked Sendable`, or inherits it. Swift warns on a subclass of a module or shared object until the subclass restates `@unchecked Sendable`.
|
|
72
|
+
|
|
67
73
|
# Scanner
|
|
68
74
|
|
|
69
75
|
The Swift compiler launches a plugin executable with no arguments and speaks the plugin protocol over stdin, so the binary treats any argument as a scanner invocation instead:
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -293,9 +293,12 @@ 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
|
|
297
|
-
|
|
296
|
+
/// What the class inherits from `SharedRef<T>`, directly or through another scanned `@SharedObject`
|
|
297
|
+
/// class, or `nil` when it doesn't inherit `SharedRef`.
|
|
298
|
+
var sharedRef: ExportedSharedRef?
|
|
298
299
|
|
|
300
|
+
/// The class's `@JS` members, followed by the ones it inherits from scanned `@SharedObject`
|
|
301
|
+
/// superclasses (see `ExportedSurface.mergingInheritedMembers()`).
|
|
299
302
|
var functions: [ExportedFunction]
|
|
300
303
|
var properties: [ExportedProperty]
|
|
301
304
|
var events: [ExportedEvent]
|
|
@@ -306,10 +309,24 @@ struct ExportedSharedObject: Encodable, Equatable {
|
|
|
306
309
|
let platforms: [ExportedPlatform]
|
|
307
310
|
|
|
308
311
|
let file: String
|
|
312
|
+
|
|
313
|
+
/// The superclass as written, without generic arguments, or `nil` when there is none or it is
|
|
314
|
+
/// `SharedObject` or `SharedRef`. Not encoded: it is only read to find the inherited members.
|
|
315
|
+
var superclass: String? = nil
|
|
316
|
+
|
|
317
|
+
/// The value of the class's own `nativeRefType` override (see `ExportedSharedRef.nativeRefType`), or
|
|
318
|
+
/// `.none` when it declares none. Not encoded: a subclass of a `SharedRef` subclass reports its own
|
|
319
|
+
/// override in the inherited `sharedRef`.
|
|
320
|
+
var ownNativeRefType: String?? = .none
|
|
321
|
+
|
|
322
|
+
private enum CodingKeys: String, CodingKey {
|
|
323
|
+
case name, jsName, initializer, sharedRef, functions, properties, events, accessLevel, platforms, file
|
|
324
|
+
}
|
|
309
325
|
}
|
|
310
326
|
|
|
311
|
-
/// The `SharedRef<T>` superclass of a shared object.
|
|
312
|
-
///
|
|
327
|
+
/// The `SharedRef<T>` superclass of a shared object. A class that inherits `SharedRef` through another
|
|
328
|
+
/// `@SharedObject` class reports the `T` of that class. A class that inherits it through a class the
|
|
329
|
+
/// scan doesn't report has none.
|
|
313
330
|
struct ExportedSharedRef: Encodable, Equatable {
|
|
314
331
|
/// The `T` in `SharedRef<T>`: the native type the object holds, which JS never sees.
|
|
315
332
|
let native: TypeNode
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/// Adds to each `@SharedObject` class the `@JS` members it inherits from scanned `@SharedObject`
|
|
2
|
+
/// superclasses.
|
|
3
|
+
///
|
|
4
|
+
/// The `@SharedObject` macro binds the members of the superclass on the JavaScript objects of the
|
|
5
|
+
/// subclass too, since core builds each JavaScript class on the base `SharedObject` class and not on the
|
|
6
|
+
/// JavaScript class of the superclass. So the reported surface of a subclass lists the inherited members
|
|
7
|
+
/// as its own, and a consumer generates the same type as for any other class.
|
|
8
|
+
|
|
9
|
+
extension ExportedSurface {
|
|
10
|
+
/// This surface with the inherited members added to each shared object. Runs after
|
|
11
|
+
/// `resolvingRefs()`, so the refs in an inherited member are resolved from the superclass that
|
|
12
|
+
/// declares it, as Swift resolves them.
|
|
13
|
+
///
|
|
14
|
+
/// The superclass is looked up from the scope that encloses the class, the way `RefIndex` looks up a
|
|
15
|
+
/// ref. A superclass the scan doesn't report as a shared object (a protocol, a class without
|
|
16
|
+
/// `@SharedObject`, or a class from another module) ends the chain, so nothing is inherited through it.
|
|
17
|
+
///
|
|
18
|
+
/// A superclass declared more than once, in `#if` branches, contributes the members of each
|
|
19
|
+
/// declaration whose platforms overlap the platforms of the class. Each member keeps its own
|
|
20
|
+
/// `platforms`.
|
|
21
|
+
///
|
|
22
|
+
/// The members merge the way the macro binds them: the inherited ones first, then the class's own. An
|
|
23
|
+
/// own member replaces an inherited function or property with the same JS name, separately for static
|
|
24
|
+
/// and instance members. Events are kept from both: an event is a stored property that emits under its
|
|
25
|
+
/// JS name, so a subclass can't replace an inherited one, and both are sent. The `@JS init` isn't
|
|
26
|
+
/// inherited: core doesn't construct a subclass through the initializer of its superclass.
|
|
27
|
+
///
|
|
28
|
+
/// A class that inherits `SharedRef` through its superclass gets the `sharedRef` of the superclass,
|
|
29
|
+
/// with the class's own `nativeRefType` override when it has one.
|
|
30
|
+
func mergingInheritedMembers() -> ExportedSurface {
|
|
31
|
+
let index = RefIndex(surface: self)
|
|
32
|
+
let declarations = Dictionary(grouping: sharedObjects, by: \.name)
|
|
33
|
+
|
|
34
|
+
// `chain` holds the classes being merged, so a malformed cycle in the inheritance ends instead of
|
|
35
|
+
// recursing forever.
|
|
36
|
+
func merged(_ sharedObject: ExportedSharedObject, chain: Set<String>) -> ExportedSharedObject {
|
|
37
|
+
let scope = sharedObject.name.split(separator: ".").dropLast().joined(separator: ".")
|
|
38
|
+
guard let superclass = sharedObject.superclass,
|
|
39
|
+
let entry = index.lookup(superclass, in: scope),
|
|
40
|
+
entry.kind == .sharedObject,
|
|
41
|
+
!chain.contains(entry.name)
|
|
42
|
+
else {
|
|
43
|
+
return sharedObject
|
|
44
|
+
}
|
|
45
|
+
let platforms = Set(sharedObject.platforms.map(\.name))
|
|
46
|
+
let parents = (declarations[entry.name] ?? [])
|
|
47
|
+
.filter { declaration in declaration.platforms.contains { platforms.contains($0.name) } }
|
|
48
|
+
.map { merged($0, chain: chain.union([sharedObject.name])) }
|
|
49
|
+
guard !parents.isEmpty else {
|
|
50
|
+
return sharedObject
|
|
51
|
+
}
|
|
52
|
+
var result = sharedObject
|
|
53
|
+
|
|
54
|
+
let ownMembers = Set(
|
|
55
|
+
sharedObject.functions.map { MemberKey(jsName: $0.jsName, isStatic: $0.isStatic) }
|
|
56
|
+
+ sharedObject.properties.map { MemberKey(jsName: $0.jsName, isStatic: $0.isStatic) }
|
|
57
|
+
)
|
|
58
|
+
result.functions =
|
|
59
|
+
parents.flatMap(\.functions).filter {
|
|
60
|
+
!ownMembers.contains(MemberKey(jsName: $0.jsName, isStatic: $0.isStatic))
|
|
61
|
+
}
|
|
62
|
+
+ sharedObject.functions
|
|
63
|
+
result.properties =
|
|
64
|
+
parents.flatMap(\.properties).filter {
|
|
65
|
+
!ownMembers.contains(MemberKey(jsName: $0.jsName, isStatic: $0.isStatic))
|
|
66
|
+
}
|
|
67
|
+
+ sharedObject.properties
|
|
68
|
+
result.events = parents.flatMap(\.events) + sharedObject.events
|
|
69
|
+
|
|
70
|
+
// Every declaration of a superclass names the same superclass in practice, so the first one that
|
|
71
|
+
// inherits `SharedRef` stands for all of them.
|
|
72
|
+
if result.sharedRef == nil, let inherited = parents.lazy.compactMap(\.sharedRef).first {
|
|
73
|
+
result.sharedRef = ExportedSharedRef(
|
|
74
|
+
native: inherited.native,
|
|
75
|
+
nativeRefType: sharedObject.ownNativeRefType ?? inherited.nativeRefType
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return ExportedSurface(
|
|
82
|
+
modules: modules,
|
|
83
|
+
sharedObjects: sharedObjects.map { merged($0, chain: []) },
|
|
84
|
+
records: records,
|
|
85
|
+
enums: enums,
|
|
86
|
+
unions: unions
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// A member's slot on the JavaScript side: static members live on the constructor and instance members
|
|
92
|
+
/// on the prototype, so the two don't replace each other.
|
|
93
|
+
private struct MemberKey: Hashable {
|
|
94
|
+
let jsName: String
|
|
95
|
+
let isStatic: Bool
|
|
96
|
+
}
|
|
@@ -57,14 +57,15 @@ func scanExports(paths: [String]) -> ScanExportsResult {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Refs resolve only once every file has been walked: a type parsed in one file may name a type
|
|
60
|
-
// declared in another, so the full set of declarations has to exist before any lookup is valid.
|
|
60
|
+
// declared in another, so the full set of declarations has to exist before any lookup is valid. The
|
|
61
|
+
// same holds for a superclass, so the inherited members are merged after that.
|
|
61
62
|
let surface = ExportedSurface(
|
|
62
63
|
modules: modules,
|
|
63
64
|
sharedObjects: sharedObjects,
|
|
64
65
|
records: records,
|
|
65
66
|
enums: enums,
|
|
66
67
|
unions: unions
|
|
67
|
-
).resolvingRefs()
|
|
68
|
+
).resolvingRefs().mergingInheritedMembers()
|
|
68
69
|
|
|
69
70
|
return ScanExportsResult(
|
|
70
71
|
schemaVersion: scanExportsSchemaVersion,
|
|
@@ -183,7 +183,9 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
183
183
|
events: events,
|
|
184
184
|
accessLevel: accessLevel,
|
|
185
185
|
platforms: platforms,
|
|
186
|
-
file: file
|
|
186
|
+
file: file,
|
|
187
|
+
superclass: sharedObjectSuperclass(of: node),
|
|
188
|
+
ownNativeRefType: declaredNativeRefType(in: members)
|
|
187
189
|
)
|
|
188
190
|
)
|
|
189
191
|
return
|
|
@@ -759,21 +761,50 @@ private func sharedRefSuperclass(of node: some DeclGroupSyntax) -> ExportedShare
|
|
|
759
761
|
else {
|
|
760
762
|
return nil
|
|
761
763
|
}
|
|
762
|
-
return ExportedSharedRef(
|
|
764
|
+
return ExportedSharedRef(
|
|
765
|
+
native: native,
|
|
766
|
+
nativeRefType: declaredNativeRefType(in: node.memberBlock.members) ?? "unknown"
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/// The superclass a `@SharedObject` class names, without generic arguments (`Outer.ImageRef`), or `nil`
|
|
771
|
+
/// when it names none, `SharedObject`, or `SharedRef`. The first entry of the inheritance clause is read,
|
|
772
|
+
/// since Swift requires the superclass to come first. It may also be a protocol, which then names no
|
|
773
|
+
/// scanned shared object.
|
|
774
|
+
private func sharedObjectSuperclass(of node: some DeclGroupSyntax) -> String? {
|
|
775
|
+
guard let type = node.inheritanceClause?.inheritedTypes.first?.type,
|
|
776
|
+
let spelling = spellingWithoutGenericArguments(of: type)
|
|
777
|
+
else {
|
|
778
|
+
return nil
|
|
779
|
+
}
|
|
780
|
+
let name = spelling.split(separator: ".").last.map(String.init)
|
|
781
|
+
return name == "SharedObject" || name == "SharedRef" ? nil : spelling
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/// A simple or qualified type name as written, without backticks or generic arguments, or `nil` for any
|
|
785
|
+
/// other kind of type.
|
|
786
|
+
private func spellingWithoutGenericArguments(of type: TypeSyntax) -> String? {
|
|
787
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
788
|
+
return identifier.name.text.filter { $0 != "`" }
|
|
789
|
+
}
|
|
790
|
+
if let member = type.as(MemberTypeSyntax.self), let base = spellingWithoutGenericArguments(of: member.baseType) {
|
|
791
|
+
return base + "." + member.name.text.filter { $0 != "`" }
|
|
792
|
+
}
|
|
793
|
+
return nil
|
|
763
794
|
}
|
|
764
795
|
|
|
765
|
-
/// The value of
|
|
766
|
-
/// `override var nativeRefType: String { "image" }`, also with `return` or inside a `get` block.
|
|
767
|
-
/// `
|
|
768
|
-
/// anything else.
|
|
769
|
-
private func
|
|
796
|
+
/// The value of the class's `nativeRefType` override when it returns a plain string literal: `"image"`
|
|
797
|
+
/// in `override var nativeRefType: String { "image" }`, also with `return` or inside a `get` block.
|
|
798
|
+
/// `.none` when the class declares no `nativeRefType` (core's default is `unknown`), and `.some(nil)`
|
|
799
|
+
/// when its getter is anything else.
|
|
800
|
+
private func declaredNativeRefType(in members: MemberBlockItemListSyntax) -> String?? {
|
|
770
801
|
let binding = members.lazy
|
|
771
802
|
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
|
|
772
803
|
.filter { !isTypeLevel($0.modifiers) }
|
|
773
804
|
.flatMap { $0.bindings }
|
|
774
805
|
.first { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "nativeRefType" }
|
|
775
806
|
guard let binding else {
|
|
776
|
-
return
|
|
807
|
+
return .none
|
|
777
808
|
}
|
|
778
809
|
let statements: CodeBlockItemListSyntax?
|
|
779
810
|
switch binding.accessorBlock?.accessors {
|
|
@@ -785,18 +816,18 @@ private func nativeRefType(in members: MemberBlockItemListSyntax) -> String? {
|
|
|
785
816
|
statements = nil
|
|
786
817
|
}
|
|
787
818
|
guard let statements, statements.count == 1, let item = statements.first?.item else {
|
|
788
|
-
return nil
|
|
819
|
+
return .some(nil)
|
|
789
820
|
}
|
|
790
821
|
let expression = item.as(ReturnStmtSyntax.self)?.expression ?? item.as(ExprSyntax.self)
|
|
791
822
|
guard let literal = expression?.as(StringLiteralExprSyntax.self),
|
|
792
823
|
literal.segments.count == 1,
|
|
793
824
|
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
794
825
|
else {
|
|
795
|
-
return nil
|
|
826
|
+
return .some(nil)
|
|
796
827
|
}
|
|
797
828
|
// An escape would need real unescaping, so it's treated as unreadable, as in `writtenRawValue(of:)`.
|
|
798
829
|
let content = segment.content.text
|
|
799
|
-
return content.contains("\\") ? nil : content
|
|
830
|
+
return .some(content.contains("\\") ? nil : content)
|
|
800
831
|
}
|
|
801
832
|
|
|
802
833
|
private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
@@ -13,7 +13,8 @@ import SwiftSyntaxMacros
|
|
|
13
13
|
/// synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
|
|
14
14
|
/// - `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
|
|
15
15
|
/// `@ModuleDefinitionBuilder` on a `definition()` method.
|
|
16
|
-
/// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it
|
|
16
|
+
/// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it, and an
|
|
17
|
+
/// `@unchecked Sendable` conformance when the class isn't `Sendable` yet.
|
|
17
18
|
///
|
|
18
19
|
/// Inheriting from `Module` is therefore optional — a class can carry any superclass (or
|
|
19
20
|
/// none) and still be a module, because everything inheritance used to provide is synthesized.
|
|
@@ -219,15 +220,20 @@ extension ExpoModuleMacro: ExtensionMacro {
|
|
|
219
220
|
guard !protocols.isEmpty else {
|
|
220
221
|
return []
|
|
221
222
|
}
|
|
223
|
+
var extensions: [ExtensionDeclSyntax] = []
|
|
222
224
|
// These base types already supply `AnyModule`; emitting a second conformance here
|
|
223
225
|
// would be redundant and error.
|
|
224
|
-
if
|
|
225
|
-
|
|
226
|
+
if protocols.contains(where: { $0.trimmedDescription == "AnyModule" }),
|
|
227
|
+
let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
228
|
+
!inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
|
|
226
229
|
{
|
|
227
|
-
|
|
230
|
+
let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
|
|
231
|
+
extensions.append(conformance.cast(ExtensionDeclSyntax.self))
|
|
232
|
+
}
|
|
233
|
+
if let sendable = uncheckedSendableExtension(for: type, of: declaration, conformingTo: protocols) {
|
|
234
|
+
extensions.append(sendable)
|
|
228
235
|
}
|
|
229
|
-
|
|
230
|
-
return [conformance.cast(ExtensionDeclSyntax.self)]
|
|
236
|
+
return extensions
|
|
231
237
|
}
|
|
232
238
|
}
|
|
233
239
|
|
|
@@ -11,8 +11,9 @@ import SwiftSyntaxMacros
|
|
|
11
11
|
/// - `decode(_:in:)` and `encode(_:in:)` — the fast paths, reading each property straight off a JS
|
|
12
12
|
/// object and writing it straight into one,
|
|
13
13
|
/// - `from(object:appContext:)` and `toObject(appContext:)`, which forward to `decode` and `encode`,
|
|
14
|
-
/// - `
|
|
15
|
-
///
|
|
14
|
+
/// - `toDictionary(appContext:)`, which puts each property into a `[String: Any]` dictionary,
|
|
15
|
+
/// - `from(dictionary:appContext:)`, which throws `RecordDictionaryConversionException`: a record
|
|
16
|
+
/// converts only from JavaScript values, so it can't be created from a native dictionary.
|
|
16
17
|
///
|
|
17
18
|
/// The type is auto-conformed to `Record` (a core protocol whose requirements are this surface); the
|
|
18
19
|
/// synthesized methods override `Record`'s reflection-based defaults, and the synthesized `decode` and
|
|
@@ -37,23 +38,16 @@ import SwiftSyntaxMacros
|
|
|
37
38
|
/// have static stored properties. The macro can't tell that an extension extends a generic type, so a
|
|
38
39
|
/// record declared in one doesn't compile.
|
|
39
40
|
///
|
|
40
|
-
///
|
|
41
|
-
/// (`decode(_:in:)` / `encode(_:in:)`)
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
/// values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
|
|
45
|
-
/// user modules without any internal core symbols. Every property type must therefore conform to both
|
|
46
|
-
/// `AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`. The conformance assertion checks only
|
|
47
|
-
/// the JS codable protocols; the compiler reports a missing `AnyArgument` at the `getDynamicType()`
|
|
48
|
-
/// call.
|
|
41
|
+
/// Properties convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
|
|
42
|
+
/// (`decode(_:in:)` / `encode(_:in:)`), spelled with `public` symbols so the synthesized code compiles
|
|
43
|
+
/// inside user modules without any internal core symbols. Every property type must conform to
|
|
44
|
+
/// `JavaScriptDecodable & JavaScriptEncodable`, which the conformance assertion checks.
|
|
49
45
|
///
|
|
50
46
|
/// A property whose type mentions `Any` (`Any?`, `[String: Any]?`, `[String: [Any]]`, …) can't conform to
|
|
51
|
-
/// the JS codable protocols, so it isn't asserted.
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
/// `
|
|
55
|
-
/// `AnyArgument` for the dictionary paths, and a bare `Any` needs nothing, since the dictionary paths
|
|
56
|
-
/// pass it through unchanged.
|
|
47
|
+
/// the JS codable protocols, so it isn't asserted. It converts through the `JavaScriptValue` free-form
|
|
48
|
+
/// methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and the matching `encodeAny…` for
|
|
49
|
+
/// `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic `decodeAny(_:as:in:)` /
|
|
50
|
+
/// `encodeAny(_:in:)` for any other type.
|
|
57
51
|
///
|
|
58
52
|
/// A record class must be `final` and have no inheritance clause. The synthesized factories construct
|
|
59
53
|
/// `Self` through the memberwise init, which only a final class can do without a `required` init, and
|
|
@@ -112,7 +106,7 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
112
106
|
members.append(memberwiseInit(properties: properties))
|
|
113
107
|
}
|
|
114
108
|
members.append(fromJSObjectFactory())
|
|
115
|
-
members.append(fromDictionaryFactory(
|
|
109
|
+
members.append(fromDictionaryFactory())
|
|
116
110
|
members.append(toDictionaryMethod(properties: properties))
|
|
117
111
|
members.append(toObjectMethod())
|
|
118
112
|
if cachesPropNames {
|
|
@@ -152,8 +146,10 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
152
146
|
return []
|
|
153
147
|
}
|
|
154
148
|
|
|
149
|
+
// `nonisolated` keeps the conformance usable on the JavaScript thread when the module builds with a
|
|
150
|
+
// default isolation, which would otherwise isolate it to the main actor.
|
|
155
151
|
let ext: DeclSyntax = """
|
|
156
|
-
extension \(type.trimmed): Record {}
|
|
152
|
+
extension \(type.trimmed): nonisolated Record {}
|
|
157
153
|
"""
|
|
158
154
|
guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
|
|
159
155
|
return []
|
|
@@ -171,8 +167,7 @@ private struct RecordProperty {
|
|
|
171
167
|
/// every identifier position in the generated code needs them.
|
|
172
168
|
let name: String
|
|
173
169
|
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
|
|
174
|
-
/// memberwise-init parameter and as the receiver of the per-property `decode`/`encode`
|
|
175
|
-
/// dictionary paths, `getDynamicType()`) conversions.
|
|
170
|
+
/// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` conversions.
|
|
176
171
|
let type: String
|
|
177
172
|
/// The default-value expression verbatim (`0`, `""`, `[]`), or `nil` when the property has none.
|
|
178
173
|
/// Inlined into the memberwise init and the factories' omitted-property branch so the synthesized
|
|
@@ -304,13 +299,13 @@ private func defaultInit(properties: [RecordProperty]) -> DeclSyntax? {
|
|
|
304
299
|
}
|
|
305
300
|
if properties.contains(where: { $0.isRequired }) {
|
|
306
301
|
return """
|
|
307
|
-
public init() {
|
|
308
|
-
fatalError("\\(Self.self) has required properties and cannot be created with init();
|
|
302
|
+
nonisolated public init() {
|
|
303
|
+
fatalError("\\(Self.self) has required properties and cannot be created with init(); create it from JavaScript or with its memberwise initializer")
|
|
309
304
|
}
|
|
310
305
|
"""
|
|
311
306
|
}
|
|
312
307
|
return """
|
|
313
|
-
public init() {
|
|
308
|
+
nonisolated public init() {
|
|
314
309
|
}
|
|
315
310
|
"""
|
|
316
311
|
}
|
|
@@ -333,7 +328,7 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
333
328
|
let signature = params.joined(separator: ", ")
|
|
334
329
|
|
|
335
330
|
return """
|
|
336
|
-
public init(\(raw: signature)) {
|
|
331
|
+
nonisolated public init(\(raw: signature)) {
|
|
337
332
|
\(raw: assignments)
|
|
338
333
|
}
|
|
339
334
|
"""
|
|
@@ -355,13 +350,13 @@ private func fromJSObjectFactory() -> DeclSyntax {
|
|
|
355
350
|
"""
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
/// `from(dictionary:appContext:)
|
|
359
|
-
///
|
|
360
|
-
|
|
361
|
-
|
|
353
|
+
/// `from(dictionary:appContext:)`, a `Record` requirement. A record converts only from JavaScript values,
|
|
354
|
+
/// through `decode(_:in:)`, so it throws. Converting each property from a native `Any` would need the
|
|
355
|
+
/// dynamic types of the 1.0 conversion API.
|
|
356
|
+
private func fromDictionaryFactory() -> DeclSyntax {
|
|
362
357
|
return """
|
|
363
|
-
public static func from(dictionary: [String: Any], appContext: AppContext) throws -> Self {
|
|
364
|
-
|
|
358
|
+
nonisolated public static func from(dictionary: [String: Any], appContext: AppContext) throws -> Self {
|
|
359
|
+
throw RecordDictionaryConversionException(String(describing: Self.self))
|
|
365
360
|
}
|
|
366
361
|
"""
|
|
367
362
|
}
|
|
@@ -413,44 +408,7 @@ private func jsObjectReadLines(properties: [RecordProperty], cachesPropNames: Bo
|
|
|
413
408
|
return lines
|
|
414
409
|
}
|
|
415
410
|
|
|
416
|
-
///
|
|
417
|
-
/// raw dictionary value in `entry<index>`. A bare `Any` property has no dynamic type to cast through, and
|
|
418
|
-
/// the dictionary value already is an `Any`, so it's read unchanged.
|
|
419
|
-
private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
420
|
-
var lines: [String] = []
|
|
421
|
-
for (index, property) in properties.enumerated() {
|
|
422
|
-
let field = fieldLocal(index)
|
|
423
|
-
let entry = "entry\(index)"
|
|
424
|
-
let exprType = expressionType(property.type)
|
|
425
|
-
let isBareAny = property.freeForm?.isBareAny == true
|
|
426
|
-
let cast: String
|
|
427
|
-
if isBareAny {
|
|
428
|
-
cast = entry
|
|
429
|
-
} else if tupleConversion(of: property.type) != nil {
|
|
430
|
-
// A type that holds a tuple has no dynamic type, so the dictionary value must already be of the
|
|
431
|
-
// property's type.
|
|
432
|
-
cast =
|
|
433
|
-
"try { () throws -> \(exprType) in guard let value = \(entry) as? \(exprType) else { throw Conversions.ConvertingException<\(exprType)>(\(entry)) }; return value }()"
|
|
434
|
-
} else {
|
|
435
|
-
cast = "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
436
|
-
}
|
|
437
|
-
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
438
|
-
if property.isRequired {
|
|
439
|
-
lines.append(" guard let \(entry) else {")
|
|
440
|
-
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
441
|
-
lines.append(" }")
|
|
442
|
-
lines.append(" let \(field) = \(cast)")
|
|
443
|
-
} else if property.isOptional {
|
|
444
|
-
lines.append(" let \(field): \(property.type) = (\(entry) == nil || \(entry)! is NSNull) ? nil : \(cast)")
|
|
445
|
-
} else {
|
|
446
|
-
lines.append(" let \(field) = \(entry) == nil ? \(property.defaultValue!) : \(isBareAny ? entry + "!" : cast)")
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return lines
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/// `toDictionary(appContext:)` — converts each property back to a JS-compatible value via the
|
|
453
|
-
/// dynamic type and assembles a `[String: Any]`.
|
|
411
|
+
/// `toDictionary(appContext:)`, which puts each property into a `[String: Any]` as it is.
|
|
454
412
|
private func toDictionaryMethod(properties: [RecordProperty]) -> DeclSyntax {
|
|
455
413
|
var lines = [" var dictionary: [String: Any] = [:]"]
|
|
456
414
|
for property in properties {
|
|
@@ -458,7 +416,7 @@ private func toDictionaryMethod(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
458
416
|
}
|
|
459
417
|
lines.append(" return dictionary")
|
|
460
418
|
return """
|
|
461
|
-
public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
419
|
+
nonisolated public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
462
420
|
\(raw: lines.joined(separator: "\n"))
|
|
463
421
|
}
|
|
464
422
|
"""
|
|
@@ -578,7 +536,7 @@ private func propNamesClass(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
578
536
|
" self.\(propNameMember(index)) = JavaScriptPropNameID(runtime, string: \"\(property.key)\")"
|
|
579
537
|
}
|
|
580
538
|
return """
|
|
581
|
-
private final class \(raw: propNamesTypeName) {
|
|
539
|
+
nonisolated private final class \(raw: propNamesTypeName) {
|
|
582
540
|
static let _key = JavaScriptRuntime.Cache.Key<\(raw: propNamesTypeName)>()
|
|
583
541
|
|
|
584
542
|
\(raw: storedProperties.joined(separator: "\n"))
|
|
@@ -21,6 +21,9 @@ import SwiftSyntaxMacros
|
|
|
21
21
|
///
|
|
22
22
|
/// The companion `@ExpoModule(classes: [Cache.self])` registers the class with the module, which
|
|
23
23
|
/// exposes it in its JavaScript object.
|
|
24
|
+
///
|
|
25
|
+
/// As an extension macro, it adds an `@unchecked Sendable` conformance when the class isn't
|
|
26
|
+
/// `Sendable` yet.
|
|
24
27
|
public struct SharedObjectMacro: MemberMacro {
|
|
25
28
|
public static func expansion(
|
|
26
29
|
of node: AttributeSyntax,
|
|
@@ -38,6 +41,11 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
38
41
|
let typeName = classDecl.name.text
|
|
39
42
|
let jsName = jsNameArgument(of: node) ?? withoutBackticks(typeName)
|
|
40
43
|
|
|
44
|
+
// A superclass other than `SharedObject` or `SharedRef` may be another `@SharedObject` class, so the
|
|
45
|
+
// decorator phases call `super` to bind its `@JS` members too. The base classes' phases bind nothing.
|
|
46
|
+
let callsSuper = !inheritsFromAny(classDecl, names: ["SharedObject", "SharedRef"])
|
|
47
|
+
let isFinal = classDecl.modifiers.contains { $0.name.tokenKind == .keyword(.final) }
|
|
48
|
+
|
|
41
49
|
// `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
|
|
42
50
|
// the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`.
|
|
43
51
|
//
|
|
@@ -117,7 +125,9 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
117
125
|
phase: .prototype,
|
|
118
126
|
functions: instanceFunctions,
|
|
119
127
|
properties: instanceProperties,
|
|
120
|
-
typeName: typeName
|
|
128
|
+
typeName: typeName,
|
|
129
|
+
callsSuper: callsSuper,
|
|
130
|
+
isFinal: isFinal
|
|
121
131
|
)
|
|
122
132
|
)
|
|
123
133
|
}
|
|
@@ -127,12 +137,14 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
127
137
|
phase: .constructor,
|
|
128
138
|
functions: staticFunctions,
|
|
129
139
|
properties: staticProperties,
|
|
130
|
-
typeName: typeName
|
|
140
|
+
typeName: typeName,
|
|
141
|
+
callsSuper: callsSuper,
|
|
142
|
+
isFinal: isFinal
|
|
131
143
|
)
|
|
132
144
|
)
|
|
133
145
|
}
|
|
134
146
|
if let constructor {
|
|
135
|
-
emitted.append(constructor.buildConstructor(typeName: typeName))
|
|
147
|
+
emitted.append(constructor.buildConstructor(typeName: typeName, isFinal: isFinal))
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
// The conformance assertions of the `@JS` members, which `@JS` leaves to this macro (see
|
|
@@ -168,3 +180,18 @@ extension SharedObjectMacro: MemberAttributeMacro {
|
|
|
168
180
|
return ["@JavaScriptActor"]
|
|
169
181
|
}
|
|
170
182
|
}
|
|
183
|
+
|
|
184
|
+
extension SharedObjectMacro: ExtensionMacro {
|
|
185
|
+
public static func expansion(
|
|
186
|
+
of node: AttributeSyntax,
|
|
187
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
188
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
189
|
+
conformingTo protocols: [TypeSyntax],
|
|
190
|
+
in context: some MacroExpansionContext
|
|
191
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
192
|
+
guard let sendable = uncheckedSendableExtension(for: type, of: declaration, conformingTo: protocols) else {
|
|
193
|
+
return []
|
|
194
|
+
}
|
|
195
|
+
return [sendable]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -98,7 +98,9 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
|
|
|
98
98
|
return []
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// `nonisolated` keeps the conformances usable on the JavaScript thread when the module builds with
|
|
102
|
+
// a default isolation, which would otherwise isolate them to the main actor.
|
|
103
|
+
let conformances = missing.map { "nonisolated \($0.trimmedDescription)" }.joined(separator: ", ")
|
|
102
104
|
let ext: DeclSyntax = """
|
|
103
105
|
extension \(type.trimmed): \(raw: conformances) {}
|
|
104
106
|
"""
|
|
@@ -298,7 +300,7 @@ private func decodableKindsProperty(union: UnionType) -> DeclSyntax {
|
|
|
298
300
|
// optimizer folds it to a constant after specialization, the same as a chain of `union(_:)` calls.
|
|
299
301
|
let expression = kinds.count == 1 ? kinds[0] : "[\(kinds.joined(separator: ", "))]"
|
|
300
302
|
return """
|
|
301
|
-
public static var decodableKinds: JavaScriptValueKinds {
|
|
303
|
+
nonisolated public static var decodableKinds: JavaScriptValueKinds {
|
|
302
304
|
return \(raw: expression)
|
|
303
305
|
}
|
|
304
306
|
"""
|
|
@@ -367,7 +369,7 @@ private func accessorMethods(union: UnionType) -> [DeclSyntax] {
|
|
|
367
369
|
let mismatch =
|
|
368
370
|
"(unionName: \"\(union.name)\", received: _payloadTypeName, expected: [\"\(unionCase.payloadType)\"])"
|
|
369
371
|
return """
|
|
370
|
-
public func `as`(_ type: \(raw: payloadType).Type) throws -> \(raw: payloadType) {
|
|
372
|
+
nonisolated public func `as`(_ type: \(raw: payloadType).Type) throws -> \(raw: payloadType) {
|
|
371
373
|
if case \(raw: unionCase.pattern) = self {
|
|
372
374
|
return payload
|
|
373
375
|
}
|
|
@@ -389,7 +391,7 @@ private func payloadTypeNameProperty(union: UnionType) -> DeclSyntax {
|
|
|
389
391
|
lines.append(" }")
|
|
390
392
|
let body = lines.joined(separator: "\n")
|
|
391
393
|
return """
|
|
392
|
-
private var _payloadTypeName: String {
|
|
394
|
+
nonisolated private var _payloadTypeName: String {
|
|
393
395
|
\(raw: body)
|
|
394
396
|
}
|
|
395
397
|
"""
|
|
@@ -16,8 +16,8 @@ struct ProvidedMacro {
|
|
|
16
16
|
ProvidedMacro(name: "OptimizedFunction", type: OptimizedFunctionAttachedMacro.self),
|
|
17
17
|
ProvidedMacro(name: "JS", type: JSMacro.self),
|
|
18
18
|
ProvidedMacro(name: "Event", type: EventMacro.self),
|
|
19
|
-
ProvidedMacro(name: "ExpoModule", type: ExpoModuleMacro.self, conformances: ["AnyModule"]),
|
|
20
|
-
ProvidedMacro(name: "SharedObject", type: SharedObjectMacro.self),
|
|
19
|
+
ProvidedMacro(name: "ExpoModule", type: ExpoModuleMacro.self, conformances: ["AnyModule", "Sendable"]),
|
|
20
|
+
ProvidedMacro(name: "SharedObject", type: SharedObjectMacro.self, conformances: ["Sendable"]),
|
|
21
21
|
ProvidedMacro(name: "Record", type: RecordMacro.self, conformances: ["Record"]),
|
|
22
22
|
ProvidedMacro(
|
|
23
23
|
name: "Union",
|
|
@@ -334,13 +334,18 @@ internal struct JSFunction {
|
|
|
334
334
|
// The parameter list is parenthesized and fully typed, since Swift rejects a type annotation on
|
|
335
335
|
// a shorthand `{ [capture] name, name in }` parameter.
|
|
336
336
|
// A sync function binds through the result-slot overload (`JavaScriptRuntime.HostFunctionClosure`):
|
|
337
|
-
// it encodes its result into the slot and gets the runtime as a parameter. A function that
|
|
338
|
-
//
|
|
337
|
+
// it encodes its result into the slot and gets the runtime as a parameter. A sync function that
|
|
338
|
+
// returns nothing binds through the `Void` overload (`JavaScriptRuntime.VoidFunctionClosure`)
|
|
339
|
+
// instead, which leaves JS's result as `undefined`. A call through it is faster than through a
|
|
340
|
+
// result-slot closure that writes nothing. The closure declares `-> Void` because a body of several
|
|
341
|
+
// statements without a `return` is otherwise ambiguous between the overloads.
|
|
339
342
|
let captures = receiver.captureClause
|
|
340
343
|
let parameters =
|
|
341
344
|
isAsync
|
|
342
345
|
? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
343
|
-
:
|
|
346
|
+
: returnType == nil
|
|
347
|
+
? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) -> Void"
|
|
348
|
+
: resultSlotClosureParameters
|
|
344
349
|
|
|
345
350
|
return """
|
|
346
351
|
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
@@ -393,7 +398,10 @@ internal struct JSProperty {
|
|
|
393
398
|
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
394
399
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
395
400
|
/// instance. Getter and setter are gated independently.
|
|
396
|
-
|
|
401
|
+
///
|
|
402
|
+
/// With `configurable`, the accessor can be redefined, so a subclass of a shared object class can
|
|
403
|
+
/// replace it with its own member of the same JS name. A `lazy var` is always configurable.
|
|
404
|
+
func decorateStatements(object: String, receiver: Receiver, configurable: Bool = false) -> String {
|
|
397
405
|
let descriptorName = derivedIdentifier(from: swiftName, suffix: "Descriptor")
|
|
398
406
|
let callee = receiver.callee
|
|
399
407
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
@@ -404,8 +412,8 @@ internal struct JSProperty {
|
|
|
404
412
|
|
|
405
413
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
406
414
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
407
|
-
if isLazy {
|
|
408
|
-
//
|
|
415
|
+
if isLazy || configurable {
|
|
416
|
+
// A lazy getter redefines the property on `this`, which needs a configurable accessor when `this`
|
|
409
417
|
// is the object that holds it (a module).
|
|
410
418
|
lines.append("\(descriptorName).setProperty(\"configurable\", value: true)")
|
|
411
419
|
}
|
|
@@ -518,14 +526,30 @@ private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversio
|
|
|
518
526
|
/// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
|
|
519
527
|
/// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
|
|
520
528
|
/// metatype for a static member).
|
|
529
|
+
///
|
|
530
|
+
/// `configurableProperties` makes the property accessors configurable. `replacesInherited` first
|
|
531
|
+
/// redefines the name of each function as a writable data property: the object may already hold an
|
|
532
|
+
/// inherited accessor with that name, which an assignment would call instead of replacing.
|
|
521
533
|
private func decorateBody(
|
|
522
534
|
functions: [JSFunction],
|
|
523
535
|
properties: [JSProperty],
|
|
524
536
|
object: String,
|
|
525
|
-
receiver: Receiver
|
|
537
|
+
receiver: Receiver,
|
|
538
|
+
configurableProperties: Bool = false,
|
|
539
|
+
replacesInherited: Bool = false
|
|
526
540
|
) -> String {
|
|
527
|
-
let functionBody = functions.map {
|
|
528
|
-
|
|
541
|
+
let functionBody = functions.map { function in
|
|
542
|
+
let binding = function.decorateStatements(object: object, receiver: receiver)
|
|
543
|
+
guard replacesInherited else {
|
|
544
|
+
return binding
|
|
545
|
+
}
|
|
546
|
+
let redefine =
|
|
547
|
+
" \(object).defineProperty(\"\(function.jsName)\", descriptor: .init(configurable: true, enumerable: true, writable: true))"
|
|
548
|
+
return redefine + "\n" + binding
|
|
549
|
+
}
|
|
550
|
+
let propertyBody = properties.map {
|
|
551
|
+
$0.decorateStatements(object: object, receiver: receiver, configurable: configurableProperties)
|
|
552
|
+
}
|
|
529
553
|
return (functionBody + propertyBody).joined(separator: "\n")
|
|
530
554
|
}
|
|
531
555
|
|
|
@@ -563,11 +587,21 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
563
587
|
/// constructor). The constructor *object* the static members decorate is distinct from the `@JS init`,
|
|
564
588
|
/// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
|
|
565
589
|
/// is emitted only when the type has a member for it.
|
|
590
|
+
///
|
|
591
|
+
/// With `callsSuper`, the phase first calls the superclass's phase, so a subclass of another
|
|
592
|
+
/// `@SharedObject` class also gets the inherited `@JS` members. Core builds each JavaScript class on
|
|
593
|
+
/// the base `SharedObject` class, not on the JavaScript class of the superclass, so the inherited
|
|
594
|
+
/// members have to be bound on the subclass's own objects. They are bound first, so a subclass member
|
|
595
|
+
/// with the same JavaScript name replaces the inherited one. For that, the property accessors of a
|
|
596
|
+
/// class that isn't `final` are configurable, and a subclass redefines the name of each of its functions
|
|
597
|
+
/// before it binds the function.
|
|
566
598
|
internal func buildDecorateSharedObjectPhase(
|
|
567
599
|
phase: Phase,
|
|
568
600
|
functions: [JSFunction],
|
|
569
601
|
properties: [JSProperty],
|
|
570
|
-
typeName: String
|
|
602
|
+
typeName: String,
|
|
603
|
+
callsSuper: Bool,
|
|
604
|
+
isFinal: Bool
|
|
571
605
|
) -> DeclSyntax {
|
|
572
606
|
let receiver: Receiver =
|
|
573
607
|
phase == .constructor
|
|
@@ -577,12 +611,18 @@ internal func buildDecorateSharedObjectPhase(
|
|
|
577
611
|
functions: functions,
|
|
578
612
|
properties: properties,
|
|
579
613
|
object: phase.rawValue,
|
|
580
|
-
receiver: receiver
|
|
614
|
+
receiver: receiver,
|
|
615
|
+
configurableProperties: !isFinal,
|
|
616
|
+
replacesInherited: callsSuper
|
|
581
617
|
)
|
|
618
|
+
let superCall =
|
|
619
|
+
callsSuper
|
|
620
|
+
? " try super._decorateSharedObject(\(phase.rawValue): \(phase.rawValue), in: runtime)\n"
|
|
621
|
+
: ""
|
|
582
622
|
return """
|
|
583
623
|
@JavaScriptActor
|
|
584
624
|
public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
585
|
-
\(raw: body)
|
|
625
|
+
\(raw: superCall)\(raw: body)
|
|
586
626
|
}
|
|
587
627
|
"""
|
|
588
628
|
}
|
|
@@ -51,11 +51,24 @@ internal struct JSConstructor {
|
|
|
51
51
|
|
|
52
52
|
/// Builds an instance from the JS arguments. Overrides the base `SharedObject` class method; returns
|
|
53
53
|
/// the concrete instance, which promotes to the base `SharedObject?` return type.
|
|
54
|
-
|
|
54
|
+
///
|
|
55
|
+
/// A subclass without a `@JS init` of its own inherits this override, and would construct an instance
|
|
56
|
+
/// of this class instead of the subclass. Unless the class is `final`, the override returns `nil` when
|
|
57
|
+
/// called on a subclass, so core reports that the subclass has no JavaScript constructor.
|
|
58
|
+
func buildConstructor(typeName: String, isFinal: Bool) -> DeclSyntax {
|
|
59
|
+
let subclassGuard =
|
|
60
|
+
isFinal
|
|
61
|
+
? ""
|
|
62
|
+
: """
|
|
63
|
+
guard self == \(typeName).self else {
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
"""
|
|
55
68
|
return """
|
|
56
69
|
@JavaScriptActor
|
|
57
70
|
public override class func _constructSharedObject(this: borrowing JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
|
|
58
|
-
\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
71
|
+
\(raw: subclassGuard)\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
59
72
|
}
|
|
60
73
|
"""
|
|
61
74
|
}
|
|
@@ -479,8 +479,7 @@ extension AttributeListSyntax {
|
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
-
/// A type spelled so it's valid in expression position (before `.decode`/`.encode
|
|
483
|
-
/// `.getDynamicType()`, or after `as!`). Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
482
|
+
/// A type spelled so it's valid in expression position (before `.decode`/`.encode`). Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
484
483
|
/// type-annotation position, so a trailing `!` is rewritten to `?` (`T!` and `T?` are both
|
|
485
484
|
/// `Optional<T>`, which the conversion layer treats identically). Other type spellings pass through
|
|
486
485
|
/// unchanged.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
|
|
4
|
+
/// The `@unchecked Sendable` conformance that `@ExpoModule` and `@SharedObject` add to their class, or
|
|
5
|
+
/// `nil` when the class already conforms. The compiler leaves `Sendable` out of `protocols` when the
|
|
6
|
+
/// class already conforms, also through a superclass. The inheritance clause is checked too, so that
|
|
7
|
+
/// `expand`, which passes every declared conformance, gives the same result for a class that spells
|
|
8
|
+
/// out its own conformance.
|
|
9
|
+
///
|
|
10
|
+
/// A module or shared object is called from more than one thread: `.withAsync` runs a function on
|
|
11
|
+
/// the concurrent thread pool, and native code calls events and closures from any thread. The class
|
|
12
|
+
/// can't be checked `Sendable`, because `Module` and `SharedObject` aren't `Sendable`, so
|
|
13
|
+
/// `@unchecked Sendable` is the only conformance it can have.
|
|
14
|
+
internal func uncheckedSendableExtension(
|
|
15
|
+
for type: some TypeSyntaxProtocol,
|
|
16
|
+
of declaration: some DeclGroupSyntax,
|
|
17
|
+
conformingTo protocols: [TypeSyntax]
|
|
18
|
+
) -> ExtensionDeclSyntax? {
|
|
19
|
+
guard protocols.contains(where: { $0.trimmedDescription == "Sendable" }) else {
|
|
20
|
+
return nil
|
|
21
|
+
}
|
|
22
|
+
let inherited = declaration.inheritanceClause?.inheritedTypes ?? []
|
|
23
|
+
if inherited.contains(where: { isSendable($0.type) }) {
|
|
24
|
+
return nil
|
|
25
|
+
}
|
|
26
|
+
let conformance: DeclSyntax = "extension \(type.trimmed): @unchecked Sendable {}"
|
|
27
|
+
return conformance.cast(ExtensionDeclSyntax.self)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Whether an inherited type is `Sendable`, with or without attributes such as `@unchecked`.
|
|
31
|
+
private func isSendable(_ type: TypeSyntax) -> Bool {
|
|
32
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
33
|
+
return isSendable(attributed.baseType)
|
|
34
|
+
}
|
|
35
|
+
return baseIdentifier(of: type) == "Sendable"
|
|
36
|
+
}
|
package/build/types.d.ts
CHANGED
|
@@ -281,16 +281,24 @@ export interface ExportedSharedObject extends ExportedAccess, ExportedAvailabili
|
|
|
281
281
|
* which every JS object inherits.
|
|
282
282
|
*/
|
|
283
283
|
initializer?: ExportedInitializer;
|
|
284
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* What the class inherits from `SharedRef<T>`, directly or through another scanned `@SharedObject`
|
|
286
|
+
* class, or absent when it doesn't inherit `SharedRef`.
|
|
287
|
+
*/
|
|
285
288
|
sharedRef?: ExportedSharedRef;
|
|
289
|
+
/**
|
|
290
|
+
* The class's `@JS` members include the ones it inherits from scanned `@SharedObject` superclasses,
|
|
291
|
+
* the inherited ones first. An own member replaces an inherited one with the same JS name.
|
|
292
|
+
*/
|
|
286
293
|
functions: ExportedFunction[];
|
|
287
294
|
properties: ExportedProperty[];
|
|
288
295
|
events: ExportedEvent[];
|
|
289
296
|
file: string;
|
|
290
297
|
}
|
|
291
298
|
/**
|
|
292
|
-
* The `SharedRef<T>` superclass of a shared object.
|
|
293
|
-
*
|
|
299
|
+
* The `SharedRef<T>` superclass of a shared object. A class that inherits `SharedRef` through another
|
|
300
|
+
* `@SharedObject` class reports the `T` of that class. A class that inherits it through a class the
|
|
301
|
+
* scan doesn't report has none.
|
|
294
302
|
*/
|
|
295
303
|
export interface ExportedSharedRef {
|
|
296
304
|
/** The `T` in `SharedRef<T>`: the native type the object holds, which JS never sees. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Swift macro plugin for Expo modules",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typescript": "^5.4.0"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"@expo/modules-macros-linux-arm64": "0.
|
|
30
|
-
"@expo/modules-macros-linux-x64": "0.
|
|
31
|
-
"@expo/modules-macros-win32-arm64": "0.
|
|
32
|
-
"@expo/modules-macros-win32-x64": "0.
|
|
29
|
+
"@expo/modules-macros-linux-arm64": "0.28.0",
|
|
30
|
+
"@expo/modules-macros-linux-x64": "0.28.0",
|
|
31
|
+
"@expo/modules-macros-win32-arm64": "0.28.0",
|
|
32
|
+
"@expo/modules-macros-win32-x64": "0.28.0"
|
|
33
33
|
}
|
|
34
34
|
}
|