@expo/expo-modules-macros-plugin 0.0.8 → 0.0.9
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/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Package.swift +1 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +95 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +29 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +52 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +4 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +234 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +191 -0
- package/package.json +1 -1
|
Binary file
|
package/apple/Package.swift
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Member macro applied to a `Module` subclass. Scans the class body for declarations
|
|
7
|
+
marked with `@JS` and synthesizes a framework-internal `_exposedDefinition()`
|
|
8
|
+
method returning `[AnyDefinition]`. `expo-modules-core` calls it automatically
|
|
9
|
+
and merges the result into the module's definition.
|
|
10
|
+
|
|
11
|
+
@ExpoModule
|
|
12
|
+
public final class MyModule: Module {
|
|
13
|
+
public func definition() -> ModuleDefinition {
|
|
14
|
+
Name("MyModule")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@JS
|
|
18
|
+
func greet(name: String) -> String { "Hi, \(name)" }
|
|
19
|
+
}
|
|
20
|
+
*/
|
|
21
|
+
public struct ExpoModuleMacro: MemberMacro {
|
|
22
|
+
public static func expansion(
|
|
23
|
+
of node: AttributeSyntax,
|
|
24
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
25
|
+
conformingTo protocols: [TypeSyntax],
|
|
26
|
+
in context: some MacroExpansionContext
|
|
27
|
+
) throws -> [DeclSyntax] {
|
|
28
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
29
|
+
throw MacroExpansionErrorMessage("@ExpoModule can only be applied to a class")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let moduleName = jsNameArgument(of: node) ?? classDecl.name.text
|
|
33
|
+
var entries: [String] = ["Name(\"\(moduleName)\")"]
|
|
34
|
+
|
|
35
|
+
for typeName in classListArgument(of: node, label: "classes") {
|
|
36
|
+
entries.append("\(typeName)._exposedClassDefinition()")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for member in classDecl.memberBlock.members {
|
|
40
|
+
let decl = member.decl
|
|
41
|
+
|
|
42
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
43
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
44
|
+
entries.append(buildFunctionEntry(funcDecl: funcDecl, attribute: attribute))
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
49
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
50
|
+
entries.append(contentsOf: buildPropertyEntries(varDecl: varDecl, attribute: attribute))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let lines = entries.map { " \($0)" }.joined(separator: ",\n")
|
|
55
|
+
let body = " return [\n\(lines)\n ]"
|
|
56
|
+
|
|
57
|
+
let method: DeclSyntax = """
|
|
58
|
+
public func _exposedDefinition() -> [AnyDefinition] {
|
|
59
|
+
\(raw: body)
|
|
60
|
+
}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
return [method]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Member builders
|
|
68
|
+
|
|
69
|
+
private func buildFunctionEntry(
|
|
70
|
+
funcDecl: FunctionDeclSyntax,
|
|
71
|
+
attribute: AttributeSyntax
|
|
72
|
+
) -> String {
|
|
73
|
+
let swiftName = funcDecl.name.text
|
|
74
|
+
let jsName = jsNameArgument(of: attribute) ?? swiftName
|
|
75
|
+
let isAsync = funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil
|
|
76
|
+
let dslEntry = isAsync ? "AsyncFunction" : "Function"
|
|
77
|
+
return "\(dslEntry)(\"\(jsName)\", \(swiftName))"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func buildPropertyEntries(
|
|
81
|
+
varDecl: VariableDeclSyntax,
|
|
82
|
+
attribute: AttributeSyntax
|
|
83
|
+
) -> [String] {
|
|
84
|
+
let jsNameOverride = jsNameArgument(of: attribute)
|
|
85
|
+
|
|
86
|
+
return varDecl.bindings.compactMap { binding in
|
|
87
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
88
|
+
return nil
|
|
89
|
+
}
|
|
90
|
+
let swiftName = ident.identifier.text
|
|
91
|
+
let jsName = jsNameOverride ?? swiftName
|
|
92
|
+
return "Property(\"\(jsName)\") { self.\(swiftName) }"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxMacros
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
Marker macro applied to module / shared-object members that should be exposed to JavaScript.
|
|
6
|
+
Expands to nothing on its own; `@ExpoModule` and `@SharedObject` discover declarations
|
|
7
|
+
carrying this attribute and generate the corresponding `Function` / `AsyncFunction` /
|
|
8
|
+
`Property` / `Constructor` registrations.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
|
|
12
|
+
@JS
|
|
13
|
+
func greet(name: String) -> String { ... }
|
|
14
|
+
|
|
15
|
+
@JS("doWork")
|
|
16
|
+
func performWork() async throws { ... }
|
|
17
|
+
|
|
18
|
+
@JS
|
|
19
|
+
var status: String { "ok" }
|
|
20
|
+
*/
|
|
21
|
+
public struct JSMacro: PeerMacro {
|
|
22
|
+
public static func expansion(
|
|
23
|
+
of node: AttributeSyntax,
|
|
24
|
+
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
25
|
+
in context: some MacroExpansionContext
|
|
26
|
+
) throws -> [DeclSyntax] {
|
|
27
|
+
return []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Reads the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork".
|
|
5
|
+
Returns nil if the attribute has no arguments or the first argument is not a string literal.
|
|
6
|
+
*/
|
|
7
|
+
internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
8
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
9
|
+
let first = args.first,
|
|
10
|
+
let str = first.expression.as(StringLiteralExprSyntax.self),
|
|
11
|
+
let segment = str.segments.first?.as(StringSegmentSyntax.self) else {
|
|
12
|
+
return nil
|
|
13
|
+
}
|
|
14
|
+
return segment.content.text
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
|
|
19
|
+
and returns the type names referenced (e.g. `["Foo", "Bar"]`). Each element must be a
|
|
20
|
+
`<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
|
|
21
|
+
*/
|
|
22
|
+
internal func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
|
|
23
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
24
|
+
return []
|
|
25
|
+
}
|
|
26
|
+
for arg in args where arg.label?.text == label {
|
|
27
|
+
guard let array = arg.expression.as(ArrayExprSyntax.self) else {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
return array.elements.compactMap { element -> String? in
|
|
31
|
+
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
32
|
+
memberAccess.declName.baseName.text == "self",
|
|
33
|
+
let base = memberAccess.base?.as(DeclReferenceExprSyntax.self) else {
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
return base.baseName.text
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
extension AttributeListSyntax {
|
|
43
|
+
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
44
|
+
for element in self {
|
|
45
|
+
if let attr = element.as(AttributeSyntax.self),
|
|
46
|
+
attr.attributeName.trimmedDescription == name {
|
|
47
|
+
return attr
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return nil
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Member macro applied to a `Record` type. Scans the body for `@Field`-annotated
|
|
7
|
+
stored properties and synthesizes `_recordFields(of:)`, returning each field
|
|
8
|
+
paired with its compile-time key. This replaces the runtime `Mirror` walk in
|
|
9
|
+
`fieldsOf(_:)` and lets the framework drop the lazy keying that today protects
|
|
10
|
+
the field's options with a `Mutex`.
|
|
11
|
+
|
|
12
|
+
@Record
|
|
13
|
+
struct Options: Record {
|
|
14
|
+
@Field var name: String = ""
|
|
15
|
+
@Field(.required) var count: Int = 0
|
|
16
|
+
@Field(.keyed("custom_key")) var flag: Bool = false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Expansion:
|
|
20
|
+
|
|
21
|
+
public static func _recordFields(of instance: Self) -> [RecordFieldDescriptor] {
|
|
22
|
+
var fields: [RecordFieldDescriptor] = []
|
|
23
|
+
fields.append(RecordFieldDescriptor(key: "name", isRequired: false, field: instance._name))
|
|
24
|
+
fields.append(RecordFieldDescriptor(key: "count", isRequired: true, field: instance._count))
|
|
25
|
+
fields.append(RecordFieldDescriptor(key: "custom_key", isRequired: false, field: instance._flag))
|
|
26
|
+
return fields
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
`isRequired` is computed from the `@Field` attribute arguments at compile time so the
|
|
30
|
+
framework can avoid a `Mutex.withLock` per field per call.
|
|
31
|
+
|
|
32
|
+
For classes that inherit from another `@Record`-annotated class, the macro
|
|
33
|
+
prepends `super._recordFields(of: instance)` so inherited fields appear first.
|
|
34
|
+
*/
|
|
35
|
+
public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
36
|
+
public static func expansion(
|
|
37
|
+
of node: AttributeSyntax,
|
|
38
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
39
|
+
conformingTo protocols: [TypeSyntax],
|
|
40
|
+
in context: some MacroExpansionContext
|
|
41
|
+
) throws -> [DeclSyntax] {
|
|
42
|
+
let isClass = declaration.is(ClassDeclSyntax.self)
|
|
43
|
+
guard declaration.is(StructDeclSyntax.self) || isClass else {
|
|
44
|
+
throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var entries: [(propertyName: String, key: String, isRequired: Bool)] = []
|
|
48
|
+
|
|
49
|
+
for member in declaration.memberBlock.members {
|
|
50
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
51
|
+
let attribute = varDecl.attributes.firstAttribute(named: "Field") else {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
for binding in varDecl.bindings {
|
|
55
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
let propertyName = ident.identifier.text
|
|
59
|
+
let key = explicitKeyArgument(of: attribute) ?? propertyName
|
|
60
|
+
let isRequired = hasRequiredArgument(of: attribute)
|
|
61
|
+
entries.append((propertyName, key, isRequired))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let inheritsRecord = isClass && classHasInheritance(declaration)
|
|
66
|
+
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
67
|
+
|
|
68
|
+
var lines: [String] = []
|
|
69
|
+
if inheritsRecord {
|
|
70
|
+
lines.append(" var fields: [RecordFieldDescriptor] = super._recordFields(of: instance)")
|
|
71
|
+
} else {
|
|
72
|
+
lines.append(" var fields: [RecordFieldDescriptor] = []")
|
|
73
|
+
}
|
|
74
|
+
for entry in entries {
|
|
75
|
+
let requiredLiteral = entry.isRequired ? "true" : "false"
|
|
76
|
+
lines.append(" fields.append(RecordFieldDescriptor(key: \"\(entry.key)\", isRequired: \(requiredLiteral), field: instance._\(entry.propertyName)))")
|
|
77
|
+
}
|
|
78
|
+
lines.append(" return fields")
|
|
79
|
+
|
|
80
|
+
let body = lines.joined(separator: "\n")
|
|
81
|
+
|
|
82
|
+
let method: DeclSyntax = """
|
|
83
|
+
public \(raw: overrideKeyword)class func _recordFields(of instance: Self) -> [RecordFieldDescriptor] {
|
|
84
|
+
\(raw: body)
|
|
85
|
+
}
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
let staticMethod: DeclSyntax = """
|
|
89
|
+
public static func _recordFields(of instance: Self) -> [RecordFieldDescriptor] {
|
|
90
|
+
\(raw: body)
|
|
91
|
+
}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
return [isClass ? method : staticMethod]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
Adds `Record` and `_RecordFieldsProvider` conformances. Each is only added when the type
|
|
99
|
+
doesn't already declare it — Swift rejects redundant conformances, so the macro must check
|
|
100
|
+
the inheritance clause syntactically.
|
|
101
|
+
|
|
102
|
+
For class subclasses (which inherit conformance from a `@Record`-annotated parent), no
|
|
103
|
+
extension is emitted at all. The user can write either `@Record struct Options { ... }`
|
|
104
|
+
or `@Record struct Options: Record { ... }`; both work.
|
|
105
|
+
*/
|
|
106
|
+
public static func expansion(
|
|
107
|
+
of node: AttributeSyntax,
|
|
108
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
109
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
110
|
+
conformingTo protocols: [TypeSyntax],
|
|
111
|
+
in context: some MacroExpansionContext
|
|
112
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
113
|
+
if declaration.is(ClassDeclSyntax.self) && classHasInheritance(declaration) {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else {
|
|
117
|
+
return []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
var conformances: [String] = []
|
|
121
|
+
if !inheritsProtocol(named: "Record", in: declaration) {
|
|
122
|
+
conformances.append("Record")
|
|
123
|
+
}
|
|
124
|
+
if !inheritsProtocol(named: "_RecordFieldsProvider", in: declaration) {
|
|
125
|
+
conformances.append("_RecordFieldsProvider")
|
|
126
|
+
}
|
|
127
|
+
if conformances.isEmpty {
|
|
128
|
+
return []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let ext: DeclSyntax = """
|
|
132
|
+
extension \(type.trimmed): \(raw: conformances.joined(separator: ", ")) {}
|
|
133
|
+
"""
|
|
134
|
+
guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
return [extDecl]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
True if the type's inheritance clause already lists a protocol with the given name.
|
|
143
|
+
Matches either the bare identifier (`Record`) or a qualified member access ending in
|
|
144
|
+
the name (`ExpoModulesCore.Record`). Typealiases can't be resolved syntactically, so a
|
|
145
|
+
user who refers to `Record` via a typealias will get a redundant-conformance error from
|
|
146
|
+
the macro-emitted extension and should drop the alias for the inheritance clause.
|
|
147
|
+
*/
|
|
148
|
+
private func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
|
|
149
|
+
let inheritanceClause: InheritanceClauseSyntax?
|
|
150
|
+
if let structDecl = declaration.as(StructDeclSyntax.self) {
|
|
151
|
+
inheritanceClause = structDecl.inheritanceClause
|
|
152
|
+
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
|
|
153
|
+
inheritanceClause = classDecl.inheritanceClause
|
|
154
|
+
} else {
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
guard let inherited = inheritanceClause?.inheritedTypes else {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
for entry in inherited {
|
|
161
|
+
let typeSyntax = entry.type
|
|
162
|
+
if let identifier = typeSyntax.as(IdentifierTypeSyntax.self),
|
|
163
|
+
identifier.name.text == name {
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
if let member = typeSyntax.as(MemberTypeSyntax.self),
|
|
167
|
+
member.name.text == name {
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
True if the class declaration has any inheritance clause. Used as a heuristic for
|
|
176
|
+
whether the superclass also conforms to `Record` and provides `_recordFields(of:)`.
|
|
177
|
+
The macro emits an `override` in this case; if the superclass is not a `Record`,
|
|
178
|
+
the user gets a clear compiler error pointing at the missing override target.
|
|
179
|
+
*/
|
|
180
|
+
private func classHasInheritance(_ declaration: some DeclGroupSyntax) -> Bool {
|
|
181
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
182
|
+
let inherited = classDecl.inheritanceClause?.inheritedTypes,
|
|
183
|
+
!inherited.isEmpty else {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
True if the `@Field` attribute has `.required` as one of its arguments. The check is purely
|
|
191
|
+
syntactic — it matches the literal member-access expression `.required`. Variants like
|
|
192
|
+
`FieldOption.required` or aliased identifiers won't be detected; the framework still falls
|
|
193
|
+
back to the field's runtime `isRequired` for correctness in those cases (see `Record.swift`).
|
|
194
|
+
*/
|
|
195
|
+
private func hasRequiredArgument(of attribute: AttributeSyntax) -> Bool {
|
|
196
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
for arg in args {
|
|
200
|
+
if let member = arg.expression.as(MemberAccessExprSyntax.self),
|
|
201
|
+
member.declName.baseName.text == "required" {
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
Extracts the dictionary key from a `@Field` attribute. Recognizes both forms:
|
|
210
|
+
a bare string literal (`@Field("custom_key")`, relying on
|
|
211
|
+
`FieldOption: ExpressibleByStringLiteral`) and the explicit factory call
|
|
212
|
+
(`@Field(.keyed("custom_key"))`). Returns nil if neither is present; other
|
|
213
|
+
`FieldOption` cases (`.required`, etc.) are ignored.
|
|
214
|
+
*/
|
|
215
|
+
private func explicitKeyArgument(of attribute: AttributeSyntax) -> String? {
|
|
216
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
217
|
+
return nil
|
|
218
|
+
}
|
|
219
|
+
for arg in args {
|
|
220
|
+
if let str = arg.expression.as(StringLiteralExprSyntax.self),
|
|
221
|
+
let segment = str.segments.first?.as(StringSegmentSyntax.self) {
|
|
222
|
+
return segment.content.text
|
|
223
|
+
}
|
|
224
|
+
if let call = arg.expression.as(FunctionCallExprSyntax.self),
|
|
225
|
+
let member = call.calledExpression.as(MemberAccessExprSyntax.self),
|
|
226
|
+
member.declName.baseName.text == "keyed",
|
|
227
|
+
let firstArg = call.arguments.first,
|
|
228
|
+
let str = firstArg.expression.as(StringLiteralExprSyntax.self),
|
|
229
|
+
let segment = str.segments.first?.as(StringSegmentSyntax.self) {
|
|
230
|
+
return segment.content.text
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return nil
|
|
234
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Member macro applied to a `SharedObject` subclass. Scans the class body for
|
|
7
|
+
declarations marked with `@JS` and synthesizes `_exposedClassDefinition()`,
|
|
8
|
+
returning a `ClassDefinition` ready to drop into a module's `Class { ... }` slot.
|
|
9
|
+
|
|
10
|
+
@SharedObject
|
|
11
|
+
final class Cache: SharedObject {
|
|
12
|
+
@JS
|
|
13
|
+
init(name: String) { self.name = name }
|
|
14
|
+
|
|
15
|
+
@JS
|
|
16
|
+
func get(_ key: String) -> String? { ... }
|
|
17
|
+
|
|
18
|
+
@JS
|
|
19
|
+
var size: Int { 42 }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
The companion `@ExpoModule(classes: [Cache.self])` wires the resulting
|
|
23
|
+
definition into the module's exposed surface.
|
|
24
|
+
*/
|
|
25
|
+
public struct SharedObjectMacro: MemberMacro {
|
|
26
|
+
public static func expansion(
|
|
27
|
+
of node: AttributeSyntax,
|
|
28
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
29
|
+
conformingTo protocols: [TypeSyntax],
|
|
30
|
+
in context: some MacroExpansionContext
|
|
31
|
+
) throws -> [DeclSyntax] {
|
|
32
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
33
|
+
throw MacroExpansionErrorMessage("@SharedObject can only be applied to a class")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
guard inheritsFromSharedObject(classDecl) else {
|
|
37
|
+
throw MacroExpansionErrorMessage(
|
|
38
|
+
"@SharedObject class must inherit from SharedObject. Add `: SharedObject` to the class declaration.")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let typeName = classDecl.name.text
|
|
42
|
+
let jsName = jsNameArgument(of: node) ?? typeName
|
|
43
|
+
|
|
44
|
+
var entries: [String] = []
|
|
45
|
+
var sawConstructor = false
|
|
46
|
+
|
|
47
|
+
for member in classDecl.memberBlock.members {
|
|
48
|
+
let decl = member.decl
|
|
49
|
+
|
|
50
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
51
|
+
initDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
52
|
+
if sawConstructor {
|
|
53
|
+
throw MacroExpansionErrorMessage(
|
|
54
|
+
"@SharedObject classes can have at most one @JS initializer; JavaScript classes have a single constructor.")
|
|
55
|
+
}
|
|
56
|
+
sawConstructor = true
|
|
57
|
+
entries.append(buildConstructorEntry(initDecl: initDecl, typeName: typeName))
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
62
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
63
|
+
entries.append(
|
|
64
|
+
buildClassFunctionEntry(funcDecl: funcDecl, attribute: attribute, typeName: typeName))
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
69
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
70
|
+
entries.append(
|
|
71
|
+
contentsOf: buildClassPropertyEntries(
|
|
72
|
+
varDecl: varDecl, attribute: attribute, typeName: typeName))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let lines = entries.map { " \($0)" }.joined(separator: "\n")
|
|
77
|
+
let body = entries.isEmpty
|
|
78
|
+
? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
|
|
79
|
+
: " return Class(\"\(jsName)\", \(typeName).self) {\n\(lines)\n }"
|
|
80
|
+
|
|
81
|
+
let method: DeclSyntax = """
|
|
82
|
+
public static func _exposedClassDefinition() -> ClassDefinition {
|
|
83
|
+
\(raw: body)
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return [method]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// MARK: - Inheritance check
|
|
92
|
+
|
|
93
|
+
private func inheritsFromSharedObject(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
94
|
+
guard let inherited = classDecl.inheritanceClause?.inheritedTypes else {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
for entry in inherited {
|
|
98
|
+
if baseIdentifier(of: entry.type) == "SharedObject" {
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
106
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
107
|
+
return identifier.name.text
|
|
108
|
+
}
|
|
109
|
+
if let member = type.as(MemberTypeSyntax.self) {
|
|
110
|
+
return member.name.text
|
|
111
|
+
}
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Class-scope entry builders
|
|
116
|
+
|
|
117
|
+
private func buildClassFunctionEntry(
|
|
118
|
+
funcDecl: FunctionDeclSyntax,
|
|
119
|
+
attribute: AttributeSyntax,
|
|
120
|
+
typeName: String
|
|
121
|
+
) -> String {
|
|
122
|
+
let swiftName = funcDecl.name.text
|
|
123
|
+
let jsName = jsNameArgument(of: attribute) ?? swiftName
|
|
124
|
+
let effects = funcDecl.signature.effectSpecifiers
|
|
125
|
+
let isAsync = effects?.asyncSpecifier != nil
|
|
126
|
+
let isThrowing = effects?.throwsClause?.throwsSpecifier != nil
|
|
127
|
+
let dslEntry = isAsync ? "AsyncFunction" : "Function"
|
|
128
|
+
|
|
129
|
+
let params = funcDecl.signature.parameterClause.parameters
|
|
130
|
+
let closureParamList: String
|
|
131
|
+
let callArgList: String
|
|
132
|
+
if params.isEmpty {
|
|
133
|
+
closureParamList = "(this: \(typeName))"
|
|
134
|
+
callArgList = ""
|
|
135
|
+
} else {
|
|
136
|
+
let typedParams = params.enumerated().map { index, param in
|
|
137
|
+
"_ arg\(index): \(param.type.trimmedDescription)"
|
|
138
|
+
}.joined(separator: ", ")
|
|
139
|
+
closureParamList = "(this: \(typeName), \(typedParams))"
|
|
140
|
+
|
|
141
|
+
callArgList = params.enumerated().map { index, param in
|
|
142
|
+
let label = param.firstName.text
|
|
143
|
+
return label == "_" ? "arg\(index)" : "\(label): arg\(index)"
|
|
144
|
+
}.joined(separator: ", ")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let awaitKeyword = isAsync ? "await " : ""
|
|
148
|
+
let tryKeyword = (isAsync || isThrowing) ? "try " : ""
|
|
149
|
+
let callExpr = "\(tryKeyword)\(awaitKeyword)this.\(swiftName)(\(callArgList))"
|
|
150
|
+
|
|
151
|
+
return "\(dslEntry)(\"\(jsName)\") { \(closureParamList) in \(callExpr) }"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private func buildClassPropertyEntries(
|
|
155
|
+
varDecl: VariableDeclSyntax,
|
|
156
|
+
attribute: AttributeSyntax,
|
|
157
|
+
typeName: String
|
|
158
|
+
) -> [String] {
|
|
159
|
+
let jsNameOverride = jsNameArgument(of: attribute)
|
|
160
|
+
|
|
161
|
+
return varDecl.bindings.compactMap { binding in
|
|
162
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
163
|
+
return nil
|
|
164
|
+
}
|
|
165
|
+
let swiftName = ident.identifier.text
|
|
166
|
+
let jsName = jsNameOverride ?? swiftName
|
|
167
|
+
return "Property(\"\(jsName)\") { (this: \(typeName)) in this.\(swiftName) }"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func buildConstructorEntry(
|
|
172
|
+
initDecl: InitializerDeclSyntax,
|
|
173
|
+
typeName: String
|
|
174
|
+
) -> String {
|
|
175
|
+
let params = initDecl.signature.parameterClause.parameters
|
|
176
|
+
|
|
177
|
+
if params.isEmpty {
|
|
178
|
+
return "Constructor { \(typeName)() }"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let argList = params.enumerated().map { index, param in
|
|
182
|
+
"_ arg\(index): \(param.type.trimmedDescription)"
|
|
183
|
+
}.joined(separator: ", ")
|
|
184
|
+
|
|
185
|
+
let callArgs = params.enumerated().map { index, param in
|
|
186
|
+
let label = param.firstName.text
|
|
187
|
+
return label == "_" ? "arg\(index)" : "\(label): arg\(index)"
|
|
188
|
+
}.joined(separator: ", ")
|
|
189
|
+
|
|
190
|
+
return "Constructor { (\(argList)) in \(typeName)(\(callArgs)) }"
|
|
191
|
+
}
|