expo-modules-macros 0.0.1 → 0.14.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/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- package/package.json +22 -5
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/// Accessor macro applied to a function-typed `var` on a module or shared object, turning it into a
|
|
6
|
+
/// typed JavaScript event. A function-typed `var` can't be a stored property without an initializer,
|
|
7
|
+
/// so the macro expands it into a computed getter returning a closure that dispatches by name into
|
|
8
|
+
/// the `EventEmitter` `emit` overloads (core conforms both `BaseModule` and `SharedObject` to that
|
|
9
|
+
/// protocol, so `self.emit` resolves on each):
|
|
10
|
+
///
|
|
11
|
+
/// @Event
|
|
12
|
+
/// var onProgress: (ProgressEvent) -> Void
|
|
13
|
+
/// // expands to:
|
|
14
|
+
/// var onProgress: (ProgressEvent) -> Void {
|
|
15
|
+
/// get {
|
|
16
|
+
/// { [weak self] payload in self?.emit(event: "progress", payload: payload) }
|
|
17
|
+
/// }
|
|
18
|
+
/// }
|
|
19
|
+
///
|
|
20
|
+
/// A no-payload event (`() -> Void`) dispatches through the dedicated `emit(event:)` overload.
|
|
21
|
+
/// The JS event name defaults to the property name with the conventional `on` prefix stripped
|
|
22
|
+
/// (see `defaultEventName(for:)`); `@Event("customName")` overrides it verbatim.
|
|
23
|
+
///
|
|
24
|
+
/// The closure captures `self` **weakly**: it's usually invoked inline (`self.onProgress(…)`), but an
|
|
25
|
+
/// author may store it or hand it to a delegate, and a strong capture would then extend the module's
|
|
26
|
+
/// lifetime. After the emitter deallocates the closure silently no-ops, which matches what `emit`
|
|
27
|
+
/// already does once the runtime is gone.
|
|
28
|
+
///
|
|
29
|
+
/// The synthesized property is deliberately **not** isolated to `@JavaScriptActor`, unlike `@JS`
|
|
30
|
+
/// members: `emit` is itself non-isolated and schedules the dispatch onto the JS thread internally,
|
|
31
|
+
/// so the event is callable from any thread or isolation with no actor hop at the call site. It is
|
|
32
|
+
/// also self-contained: `@ExpoModule`/`@SharedObject` neither collect `@Event` members nor register
|
|
33
|
+
/// their names anywhere.
|
|
34
|
+
///
|
|
35
|
+
/// `@Event(sync: true)` opts into **synchronous dispatch**: the closure calls `emitSync` (inline
|
|
36
|
+
/// conversion + dispatch, no scheduling) instead of `emit`, and `@ExpoModule`/`@SharedObject` stamp
|
|
37
|
+
/// the member `@JavaScriptActor` so the compiler forces the call site onto the JS thread, the
|
|
38
|
+
/// inverse of the async default. The isolation is on the property access, so it guards the inline
|
|
39
|
+
/// `self.onTick(…)` usage; a closure stored or handed off escapes it, after which `emitSync` runs
|
|
40
|
+
/// wherever the caller invokes it.
|
|
41
|
+
///
|
|
42
|
+
/// As a **peer**, the macro emits a never-called conformance assertion (see
|
|
43
|
+
/// `TypeConformanceAssertion.swift`) checking that the payload type is JS-convertible and that the
|
|
44
|
+
/// enclosing type conforms to `EventEmitter`, so both failure modes surface as clear conformance
|
|
45
|
+
/// errors on the user's own declaration.
|
|
46
|
+
public struct EventMacro: AccessorMacro {
|
|
47
|
+
public static func expansion(
|
|
48
|
+
of node: AttributeSyntax,
|
|
49
|
+
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
|
50
|
+
in context: some MacroExpansionContext
|
|
51
|
+
) throws -> [AccessorDeclSyntax] {
|
|
52
|
+
let event = try validatedEvent(of: node, on: declaration)
|
|
53
|
+
// The closure's parameter and return types are inferred from the property's declared type
|
|
54
|
+
// through the getter, so the body never has to spell the payload type. A sync event calls
|
|
55
|
+
// `emitSync` (inline dispatch, JS thread only); the default calls the scheduling `emit`.
|
|
56
|
+
let emitMethod = event.isSync ? "emitSync" : "emit"
|
|
57
|
+
let closure = event.hasPayload
|
|
58
|
+
? "{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload) }"
|
|
59
|
+
: "{ [weak self] in self?.\(emitMethod)(event: \"\(event.jsName)\") }"
|
|
60
|
+
return [
|
|
61
|
+
"""
|
|
62
|
+
get {
|
|
63
|
+
\(raw: closure)
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
extension EventMacro: PeerMacro {
|
|
71
|
+
public static func expansion(
|
|
72
|
+
of node: AttributeSyntax,
|
|
73
|
+
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
74
|
+
in context: some MacroExpansionContext
|
|
75
|
+
) throws -> [DeclSyntax] {
|
|
76
|
+
// Diagnostics are owned by the accessor expansion; an invalid declaration silently emits no
|
|
77
|
+
// peer here so each error is reported once.
|
|
78
|
+
guard let event = try? validatedEvent(of: node, on: declaration) else {
|
|
79
|
+
return []
|
|
80
|
+
}
|
|
81
|
+
// One nested helper asserts everything in a single call: the payload type is JS-convertible
|
|
82
|
+
// (the `P` parameter, dropped for no-payload events and known-conforming primitives) and the
|
|
83
|
+
// enclosing type can emit (the `E` parameter, always present since any `@Event` dispatches
|
|
84
|
+
// through `self.emit`). Named after the member so the member shows up in either diagnostic,
|
|
85
|
+
// and asserting the enclosing type by its spelled name so the conformance error names the
|
|
86
|
+
// user's type rather than 'Self'.
|
|
87
|
+
let name = event.swiftName
|
|
88
|
+
let payload = event.payloadType.flatMap(assertableBoundaryType)
|
|
89
|
+
let owner = enclosingTypeName(in: context) ?? "Self"
|
|
90
|
+
let helper = payload != nil
|
|
91
|
+
? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
|
|
92
|
+
: "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
|
|
93
|
+
let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
|
|
94
|
+
return [
|
|
95
|
+
"""
|
|
96
|
+
private func _assertTypesConformance_\(raw: name)() {
|
|
97
|
+
\(raw: helper)
|
|
98
|
+
\(raw: call)
|
|
99
|
+
}
|
|
100
|
+
"""
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// What the expansions need to know about a validated `@Event` declaration: the property name, the
|
|
106
|
+
/// JS event name (after an `@Event("…")` override), and the payload type when the function type
|
|
107
|
+
/// takes one.
|
|
108
|
+
private struct EventMember {
|
|
109
|
+
let swiftName: String
|
|
110
|
+
let jsName: String
|
|
111
|
+
/// The payload type as written, or `nil` for a no-payload `() -> Void` event.
|
|
112
|
+
let payloadType: String?
|
|
113
|
+
/// Whether the event dispatches synchronously (`@Event(sync: true)`) via `emitSync`.
|
|
114
|
+
let isSync: Bool
|
|
115
|
+
|
|
116
|
+
var hasPayload: Bool {
|
|
117
|
+
return payloadType != nil
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Validates the declaration `@Event` is attached to and reads the event out of it. The checks
|
|
122
|
+
/// mirror what the expansion relies on: a single-binding instance `var` (the macro synthesizes a
|
|
123
|
+
/// computed getter, so `let`, accessors, and initializers are all incompatible) whose type is a
|
|
124
|
+
/// function type returning `Void` with at most one payload parameter.
|
|
125
|
+
private func validatedEvent(
|
|
126
|
+
of node: AttributeSyntax,
|
|
127
|
+
on declaration: some DeclSyntaxProtocol
|
|
128
|
+
) throws -> EventMember {
|
|
129
|
+
guard let varDecl = declaration.as(VariableDeclSyntax.self) else {
|
|
130
|
+
throw MacroExpansionErrorMessage("@Event can only be applied to a property")
|
|
131
|
+
}
|
|
132
|
+
if varDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
133
|
+
throw MacroExpansionErrorMessage(
|
|
134
|
+
"@Event and @JS cannot be combined on the same property; an event is exposed to JS on its own, so remove one of the attributes")
|
|
135
|
+
}
|
|
136
|
+
// The compiler also rejects accessor macros on a `let`, but with a generic message; this one says
|
|
137
|
+
// what to do instead and carries the fix-it doing it. The synthesized property is getter-only, so
|
|
138
|
+
// switching to `var` loses nothing.
|
|
139
|
+
if varDecl.bindingSpecifier.tokenKind == .keyword(.let) {
|
|
140
|
+
throw letBindingDiagnostic(for: varDecl)
|
|
141
|
+
}
|
|
142
|
+
if varDecl.modifiers.contains(where: isTypeLevelModifier) {
|
|
143
|
+
throw MacroExpansionErrorMessage(
|
|
144
|
+
"@Event must be an instance property; events are emitted from a module or shared object instance.")
|
|
145
|
+
}
|
|
146
|
+
guard varDecl.bindings.count == 1, let binding = varDecl.bindings.first,
|
|
147
|
+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
148
|
+
throw MacroExpansionErrorMessage(
|
|
149
|
+
"@Event must be applied to a single named property; declare each event separately")
|
|
150
|
+
}
|
|
151
|
+
if binding.initializer != nil {
|
|
152
|
+
throw MacroExpansionErrorMessage(
|
|
153
|
+
"@Event property cannot have an initial value; the macro synthesizes the closure")
|
|
154
|
+
}
|
|
155
|
+
if binding.accessorBlock != nil {
|
|
156
|
+
throw MacroExpansionErrorMessage(
|
|
157
|
+
"@Event property cannot declare its own accessors; the macro synthesizes the getter")
|
|
158
|
+
}
|
|
159
|
+
guard let functionType = underlyingFunctionType(of: binding.typeAnnotation?.type) else {
|
|
160
|
+
throw MacroExpansionErrorMessage(
|
|
161
|
+
"@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'")
|
|
162
|
+
}
|
|
163
|
+
guard isVoidReturn(functionType.returnClause.type) else {
|
|
164
|
+
throw MacroExpansionErrorMessage(
|
|
165
|
+
"@Event function type must return 'Void'; an event dispatches to JS and has no return value")
|
|
166
|
+
}
|
|
167
|
+
guard functionType.parameters.count <= 1 else {
|
|
168
|
+
throw MacroExpansionErrorMessage(
|
|
169
|
+
"@Event function type takes at most one payload parameter; combine multiple values into a single record")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let swiftName = identifier.identifier.text
|
|
173
|
+
return EventMember(
|
|
174
|
+
swiftName: swiftName,
|
|
175
|
+
jsName: jsNameArgument(of: node) ?? defaultEventName(for: swiftName),
|
|
176
|
+
payloadType: functionType.parameters.first?.type.trimmedDescription,
|
|
177
|
+
isSync: boolArgument(of: node, label: "sync") == true
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: - Default event name
|
|
182
|
+
|
|
183
|
+
/// The default JS event name for a property: the Swift name with the conventional `on` prefix
|
|
184
|
+
/// stripped and the remainder decapitalized (`onStatusChange` → `statusChange`). The two sides
|
|
185
|
+
/// idiomatically want different names: the Swift property reads as invoking a handler
|
|
186
|
+
/// (`self.onStatusChange(…)`) and the prefix keeps it from colliding with a state property
|
|
187
|
+
/// (`status`), while JS listens by bare name (`addListener("statusChange")`, the Node/DOM idiom
|
|
188
|
+
/// that module and shared-object events follow). Names without the prefix (`statusChange`,
|
|
189
|
+
/// `online`) pass through verbatim, and an explicit `@Event("name")` override is never
|
|
190
|
+
/// transformed — that's also the escape hatch for legacy `onX` wire names.
|
|
191
|
+
private func defaultEventName(for swiftName: String) -> String {
|
|
192
|
+
guard swiftName.hasPrefix("on") else {
|
|
193
|
+
return swiftName
|
|
194
|
+
}
|
|
195
|
+
let rest = swiftName.dropFirst(2)
|
|
196
|
+
guard let first = rest.first, first.isUppercase else {
|
|
197
|
+
return swiftName
|
|
198
|
+
}
|
|
199
|
+
return decapitalized(String(rest))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Lowercases the leading uppercase run the way Swift's API importer does: a single leading
|
|
203
|
+
/// capital is lowercased (`StatusChange` → `statusChange`); a longer acronym run keeps its last
|
|
204
|
+
/// capital when a lowercase letter follows it, since that capital starts the next word
|
|
205
|
+
/// (`URLChange` → `urlChange`, `URL` → `url`).
|
|
206
|
+
private func decapitalized(_ name: String) -> String {
|
|
207
|
+
let runEnd = name.firstIndex { !$0.isUppercase } ?? name.endIndex
|
|
208
|
+
if name[..<runEnd].count > 1 && runEnd != name.endIndex {
|
|
209
|
+
let lastCapital = name.index(before: runEnd)
|
|
210
|
+
return name[..<lastCapital].lowercased() + name[lastCapital...]
|
|
211
|
+
}
|
|
212
|
+
return name[..<runEnd].lowercased() + name[runEnd...]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MARK: - The `let` diagnostic
|
|
216
|
+
|
|
217
|
+
/// The error for `@Event let`, attached to the `let` keyword itself and carrying a fix-it that
|
|
218
|
+
/// replaces it with `var`. Unlike the other checks (plain thrown messages located at the attribute),
|
|
219
|
+
/// this one is a structured `Diagnostic` so Xcode can offer the one-click fix.
|
|
220
|
+
private func letBindingDiagnostic(for varDecl: VariableDeclSyntax) -> DiagnosticsError {
|
|
221
|
+
let specifier = varDecl.bindingSpecifier
|
|
222
|
+
let fixIt = FixIt(
|
|
223
|
+
message: EventFixItMessage("Replace 'let' with 'var'", id: "event-let-to-var"),
|
|
224
|
+
changes: [
|
|
225
|
+
// Rewriting just the token's kind keeps its surrounding trivia (indentation, the space
|
|
226
|
+
// before the property name) intact.
|
|
227
|
+
.replace(
|
|
228
|
+
oldNode: Syntax(specifier),
|
|
229
|
+
newNode: Syntax(specifier.with(\.tokenKind, .keyword(.var)))
|
|
230
|
+
)
|
|
231
|
+
]
|
|
232
|
+
)
|
|
233
|
+
let message = EventDiagnosticMessage(
|
|
234
|
+
"@Event must be applied to a 'var': it expands into a computed property, which a 'let' cannot be. The synthesized property is read-only anyway.",
|
|
235
|
+
id: "event-on-let"
|
|
236
|
+
)
|
|
237
|
+
return DiagnosticsError(diagnostics: [
|
|
238
|
+
Diagnostic(node: specifier, message: message, fixIts: [fixIt])
|
|
239
|
+
])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private struct EventDiagnosticMessage: DiagnosticMessage {
|
|
243
|
+
let message: String
|
|
244
|
+
let diagnosticID: MessageID
|
|
245
|
+
let severity: DiagnosticSeverity = .error
|
|
246
|
+
|
|
247
|
+
init(_ message: String, id: String) {
|
|
248
|
+
self.message = message
|
|
249
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private struct EventFixItMessage: FixItMessage {
|
|
254
|
+
let message: String
|
|
255
|
+
let fixItID: MessageID
|
|
256
|
+
|
|
257
|
+
init(_ message: String, id: String) {
|
|
258
|
+
self.message = message
|
|
259
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// MARK: - Declaration shape helpers
|
|
264
|
+
|
|
265
|
+
/// The spelled name of the innermost type declaration enclosing the macro, read from the lexical
|
|
266
|
+
/// context, so the emitter assertion can name the user's type in the conformance diagnostic
|
|
267
|
+
/// ("requires that 'MyModule' conform to 'EventEmitter'" instead of "'Self'"). Returns `nil` (the
|
|
268
|
+
/// caller falls back to `Self`) when there's no enclosing type, or when it's a generic type
|
|
269
|
+
/// declaration: `Foo.self` isn't valid for an unbound generic, while `Self` works anywhere. An
|
|
270
|
+
/// extension can't be detected as generic syntactically (see the extension case below).
|
|
271
|
+
private func enclosingTypeName(in context: some MacroExpansionContext) -> String? {
|
|
272
|
+
for scope in context.lexicalContext {
|
|
273
|
+
if let classDecl = scope.as(ClassDeclSyntax.self) {
|
|
274
|
+
return classDecl.genericParameterClause == nil ? classDecl.name.text : nil
|
|
275
|
+
}
|
|
276
|
+
if let structDecl = scope.as(StructDeclSyntax.self) {
|
|
277
|
+
return structDecl.genericParameterClause == nil ? structDecl.name.text : nil
|
|
278
|
+
}
|
|
279
|
+
if let actorDecl = scope.as(ActorDeclSyntax.self) {
|
|
280
|
+
return actorDecl.genericParameterClause == nil ? actorDecl.name.text : nil
|
|
281
|
+
}
|
|
282
|
+
if let extensionDecl = scope.as(ExtensionDeclSyntax.self) {
|
|
283
|
+
// An extension carries no generic-parameter clause of its own, so a bare extended type
|
|
284
|
+
// (`extension Box`) is indistinguishable from a non-generic one (`extension Foo`); both read
|
|
285
|
+
// as a plain identifier here. A written bound form (`extension Box<Int>`) is valid as `.self`,
|
|
286
|
+
// and the common non-generic case keeps its spelled name in the diagnostic. The unguarded gap
|
|
287
|
+
// is `extension <Generic>` with the parameters omitted, where the spelled name is an unbound
|
|
288
|
+
// generic invalid as `.self`; events on generic types in an extension are rare enough that the
|
|
289
|
+
// resulting compile error is an acceptable price for naming the user's type everywhere else.
|
|
290
|
+
return extensionDecl.extendedType.trimmedDescription
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return nil
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private func isTypeLevelModifier(_ modifier: DeclModifierSyntax) -> Bool {
|
|
297
|
+
return modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// The function type underlying a property's type annotation, unwrapping attributes
|
|
301
|
+
/// (`@Sendable (P) -> Void`) and single-element parentheses (`((P) -> Void)`). Returns `nil` when
|
|
302
|
+
/// the annotation is missing or isn't a function type, including an optional function type:
|
|
303
|
+
/// an event is always present, never `nil`.
|
|
304
|
+
private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax? {
|
|
305
|
+
guard let type else {
|
|
306
|
+
return nil
|
|
307
|
+
}
|
|
308
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
309
|
+
return underlyingFunctionType(of: attributed.baseType)
|
|
310
|
+
}
|
|
311
|
+
if let tuple = type.as(TupleTypeSyntax.self),
|
|
312
|
+
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil {
|
|
313
|
+
return underlyingFunctionType(of: element.type)
|
|
314
|
+
}
|
|
315
|
+
return type.as(FunctionTypeSyntax.self)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// True when the function type's return is written as `Void` / `()`. Function types always carry an
|
|
319
|
+
/// explicit return clause, so unlike a function declaration there's no "absent" case. A module-qualified
|
|
320
|
+
/// `Swift.Void` and redundant parentheses (`(Void)`, `(())`) are accepted too, so a valid void return
|
|
321
|
+
/// written one of those ways isn't rejected with a misleading "must return 'Void'" diagnostic.
|
|
322
|
+
private func isVoidReturn(_ type: TypeSyntax) -> Bool {
|
|
323
|
+
// Peel single-element, unlabeled parentheses: `(Void)` and `(())` are the same type as their content.
|
|
324
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
325
|
+
let element = tuple.elements.first, element.firstName == nil {
|
|
326
|
+
return isVoidReturn(element.type)
|
|
327
|
+
}
|
|
328
|
+
let text = type.trimmedDescription
|
|
329
|
+
return text == "Void" || text == "()" || text == "Swift.Void"
|
|
330
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Macro applied to a module class. It plays three roles, each implemented in its own
|
|
7
|
+
extension below:
|
|
8
|
+
|
|
9
|
+
- `MemberMacro`: binds `@JS`-marked members directly into the module's JS object via the
|
|
10
|
+
synthesized `_decorateModule`, and emits the resolved module name as a non-optional
|
|
11
|
+
`_jsName` static (no `Name(…)` DSL element). It also synthesizes a
|
|
12
|
+
framework-internal `_synthesizedDefinition()` returning `[AnyDefinition]` (now carrying
|
|
13
|
+
only nested `classes` entries) that `expo-modules-core` merges into the module's
|
|
14
|
+
definition. When the class doesn't already inherit `Module`/`BaseModule`, it also
|
|
15
|
+
synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
|
|
16
|
+
- `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
|
|
17
|
+
`@ModuleDefinitionBuilder` on a `definition()` method.
|
|
18
|
+
- `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
|
|
19
|
+
|
|
20
|
+
Inheriting from `Module` is therefore optional — a class can carry any superclass (or
|
|
21
|
+
none) and still be a module, because everything inheritance used to provide is synthesized.
|
|
22
|
+
|
|
23
|
+
@ExpoModule
|
|
24
|
+
public final class MyModule {
|
|
25
|
+
public func definition() -> ModuleDefinition {
|
|
26
|
+
Name("MyModule")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@JS
|
|
30
|
+
func greet(name: String) -> String { "Hi, \(name)" }
|
|
31
|
+
}
|
|
32
|
+
*/
|
|
33
|
+
public struct ExpoModuleMacro: MemberMacro {
|
|
34
|
+
public static func expansion(
|
|
35
|
+
of node: AttributeSyntax,
|
|
36
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
37
|
+
conformingTo protocols: [TypeSyntax],
|
|
38
|
+
in context: some MacroExpansionContext
|
|
39
|
+
) throws -> [DeclSyntax] {
|
|
40
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
41
|
+
throw MacroExpansionErrorMessage("@ExpoModule can only be applied to a class")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The module name is fully resolved here (the explicit `@ExpoModule("…")` argument, else the
|
|
45
|
+
// class name) and emitted as a non-optional `_jsName` static below — so the
|
|
46
|
+
// macro no longer emits a `Name(…)` DSL entry. Core reads the static for the module name,
|
|
47
|
+
// ahead of its type-name fallback (which then only applies to non-macro DSL modules).
|
|
48
|
+
let moduleName = jsNameArgument(of: node) ?? classDecl.name.text
|
|
49
|
+
var entries: [String] = []
|
|
50
|
+
|
|
51
|
+
// `@JS func`s (sync and async) and `@JS var`s are bound directly into the JS object by the
|
|
52
|
+
// synthesized `_decorateModule` rather than described with a `Function(...)` / `Property(...)`
|
|
53
|
+
// DSL entry, so they're collected here instead of appended to `entries`.
|
|
54
|
+
var functions: [JSFunction] = []
|
|
55
|
+
var properties: [JSProperty] = []
|
|
56
|
+
|
|
57
|
+
for typeName in classListArgument(of: node, label: "classes") {
|
|
58
|
+
entries.append("\(typeName)._synthesizedClassDefinition()")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for member in classDecl.memberBlock.members {
|
|
62
|
+
let decl = member.decl
|
|
63
|
+
|
|
64
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
65
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
66
|
+
functions.append(JSFunction(funcDecl: funcDecl, attribute: attribute))
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
71
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
72
|
+
properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let body: String
|
|
77
|
+
if entries.isEmpty {
|
|
78
|
+
body = " return []"
|
|
79
|
+
} else {
|
|
80
|
+
let lines = entries.map { " \($0)" }.joined(separator: ",\n")
|
|
81
|
+
body = " return [\n\(lines)\n ]"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var emitted: [DeclSyntax] = []
|
|
85
|
+
|
|
86
|
+
// The fully-resolved module name as a non-optional stored constant. Core reads this
|
|
87
|
+
// instead of the retired `Name(…)` DSL element; it feeds both native registration and the
|
|
88
|
+
// JS object name, so they can't diverge.
|
|
89
|
+
emitted.append("public static let _jsName = \"\(raw: moduleName)\"")
|
|
90
|
+
|
|
91
|
+
// View classes are registered by type, not spliced as definitions: an `@ExpoView` emits no
|
|
92
|
+
// definition function, so there is nothing to call on each entry. Core reads a view's props
|
|
93
|
+
// type from its `Props` typealias and its event names from that type's `_eventNames`, both
|
|
94
|
+
// static. Emitted only when the argument is present, so a module without views gains nothing.
|
|
95
|
+
let viewTypes = classListArgument(of: node, label: "views")
|
|
96
|
+
if !viewTypes.isEmpty {
|
|
97
|
+
let entries = viewTypes.map { "\($0).self" }.joined(separator: ", ")
|
|
98
|
+
emitted.append("public static let _viewTypes: [Any.Type] = [\(raw: entries)]")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// `Module`/`BaseModule` already provide `appContext` storage and the
|
|
102
|
+
// `init(appContext:)` requirement, so we only synthesize them for classes that
|
|
103
|
+
// inherit from neither. Each is skipped individually if the user wrote their own,
|
|
104
|
+
// to avoid emitting a duplicate declaration.
|
|
105
|
+
if !inheritsFromAny(classDecl, names: ["Module", "BaseModule"]) {
|
|
106
|
+
if !hasStoredProperty(classDecl, named: "appContext") {
|
|
107
|
+
emitted.append("public weak var appContext: AppContext?")
|
|
108
|
+
}
|
|
109
|
+
if !hasAppContextInitializer(classDecl) {
|
|
110
|
+
emitted.append(
|
|
111
|
+
"""
|
|
112
|
+
public required init(appContext: AppContext) {
|
|
113
|
+
self.appContext = appContext
|
|
114
|
+
}
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Always emitted: the definition is derived from the class name and `@JS` members,
|
|
121
|
+
// independent of any `definition()` the user wrote.
|
|
122
|
+
let method: DeclSyntax = """
|
|
123
|
+
public func _synthesizedDefinition() -> [AnyDefinition] {
|
|
124
|
+
\(raw: body)
|
|
125
|
+
}
|
|
126
|
+
"""
|
|
127
|
+
emitted.append(method)
|
|
128
|
+
|
|
129
|
+
// Direct JSI binding: one `_decorateModule` that binds each `@JS func` (inlined `setProperty`
|
|
130
|
+
// closure) and each `@JS var` (a `defineProperty` get/set accessor) into the module's JS object.
|
|
131
|
+
// Only emitted when there's at least one member to bind.
|
|
132
|
+
if !functions.isEmpty || !properties.isEmpty {
|
|
133
|
+
emitted.append(buildDecorateJavaScriptObject(functions: functions, properties: properties))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return emitted
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
extension ExpoModuleMacro: MemberAttributeMacro {
|
|
141
|
+
public static func expansion(
|
|
142
|
+
of node: AttributeSyntax,
|
|
143
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
144
|
+
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
145
|
+
in context: some MacroExpansionContext
|
|
146
|
+
) throws -> [AttributeSyntax] {
|
|
147
|
+
// This macro is invoked once per member and may attach more than one attribute,
|
|
148
|
+
// so we collect into an array rather than returning early. The two checks below are
|
|
149
|
+
// independent — a given member matches at most one in practice, but they're written
|
|
150
|
+
// so neither precludes the other.
|
|
151
|
+
var attributes: [AttributeSyntax] = []
|
|
152
|
+
|
|
153
|
+
// `@JS` sync members run on the JS thread; stamp `@JavaScriptActor` so isolation is
|
|
154
|
+
// checked at compile time. Skipped when the member already chose an isolation
|
|
155
|
+
// (`async`, `nonisolated`, or another global actor) — see `shouldStampJavaScriptActor`.
|
|
156
|
+
// `@Event(sync: true)` members get the stamp too: a sync event dispatches inline, so the
|
|
157
|
+
// isolation forces its call site onto the JS thread. Async events (the default) are
|
|
158
|
+
// deliberately left unstamped — their `emit` schedules onto the JS thread itself.
|
|
159
|
+
if memberHasJSAttribute(member) || isSyncEventMember(member),
|
|
160
|
+
shouldStampJavaScriptActor(on: member, enclosedBy: declaration) {
|
|
161
|
+
attributes.append("@JavaScriptActor")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// `@JS(.concurrent)` is the inverse: instead of the JS-thread stamp the member gets
|
|
165
|
+
// `@concurrent`, so its body runs on the concurrent pool. `shouldStampJavaScriptActor` already
|
|
166
|
+
// skipped the stamp above, leaving the two mutually exclusive.
|
|
167
|
+
if isConcurrentJSMember(member) {
|
|
168
|
+
attributes.append("@concurrent")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Apply the result builder to `definition()` so the user doesn't have to. Skipped if
|
|
172
|
+
// they already wrote `@ModuleDefinitionBuilder` themselves, which would otherwise be
|
|
173
|
+
// a duplicate attribute.
|
|
174
|
+
if isModuleDefinitionFunction(member),
|
|
175
|
+
!memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") }) {
|
|
176
|
+
attributes.append("@ModuleDefinitionBuilder")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return attributes
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private func isModuleDefinitionFunction(_ member: some DeclSyntaxProtocol) -> Bool {
|
|
184
|
+
guard let funcDecl = member.as(FunctionDeclSyntax.self),
|
|
185
|
+
funcDecl.name.text == "definition",
|
|
186
|
+
funcDecl.signature.parameterClause.parameters.isEmpty,
|
|
187
|
+
let returnType = funcDecl.signature.returnClause?.type else {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
return baseIdentifier(of: returnType) == "ModuleDefinition"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private func hasAttributeName(_ element: AttributeListSyntax.Element, _ name: String) -> Bool {
|
|
194
|
+
guard let attribute = element.as(AttributeSyntax.self) else {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
return attribute.attributeName.trimmedDescription == name
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
extension ExpoModuleMacro: ExtensionMacro {
|
|
201
|
+
public static func expansion(
|
|
202
|
+
of node: AttributeSyntax,
|
|
203
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
204
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
205
|
+
conformingTo protocols: [TypeSyntax],
|
|
206
|
+
in context: some MacroExpansionContext
|
|
207
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
208
|
+
// `protocols` is empty when the compiler already sees the conformance (e.g. it's
|
|
209
|
+
// spelled in the declaration), in which case there's nothing for us to add.
|
|
210
|
+
guard !protocols.isEmpty else {
|
|
211
|
+
return []
|
|
212
|
+
}
|
|
213
|
+
// These base types already supply `AnyModule`; emitting a second conformance here
|
|
214
|
+
// would be redundant and error.
|
|
215
|
+
if let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
216
|
+
inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"]) {
|
|
217
|
+
return []
|
|
218
|
+
}
|
|
219
|
+
let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
|
|
220
|
+
return [conformance.cast(ExtensionDeclSyntax.self)]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// MARK: - Synthesized-member detection
|
|
225
|
+
|
|
226
|
+
private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String) -> Bool {
|
|
227
|
+
for member in classDecl.memberBlock.members {
|
|
228
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
for binding in varDecl.bindings {
|
|
232
|
+
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
233
|
+
identifier.identifier.text == name {
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
True if the class declares an initializer that satisfies the `init(appContext:)` protocol
|
|
243
|
+
requirement: a single parameter labeled `appContext` whose type is written as `AppContext`
|
|
244
|
+
(or `….AppContext`). Both must match — the label is part of the requirement, so
|
|
245
|
+
`init(c: AppContext)` does *not* satisfy it (we still synthesize ours), and the type guards
|
|
246
|
+
against a same-labeled init of an unrelated type. A macro can't resolve types, so this is a
|
|
247
|
+
syntactic match on the spelled name.
|
|
248
|
+
*/
|
|
249
|
+
private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
250
|
+
for member in classDecl.memberBlock.members {
|
|
251
|
+
guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else {
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
let params = initDecl.signature.parameterClause.parameters
|
|
255
|
+
guard params.count == 1, let param = params.first else {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
if param.firstName.text == "appContext", baseIdentifier(of: param.type) == "AppContext" {
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false
|
|
263
|
+
}
|