expo-modules-macros 0.27.1 → 0.29.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/linux.yml +3 -7
- package/.github/workflows/publish.yml +2 -3
- package/.github/workflows/swift.yml +7 -13
- package/.github/workflows/windows.yml +3 -10
- package/AGENTS.md +10 -7
- package/Package.swift +110 -0
- package/README.md +31 -8
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/CLI.swift +1 -1
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesCLI/Commands}/Expand/MacroExpander.swift +1 -1
- 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/main.swift +6 -0
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModuleMacro.swift +12 -6
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/RecordMacro.swift +29 -71
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/SharedObjectMacro.swift +30 -3
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/UnionMacro.swift +6 -4
- package/apple/Sources/ExpoModulesMacrosCore/PluginServer.swift +40 -0
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/ProvidedMacros.swift +7 -7
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/DecorateModuleBuilder.swift +44 -9
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSConstructor.swift +15 -2
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/MacroHelpers.swift +2 -3
- package/apple/Sources/ExpoModulesMacrosCore/Support/SendableConformance.swift +36 -0
- package/apple/Sources/ExpoModulesMacrosTool/main.swift +27 -0
- package/build/types.d.ts +11 -3
- package/package.json +5 -5
- package/apple/Package.swift +0 -66
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +0 -36
- /package/{apple/Package.resolved → Package.resolved} +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/EventMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModulesOptimizedMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoViewMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/JSMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ViewPropsMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/ClosureArguments.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/FreeFormTypes.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSNameCollisions.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/OptimizedFunctionHelpers.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/Receiver.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TupleTypes.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TypeAliases.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TypeConformanceAssertion.swift +0 -0
|
@@ -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 {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import ExpoModulesMacrosCore
|
|
2
|
+
|
|
3
|
+
// The plugin executable that SwiftPM builds for the packages that depend on the
|
|
4
|
+
// `ExpoModulesMacrosPlugin` product. It only serves the macros, so it doesn't link the CLI; the
|
|
5
|
+
// binary that this package ships is `ExpoModulesMacrosTool`, which does both.
|
|
6
|
+
try runPluginServer()
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModuleMacro.swift
RENAMED
|
@@ -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"))
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/SharedObjectMacro.swift
RENAMED
|
@@ -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
|
"""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@_spi(PluginMessage) import SwiftCompilerPluginMessageHandling
|
|
2
|
+
import SwiftSyntaxMacros
|
|
3
|
+
|
|
4
|
+
/// The module name that `expo-modules-core` passes to `#externalMacro`, and the name of the macro
|
|
5
|
+
/// target that SwiftPM loads as the plugin.
|
|
6
|
+
private let pluginModuleName = "ExpoModulesMacros"
|
|
7
|
+
|
|
8
|
+
/// Runs the compiler plugin server on stdin and stdout until the compiler closes the connection.
|
|
9
|
+
///
|
|
10
|
+
/// `CompilerPlugin.main()` finds a macro by comparing `String(reflecting:)` of each type with
|
|
11
|
+
/// `<module>.<type>` from `#externalMacro`. The macro types live in this module, not in a module
|
|
12
|
+
/// named `ExpoModulesMacros`, so both executables that serve the plugin (the SwiftPM macro target and
|
|
13
|
+
/// the shipped tool) use this provider, which matches the type name under the plugin module name.
|
|
14
|
+
public func runPluginServer() throws {
|
|
15
|
+
let connection = try StandardIOMessageConnection()
|
|
16
|
+
let listener = CompilerPluginMessageListener(connection: connection, provider: ProvidedMacroProvider())
|
|
17
|
+
try listener.main()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private struct ProvidedMacroProvider: PluginProvider {
|
|
21
|
+
let macros: [String: Macro.Type] = Dictionary(
|
|
22
|
+
uniqueKeysWithValues: ProvidedMacro.all.map { (String(describing: $0.type), $0.type) }
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
func resolveMacro(moduleName: String, typeName: String) throws -> Macro.Type {
|
|
26
|
+
guard moduleName == pluginModuleName, let type = macros[typeName] else {
|
|
27
|
+
throw UnknownMacroError(moduleName: moduleName, typeName: typeName)
|
|
28
|
+
}
|
|
29
|
+
return type
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private struct UnknownMacroError: Error, CustomStringConvertible {
|
|
34
|
+
let moduleName: String
|
|
35
|
+
let typeName: String
|
|
36
|
+
|
|
37
|
+
var description: String {
|
|
38
|
+
"macro implementation type '\(moduleName).\(typeName)' could not be found in the Expo Modules macro plugin"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -3,21 +3,21 @@ import SwiftSyntaxMacros
|
|
|
3
3
|
|
|
4
4
|
/// A macro that the plugin provides, with the attribute name and the extension conformances of its
|
|
5
5
|
/// declaration in `expo-modules-core`.
|
|
6
|
-
struct ProvidedMacro {
|
|
7
|
-
let name: String
|
|
8
|
-
let type: Macro.Type
|
|
9
|
-
var conformances: [TypeSyntax] = []
|
|
6
|
+
public struct ProvidedMacro {
|
|
7
|
+
public let name: String
|
|
8
|
+
public let type: Macro.Type
|
|
9
|
+
public var conformances: [TypeSyntax] = []
|
|
10
10
|
|
|
11
11
|
/// Every macro of the plugin. The compiler gets the types from `providingMacros`, and `expand`
|
|
12
12
|
/// matches the attributes by name and passes the same conformances as the compiler, so both read
|
|
13
13
|
/// this list.
|
|
14
|
-
static var all: [ProvidedMacro] {
|
|
14
|
+
public static var all: [ProvidedMacro] {
|
|
15
15
|
[
|
|
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",
|
|
@@ -398,7 +398,10 @@ internal struct JSProperty {
|
|
|
398
398
|
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
399
399
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
400
400
|
/// instance. Getter and setter are gated independently.
|
|
401
|
-
|
|
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 {
|
|
402
405
|
let descriptorName = derivedIdentifier(from: swiftName, suffix: "Descriptor")
|
|
403
406
|
let callee = receiver.callee
|
|
404
407
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
@@ -409,8 +412,8 @@ internal struct JSProperty {
|
|
|
409
412
|
|
|
410
413
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
411
414
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
412
|
-
if isLazy {
|
|
413
|
-
//
|
|
415
|
+
if isLazy || configurable {
|
|
416
|
+
// A lazy getter redefines the property on `this`, which needs a configurable accessor when `this`
|
|
414
417
|
// is the object that holds it (a module).
|
|
415
418
|
lines.append("\(descriptorName).setProperty(\"configurable\", value: true)")
|
|
416
419
|
}
|
|
@@ -523,14 +526,30 @@ private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversio
|
|
|
523
526
|
/// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
|
|
524
527
|
/// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
|
|
525
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.
|
|
526
533
|
private func decorateBody(
|
|
527
534
|
functions: [JSFunction],
|
|
528
535
|
properties: [JSProperty],
|
|
529
536
|
object: String,
|
|
530
|
-
receiver: Receiver
|
|
537
|
+
receiver: Receiver,
|
|
538
|
+
configurableProperties: Bool = false,
|
|
539
|
+
replacesInherited: Bool = false
|
|
531
540
|
) -> String {
|
|
532
|
-
let functionBody = functions.map {
|
|
533
|
-
|
|
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
|
+
}
|
|
534
553
|
return (functionBody + propertyBody).joined(separator: "\n")
|
|
535
554
|
}
|
|
536
555
|
|
|
@@ -568,11 +587,21 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
568
587
|
/// constructor). The constructor *object* the static members decorate is distinct from the `@JS init`,
|
|
569
588
|
/// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
|
|
570
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.
|
|
571
598
|
internal func buildDecorateSharedObjectPhase(
|
|
572
599
|
phase: Phase,
|
|
573
600
|
functions: [JSFunction],
|
|
574
601
|
properties: [JSProperty],
|
|
575
|
-
typeName: String
|
|
602
|
+
typeName: String,
|
|
603
|
+
callsSuper: Bool,
|
|
604
|
+
isFinal: Bool
|
|
576
605
|
) -> DeclSyntax {
|
|
577
606
|
let receiver: Receiver =
|
|
578
607
|
phase == .constructor
|
|
@@ -582,12 +611,18 @@ internal func buildDecorateSharedObjectPhase(
|
|
|
582
611
|
functions: functions,
|
|
583
612
|
properties: properties,
|
|
584
613
|
object: phase.rawValue,
|
|
585
|
-
receiver: receiver
|
|
614
|
+
receiver: receiver,
|
|
615
|
+
configurableProperties: !isFinal,
|
|
616
|
+
replacesInherited: callsSuper
|
|
586
617
|
)
|
|
618
|
+
let superCall =
|
|
619
|
+
callsSuper
|
|
620
|
+
? " try super._decorateSharedObject(\(phase.rawValue): \(phase.rawValue), in: runtime)\n"
|
|
621
|
+
: ""
|
|
587
622
|
return """
|
|
588
623
|
@JavaScriptActor
|
|
589
624
|
public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
590
|
-
\(raw: body)
|
|
625
|
+
\(raw: superCall)\(raw: body)
|
|
591
626
|
}
|
|
592
627
|
"""
|
|
593
628
|
}
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSConstructor.swift
RENAMED
|
@@ -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
|
}
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/MacroHelpers.swift
RENAMED
|
@@ -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.
|
|
@@ -525,7 +524,7 @@ internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of value
|
|
|
525
524
|
/// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
|
|
526
525
|
/// backticks in every identifier position, but a name JavaScript sees (a JS member name, an event
|
|
527
526
|
/// name, a record key) is the bare name.
|
|
528
|
-
|
|
527
|
+
public func withoutBackticks(_ name: String) -> String {
|
|
529
528
|
var name = Substring(name)
|
|
530
529
|
while name.first == "`" {
|
|
531
530
|
name.removeFirst()
|