expo-modules-macros 0.21.0 → 0.23.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 +8 -5
- package/.github/workflows/publish.yml +26 -11
- package/.github/workflows/swift-format.yml +79 -0
- package/.github/workflows/swift.yml +7 -4
- package/.github/workflows/windows.yml +8 -5
- package/AGENTS.md +16 -9
- package/README.md +20 -5
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +3 -3
- package/apple/Package.swift +1 -2
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +144 -30
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +63 -68
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +184 -48
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +172 -43
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +286 -222
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +49 -56
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
- package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +20 -4
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +18 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +28 -8
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/index.d.ts +1 -1
- package/build/index.js +3 -3
- package/build/types.d.ts +32 -2
- package/build/types.js +1 -1
- package/package.json +8 -5
|
@@ -13,18 +13,27 @@ import SwiftSyntax
|
|
|
13
13
|
/// unwrap, arity guard, argument decode, all while `this` and the arguments are still valid)
|
|
14
14
|
/// and returns the async body (call and result encode), so nothing JSI-owned crosses the asynchronous
|
|
15
15
|
/// boundary and JS gets a promise.
|
|
16
|
+
///
|
|
17
|
+
/// A synchronous function marked `@JS(.withAsync("name"))` also binds a promise-returning variant
|
|
18
|
+
/// under that name (see `asyncVariant`). It decodes and encodes like an `async` function, and calls
|
|
19
|
+
/// the function on the concurrent pool.
|
|
16
20
|
internal struct JSFunction {
|
|
17
21
|
let swiftName: String
|
|
18
|
-
|
|
22
|
+
private(set) var jsName: String
|
|
19
23
|
let parameters: [FunctionParameterSyntax]
|
|
20
24
|
/// The declared return type as written, or `nil` when the function returns `Void`/nothing.
|
|
21
25
|
let returnType: String?
|
|
22
26
|
/// How the return value encodes when its type mentions `Any`, or `nil` for any other type.
|
|
23
27
|
let returnFreeForm: FreeFormConversion?
|
|
24
28
|
let isThrowing: Bool
|
|
25
|
-
|
|
29
|
+
private(set) var isAsync: Bool
|
|
26
30
|
/// The function's name in the source, where a JavaScript name collision is reported.
|
|
27
|
-
|
|
31
|
+
private(set) var nameToken: TokenSyntax
|
|
32
|
+
/// The name from `.withAsync("name")` and its token, or `nil` when the attribute doesn't list it.
|
|
33
|
+
private var asyncVariantName: (name: String, token: TokenSyntax)?
|
|
34
|
+
/// True for the binding `asyncVariant` returns: it calls the synchronous function on the concurrent
|
|
35
|
+
/// pool instead of on the JavaScript thread.
|
|
36
|
+
private(set) var isAsyncVariant = false
|
|
28
37
|
|
|
29
38
|
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
30
39
|
self.swiftName = funcDecl.name.text
|
|
@@ -39,6 +48,23 @@ internal struct JSFunction {
|
|
|
39
48
|
let effectSpecifiers = funcDecl.signature.effectSpecifiers
|
|
40
49
|
self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
|
|
41
50
|
self.isAsync = effectSpecifiers?.asyncSpecifier != nil
|
|
51
|
+
self.asyncVariantName = asyncVariantOption(of: attribute).flatMap(asyncVariantName(of:))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The promise-returning binding that `.withAsync("name")` adds next to a synchronous function, or
|
|
55
|
+
/// `nil` when the attribute doesn't list it. An `async` function gets none: `@JS` reports the option
|
|
56
|
+
/// there.
|
|
57
|
+
var asyncVariant: JSFunction? {
|
|
58
|
+
guard let asyncVariantName, !isAsync else {
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
var variant = self
|
|
62
|
+
variant.jsName = asyncVariantName.name
|
|
63
|
+
variant.nameToken = asyncVariantName.token
|
|
64
|
+
variant.isAsync = true
|
|
65
|
+
variant.isAsyncVariant = true
|
|
66
|
+
variant.asyncVariantName = nil
|
|
67
|
+
return variant
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
/// The number of leading parameters that must always be supplied: the total minus the maximal
|
|
@@ -79,14 +105,16 @@ internal struct JSFunction {
|
|
|
79
105
|
guard arguments.count == \(maximum) else {
|
|
80
106
|
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(jsName)", received: arguments.count, required: \(required), maximum: \(maximum)))
|
|
81
107
|
}
|
|
82
|
-
"""
|
|
108
|
+
"""
|
|
109
|
+
)
|
|
83
110
|
} else {
|
|
84
111
|
lines.append(
|
|
85
112
|
"""
|
|
86
113
|
guard arguments.count >= \(required) && arguments.count <= \(maximum) else {
|
|
87
114
|
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(jsName)", received: arguments.count, required: \(required), maximum: \(maximum)))
|
|
88
115
|
}
|
|
89
|
-
"""
|
|
116
|
+
"""
|
|
117
|
+
)
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
// Decode the required prefix once — these slots are present in every accepted arity, so the
|
|
@@ -142,7 +170,8 @@ internal struct JSFunction {
|
|
|
142
170
|
lines.append(contentsOf: encodeResultLines())
|
|
143
171
|
}
|
|
144
172
|
|
|
145
|
-
return
|
|
173
|
+
return
|
|
174
|
+
lines
|
|
146
175
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
147
176
|
.map { indent + $0 }
|
|
148
177
|
.joined(separator: "\n")
|
|
@@ -152,14 +181,22 @@ internal struct JSFunction {
|
|
|
152
181
|
/// the function's async body (`AsyncFunctionBody`), awaiting the call and encoding the result. It
|
|
153
182
|
/// captures only what the call needs — the decoded `arg<i>` locals, the receiver, and `runtime`
|
|
154
183
|
/// for the encode — never `this` or the arguments buffer, which are only valid for the duration
|
|
155
|
-
/// of the host call.
|
|
184
|
+
/// of the host call. The `asyncVariant` binding awaits its call on the concurrent pool instead.
|
|
156
185
|
private func asyncBodyLines(receiver: Receiver, arity: Int) -> [String] {
|
|
157
186
|
var lines: [String] = ["return {"]
|
|
187
|
+
let call =
|
|
188
|
+
isAsyncVariant
|
|
189
|
+
? concurrentCallExpression(receiver: receiver, arity: arity)
|
|
190
|
+
: callExpression(receiver: receiver, arity: arity)
|
|
191
|
+
// The concurrent call spans several lines, so each of its lines is indented into the body.
|
|
192
|
+
let indented = { (statement: String) in
|
|
193
|
+
statement.split(separator: "\n", omittingEmptySubsequences: false).map { " " + $0 }
|
|
194
|
+
}
|
|
158
195
|
if returnType != nil {
|
|
159
|
-
lines.append("
|
|
196
|
+
lines.append(contentsOf: indented("let result = \(call)"))
|
|
160
197
|
lines.append(contentsOf: encodeResultLines().map { " " + $0 })
|
|
161
198
|
} else {
|
|
162
|
-
lines.append(
|
|
199
|
+
lines.append(contentsOf: indented(call))
|
|
163
200
|
lines.append(" return .undefined()")
|
|
164
201
|
}
|
|
165
202
|
lines.append("}")
|
|
@@ -179,10 +216,36 @@ internal struct JSFunction {
|
|
|
179
216
|
)
|
|
180
217
|
}
|
|
181
218
|
|
|
182
|
-
/// The `<callee>.<name>(...)` call for the given arity
|
|
219
|
+
/// The `<callee>.<name>(...)` call for the given arity, with the arguments from `callArguments`.
|
|
220
|
+
private func callExpression(receiver: Receiver, arity: Int) -> String {
|
|
221
|
+
// `try` only for a throwing function. An `async` call that doesn't throw must not get one, or the
|
|
222
|
+
// expansion warns that no throwing call occurs within the `try` expression.
|
|
223
|
+
let tryKeyword = isThrowing ? "try " : ""
|
|
224
|
+
let awaitKeyword = isAsync ? "await " : ""
|
|
225
|
+
return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments(arity: arity)))"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// The call of the `asyncVariant` binding for the given arity: the synchronous function runs in a
|
|
229
|
+
/// `@concurrent` closure, so it leaves the JavaScript thread, and the body continues once it returns.
|
|
230
|
+
private func concurrentCallExpression(receiver: Receiver, arity: Int) -> String {
|
|
231
|
+
// The closure captures the receiver and the decoded arguments, so Swift 6 requires them to be
|
|
232
|
+
// `Sendable`, as it does for the receiver and arguments of a `.concurrent` call. The signature is
|
|
233
|
+
// spelled out because `@concurrent` needs an `async` closure, and a body without `await` would
|
|
234
|
+
// infer a synchronous one.
|
|
235
|
+
let tryKeyword = isThrowing ? "try " : ""
|
|
236
|
+
let effects = isThrowing ? "async throws" : "async"
|
|
237
|
+
let returnClause = returnType.map { " -> \(expressionType($0))" } ?? ""
|
|
238
|
+
return """
|
|
239
|
+
\(tryKeyword)await { @concurrent () \(effects)\(returnClause) in
|
|
240
|
+
\(tryKeyword)\(receiver.callee).\(swiftName)(\(callArguments(arity: arity)))
|
|
241
|
+
}()
|
|
242
|
+
"""
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// The argument list of a call for the given arity. Slots `0..<arity` are passed their decoded
|
|
183
246
|
/// `arg<i>`; a trailing optional-without-default slot that this arity omits is passed `nil`; a
|
|
184
247
|
/// trailing defaulted slot that this arity omits is dropped entirely so Swift applies its default.
|
|
185
|
-
private func
|
|
248
|
+
private func callArguments(arity: Int) -> String {
|
|
186
249
|
var callArguments: [String] = []
|
|
187
250
|
for (index, parameter) in parameters.enumerated() {
|
|
188
251
|
let label = parameter.firstName.text
|
|
@@ -201,11 +264,7 @@ internal struct JSFunction {
|
|
|
201
264
|
}
|
|
202
265
|
callArguments.append(label == "_" ? value : "\(label): \(value)")
|
|
203
266
|
}
|
|
204
|
-
|
|
205
|
-
// expansion warns that no throwing call occurs within the `try` expression.
|
|
206
|
-
let tryKeyword = isThrowing ? "try " : ""
|
|
207
|
-
let awaitKeyword = isAsync ? "await " : ""
|
|
208
|
-
return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments.joined(separator: ", ")))"
|
|
267
|
+
return callArguments.joined(separator: ", ")
|
|
209
268
|
}
|
|
210
269
|
|
|
211
270
|
/// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
|
|
@@ -304,6 +363,13 @@ internal struct JSFunction {
|
|
|
304
363
|
/// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
|
|
305
364
|
/// `JavaScriptEncodable.encode` (or the free-form methods for a type that mentions `Any`), the same
|
|
306
365
|
/// path as functions.
|
|
366
|
+
///
|
|
367
|
+
/// A `lazy var` binds a getter only, on a configurable accessor. On the first read the getter defines
|
|
368
|
+
/// the encoded value as a data property on the JS `this` with `defineProperty`, so later reads don't
|
|
369
|
+
/// call into native code. The data property goes on `this`, not on the object that holds the accessor,
|
|
370
|
+
/// so each shared object instance stores its own value. `defineProperty` leaves a frozen object
|
|
371
|
+
/// unchanged, so there the property stays an accessor. A `this` that isn't an object doesn't fail the read
|
|
372
|
+
/// either: the getter still returns the value.
|
|
307
373
|
internal struct JSProperty {
|
|
308
374
|
let swiftName: String
|
|
309
375
|
let jsName: String
|
|
@@ -314,8 +380,10 @@ internal struct JSProperty {
|
|
|
314
380
|
/// How the value converts when its type mentions `Any`, or `nil` for any other type.
|
|
315
381
|
let freeForm: FreeFormConversion?
|
|
316
382
|
/// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
|
|
317
|
-
/// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
|
|
383
|
+
/// explicit `set` accessor; `false` for a getter-only computed `var`, a `lazy var` or a `let`.
|
|
318
384
|
let isSettable: Bool
|
|
385
|
+
/// Whether the property is a `lazy var`, whose getter replaces the accessor with the first value.
|
|
386
|
+
let isLazy: Bool
|
|
319
387
|
/// The property's name in the source, where a JavaScript name collision is reported.
|
|
320
388
|
let nameToken: TokenSyntax
|
|
321
389
|
|
|
@@ -336,18 +404,37 @@ internal struct JSProperty {
|
|
|
336
404
|
|
|
337
405
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
338
406
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
407
|
+
if isLazy {
|
|
408
|
+
// The getter redefines the property on `this`, which needs a configurable accessor when `this`
|
|
409
|
+
// is the object that holds it (a module).
|
|
410
|
+
lines.append("\(descriptorName).setProperty(\"configurable\", value: true)")
|
|
411
|
+
}
|
|
339
412
|
|
|
340
413
|
// Getter: read `<callee>.<name>` and encode the result back to JS through `encode`. When the value
|
|
341
414
|
// type couldn't be inferred (no annotation and no literal default, rare for a stored var) there's
|
|
342
415
|
// no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
|
|
343
|
-
let
|
|
416
|
+
let encode: String
|
|
344
417
|
if let valueType {
|
|
345
|
-
|
|
418
|
+
encode = "try \(encodeCall(valueType, freeForm: freeForm, of: "\(callee).\(swiftName)"))"
|
|
346
419
|
} else {
|
|
347
|
-
|
|
420
|
+
encode = "\(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
421
|
+
}
|
|
422
|
+
let getBody: String
|
|
423
|
+
if isLazy {
|
|
424
|
+
let define = "this.getObject(in: runtime).defineProperty(\"\(jsName)\", value: value, options: [.enumerable])"
|
|
425
|
+
// A module getter ignores `this` otherwise, so it can be anything, and only an object can hold the
|
|
426
|
+
// value. A shared object's unwrap has already checked that `this` is an object.
|
|
427
|
+
let defineStatement =
|
|
428
|
+
receiver.unwrapStatement == nil
|
|
429
|
+
? "if this.isObject() {\n \(define)\n}"
|
|
430
|
+
: define
|
|
431
|
+
getBody = "let value = \(encode)\n\(defineStatement)\nreturn value"
|
|
432
|
+
} else {
|
|
433
|
+
getBody = "return \(encode)"
|
|
348
434
|
}
|
|
349
435
|
lines.append(
|
|
350
|
-
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(
|
|
436
|
+
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getBody)")
|
|
437
|
+
)
|
|
351
438
|
|
|
352
439
|
// Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
|
|
353
440
|
// typed setter needs a known value type; when the type couldn't be inferred the property is bound
|
|
@@ -358,12 +445,19 @@ internal struct JSProperty {
|
|
|
358
445
|
let setDecode = "\(callee).\(swiftName) = try \(decode)"
|
|
359
446
|
lines.append(
|
|
360
447
|
accessorClosure(
|
|
361
|
-
descriptorName,
|
|
448
|
+
descriptorName,
|
|
449
|
+
"set",
|
|
450
|
+
receiver: receiver,
|
|
451
|
+
returnsVoid: true,
|
|
452
|
+
body: "\(unwrap)\(setDecode)"
|
|
453
|
+
)
|
|
454
|
+
)
|
|
362
455
|
}
|
|
363
456
|
|
|
364
457
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
365
458
|
|
|
366
|
-
return
|
|
459
|
+
return
|
|
460
|
+
lines
|
|
367
461
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
368
462
|
.map { " " + $0 }
|
|
369
463
|
.joined(separator: "\n")
|
|
@@ -374,12 +468,17 @@ internal struct JSProperty {
|
|
|
374
468
|
/// A setter passes `returnsVoid`, so its closure declares `-> Void` and binds through the `Void`
|
|
375
469
|
/// overload, like a function that returns nothing.
|
|
376
470
|
private func accessorClosure(
|
|
377
|
-
_ descriptorName: String,
|
|
471
|
+
_ descriptorName: String,
|
|
472
|
+
_ key: String,
|
|
473
|
+
receiver: Receiver,
|
|
474
|
+
returnsVoid: Bool = false,
|
|
475
|
+
body: String
|
|
378
476
|
) -> String {
|
|
379
477
|
let captures = receiver.captureClause
|
|
380
478
|
// Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
|
|
381
479
|
// `\(body)` interpolation would only indent the first line.
|
|
382
|
-
let indentedBody =
|
|
480
|
+
let indentedBody =
|
|
481
|
+
body
|
|
383
482
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
384
483
|
.map { " \($0)" }
|
|
385
484
|
.joined(separator: "\n")
|
|
@@ -404,7 +503,10 @@ internal struct JSProperty {
|
|
|
404
503
|
/// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
|
|
405
504
|
/// metatype for a static member).
|
|
406
505
|
private func decorateBody(
|
|
407
|
-
functions: [JSFunction],
|
|
506
|
+
functions: [JSFunction],
|
|
507
|
+
properties: [JSProperty],
|
|
508
|
+
object: String,
|
|
509
|
+
receiver: Receiver
|
|
408
510
|
) -> String {
|
|
409
511
|
let functionBody = functions.map { $0.decorateStatements(object: object, receiver: receiver) }
|
|
410
512
|
let propertyBody = properties.map { $0.decorateStatements(object: object, receiver: receiver) }
|
|
@@ -421,7 +523,11 @@ private func decorateBody(
|
|
|
421
523
|
/// receiver; the shared-object counterpart is `buildDecorateSharedObjectPhase`.
|
|
422
524
|
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
423
525
|
let body = decorateBody(
|
|
424
|
-
functions: functions,
|
|
526
|
+
functions: functions,
|
|
527
|
+
properties: properties,
|
|
528
|
+
object: Phase.object.rawValue,
|
|
529
|
+
receiver: .module
|
|
530
|
+
)
|
|
425
531
|
return """
|
|
426
532
|
@JavaScriptActor
|
|
427
533
|
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
@@ -442,13 +548,21 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
442
548
|
/// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
|
|
443
549
|
/// is emitted only when the type has a member for it.
|
|
444
550
|
internal func buildDecorateSharedObjectPhase(
|
|
445
|
-
phase: Phase,
|
|
551
|
+
phase: Phase,
|
|
552
|
+
functions: [JSFunction],
|
|
553
|
+
properties: [JSProperty],
|
|
554
|
+
typeName: String
|
|
446
555
|
) -> DeclSyntax {
|
|
447
|
-
let receiver: Receiver =
|
|
556
|
+
let receiver: Receiver =
|
|
557
|
+
phase == .constructor
|
|
448
558
|
? .staticMember(typeName: typeName)
|
|
449
559
|
: .sharedObject(typeName: typeName)
|
|
450
560
|
let body = decorateBody(
|
|
451
|
-
functions: functions,
|
|
561
|
+
functions: functions,
|
|
562
|
+
properties: properties,
|
|
563
|
+
object: phase.rawValue,
|
|
564
|
+
receiver: receiver
|
|
565
|
+
)
|
|
452
566
|
return """
|
|
453
567
|
@JavaScriptActor
|
|
454
568
|
public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
@@ -54,7 +54,8 @@ public struct EventMacro: AccessorMacro {
|
|
|
54
54
|
// through the getter, so the body never has to spell the payload type. A sync event calls
|
|
55
55
|
// `emitSync` (inline dispatch, JS thread only); the default calls the scheduling `emit`.
|
|
56
56
|
let emitMethod = event.isSync ? "emitSync" : "emit"
|
|
57
|
-
let closure =
|
|
57
|
+
let closure =
|
|
58
|
+
event.hasPayload
|
|
58
59
|
? "{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload) }"
|
|
59
60
|
: "{ [weak self] in self?.\(emitMethod)(event: \"\(event.jsName)\") }"
|
|
60
61
|
return [
|
|
@@ -85,9 +86,10 @@ extension EventMacro: PeerMacro {
|
|
|
85
86
|
// and asserting the enclosing type by its spelled name so the conformance error names the
|
|
86
87
|
// user's type rather than 'Self'.
|
|
87
88
|
let name = event.swiftName
|
|
88
|
-
let payload = event.payloadType.flatMap(
|
|
89
|
+
let payload = event.payloadType.flatMap { assertableBoundaryType($0, constraint: jsConvertibleProtocolName) }
|
|
89
90
|
let owner = enclosingTypeName(in: context) ?? "Self"
|
|
90
|
-
let helper =
|
|
91
|
+
let helper =
|
|
92
|
+
payload != nil
|
|
91
93
|
? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
|
|
92
94
|
: "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
|
|
93
95
|
let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
|
|
@@ -132,7 +134,8 @@ private func validatedEvent(
|
|
|
132
134
|
}
|
|
133
135
|
if varDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
134
136
|
throw MacroExpansionErrorMessage(
|
|
135
|
-
"@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"
|
|
137
|
+
"@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"
|
|
138
|
+
)
|
|
136
139
|
}
|
|
137
140
|
// The compiler also rejects accessor macros on a `let`, but with a generic message; this one says
|
|
138
141
|
// what to do instead and carries the fix-it doing it. The synthesized property is getter-only, so
|
|
@@ -142,32 +145,40 @@ private func validatedEvent(
|
|
|
142
145
|
}
|
|
143
146
|
if varDecl.modifiers.contains(where: isTypeLevelModifier) {
|
|
144
147
|
throw MacroExpansionErrorMessage(
|
|
145
|
-
"@Event must be an instance property; events are emitted from a module or shared object instance."
|
|
148
|
+
"@Event must be an instance property; events are emitted from a module or shared object instance."
|
|
149
|
+
)
|
|
146
150
|
}
|
|
147
151
|
guard varDecl.bindings.count == 1, let binding = varDecl.bindings.first,
|
|
148
|
-
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
|
|
152
|
+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
|
|
153
|
+
else {
|
|
149
154
|
throw MacroExpansionErrorMessage(
|
|
150
|
-
"@Event must be applied to a single named property; declare each event separately"
|
|
155
|
+
"@Event must be applied to a single named property; declare each event separately"
|
|
156
|
+
)
|
|
151
157
|
}
|
|
152
158
|
if binding.initializer != nil {
|
|
153
159
|
throw MacroExpansionErrorMessage(
|
|
154
|
-
"@Event property cannot have an initial value; the macro synthesizes the closure"
|
|
160
|
+
"@Event property cannot have an initial value; the macro synthesizes the closure"
|
|
161
|
+
)
|
|
155
162
|
}
|
|
156
163
|
if binding.accessorBlock != nil {
|
|
157
164
|
throw MacroExpansionErrorMessage(
|
|
158
|
-
"@Event property cannot declare its own accessors; the macro synthesizes the getter"
|
|
165
|
+
"@Event property cannot declare its own accessors; the macro synthesizes the getter"
|
|
166
|
+
)
|
|
159
167
|
}
|
|
160
168
|
guard let functionType = underlyingFunctionType(of: binding.typeAnnotation?.type) else {
|
|
161
169
|
throw MacroExpansionErrorMessage(
|
|
162
|
-
"@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'"
|
|
170
|
+
"@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'"
|
|
171
|
+
)
|
|
163
172
|
}
|
|
164
173
|
guard isVoidReturn(functionType.returnClause.type) else {
|
|
165
174
|
throw MacroExpansionErrorMessage(
|
|
166
|
-
"@Event function type must return 'Void'; an event dispatches to JS and has no return value"
|
|
175
|
+
"@Event function type must return 'Void'; an event dispatches to JS and has no return value"
|
|
176
|
+
)
|
|
167
177
|
}
|
|
168
178
|
guard functionType.parameters.count <= 1 else {
|
|
169
179
|
throw MacroExpansionErrorMessage(
|
|
170
|
-
"@Event function type takes at most one payload parameter; combine multiple values into a single record"
|
|
180
|
+
"@Event function type takes at most one payload parameter; combine multiple values into a single record"
|
|
181
|
+
)
|
|
171
182
|
}
|
|
172
183
|
// The payload is encoded to JavaScript, and a native closure can't be encoded.
|
|
173
184
|
if let payloadClosure = functionType.parameters.first.flatMap({ firstFunctionType(in: $0.type) }) {
|
|
@@ -177,7 +188,8 @@ private func validatedEvent(
|
|
|
177
188
|
message: EventDiagnosticMessage(
|
|
178
189
|
"@Event payload can't hold a closure: the payload is encoded to JavaScript, and a native closure can't be encoded.",
|
|
179
190
|
id: "event-closure-payload"
|
|
180
|
-
)
|
|
191
|
+
)
|
|
192
|
+
)
|
|
181
193
|
])
|
|
182
194
|
}
|
|
183
195
|
|
|
@@ -321,7 +333,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
321
333
|
return underlyingFunctionType(of: attributed.baseType)
|
|
322
334
|
}
|
|
323
335
|
if let tuple = type.as(TupleTypeSyntax.self),
|
|
324
|
-
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
|
|
336
|
+
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
|
|
337
|
+
{
|
|
325
338
|
return underlyingFunctionType(of: element.type)
|
|
326
339
|
}
|
|
327
340
|
return type.as(FunctionTypeSyntax.self)
|
|
@@ -334,7 +347,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
334
347
|
private func isVoidReturn(_ type: TypeSyntax) -> Bool {
|
|
335
348
|
// Peel single-element, unlabeled parentheses: `(Void)` and `(())` are the same type as their content.
|
|
336
349
|
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
337
|
-
let element = tuple.elements.first, element.firstName == nil
|
|
350
|
+
let element = tuple.elements.first, element.firstName == nil
|
|
351
|
+
{
|
|
338
352
|
return isVoidReturn(element.type)
|
|
339
353
|
}
|
|
340
354
|
let text = type.trimmedDescription
|
|
@@ -2,34 +2,31 @@ import SwiftSyntax
|
|
|
2
2
|
import SwiftSyntaxBuilder
|
|
3
3
|
import SwiftSyntaxMacros
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- `
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
func greet(name: String) -> String { "Hi, \(name)" }
|
|
31
|
-
}
|
|
32
|
-
*/
|
|
5
|
+
/// Macro applied to a module class. It plays three roles, each implemented in its own
|
|
6
|
+
/// extension below:
|
|
7
|
+
///
|
|
8
|
+
/// - `MemberMacro`: binds `@JS`-marked members directly into the module's JS object via the
|
|
9
|
+
/// synthesized `_decorateModule`, and emits the resolved module name as a non-optional
|
|
10
|
+
/// `_jsName` static (no `Name(…)` DSL element). The `classes:` and `views:` arguments become
|
|
11
|
+
/// `_classTypes` and `_viewTypes` statics that `expo-modules-core` registers by type.
|
|
12
|
+
/// When the class doesn't already inherit `Module`/`BaseModule`, it also
|
|
13
|
+
/// synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
|
|
14
|
+
/// - `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
|
|
15
|
+
/// `@ModuleDefinitionBuilder` on a `definition()` method.
|
|
16
|
+
/// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
|
|
17
|
+
///
|
|
18
|
+
/// Inheriting from `Module` is therefore optional — a class can carry any superclass (or
|
|
19
|
+
/// none) and still be a module, because everything inheritance used to provide is synthesized.
|
|
20
|
+
///
|
|
21
|
+
/// @ExpoModule
|
|
22
|
+
/// public final class MyModule {
|
|
23
|
+
/// public func definition() -> ModuleDefinition {
|
|
24
|
+
/// Name("MyModule")
|
|
25
|
+
/// }
|
|
26
|
+
///
|
|
27
|
+
/// @JS
|
|
28
|
+
/// func greet(name: String) -> String { "Hi, \(name)" }
|
|
29
|
+
/// }
|
|
33
30
|
public struct ExpoModuleMacro: MemberMacro {
|
|
34
31
|
public static func expansion(
|
|
35
32
|
of node: AttributeSyntax,
|
|
@@ -46,43 +43,36 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
46
43
|
// macro no longer emits a `Name(…)` DSL entry. Core reads the static for the module name,
|
|
47
44
|
// ahead of its type-name fallback (which then only applies to non-macro DSL modules).
|
|
48
45
|
let moduleName = jsNameArgument(of: node) ?? withoutBackticks(classDecl.name.text)
|
|
49
|
-
var entries: [String] = []
|
|
50
46
|
|
|
51
47
|
// `@JS func`s (sync and async) and `@JS var`s are bound directly into the JS object by the
|
|
52
48
|
// synthesized `_decorateModule` rather than described with a `Function(...)` / `Property(...)`
|
|
53
|
-
// DSL entry
|
|
49
|
+
// DSL entry.
|
|
54
50
|
var functions: [JSFunction] = []
|
|
55
51
|
var properties: [JSProperty] = []
|
|
56
52
|
|
|
57
|
-
for typeName in classListArgument(of: node, label: "classes") {
|
|
58
|
-
entries.append("\(typeName)._synthesizedClassDefinition()")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
53
|
for member in classDecl.memberBlock.members {
|
|
62
54
|
let decl = member.decl
|
|
63
55
|
|
|
64
56
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
65
|
-
let attribute = funcDecl.attributes.firstAttribute(named: "JS")
|
|
66
|
-
|
|
57
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS")
|
|
58
|
+
{
|
|
59
|
+
let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
|
|
60
|
+
functions.append(function)
|
|
61
|
+
if let variant = function.asyncVariant {
|
|
62
|
+
functions.append(variant)
|
|
63
|
+
}
|
|
67
64
|
continue
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
71
|
-
let attribute = varDecl.attributes.firstAttribute(named: "JS")
|
|
68
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS")
|
|
69
|
+
{
|
|
72
70
|
properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
diagnoseJSNameCollisions(functions: functions, properties: properties, in: context)
|
|
77
75
|
|
|
78
|
-
let body: String
|
|
79
|
-
if entries.isEmpty {
|
|
80
|
-
body = " return []"
|
|
81
|
-
} else {
|
|
82
|
-
let lines = entries.map { " \($0)" }.joined(separator: ",\n")
|
|
83
|
-
body = " return [\n\(lines)\n ]"
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
var emitted: [DeclSyntax] = []
|
|
87
77
|
|
|
88
78
|
// The fully-resolved module name as a non-optional stored constant. Core reads this
|
|
@@ -90,6 +80,16 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
90
80
|
// JS object name, so they can't diverge.
|
|
91
81
|
emitted.append("public static let _jsName = \"\(raw: moduleName)\"")
|
|
92
82
|
|
|
83
|
+
// Shared object classes are registered by type: core builds each JavaScript class from the
|
|
84
|
+
// `@SharedObject` hooks, with the name from the class's `_jsName`. The element type makes the
|
|
85
|
+
// compiler reject a class that isn't a `SharedObject` subclass. Emitted only when the argument is
|
|
86
|
+
// present, so a module without classes gains nothing.
|
|
87
|
+
let classTypes = classListArgument(of: node, label: "classes")
|
|
88
|
+
if !classTypes.isEmpty {
|
|
89
|
+
let entries = classTypes.map { "\($0).self" }.joined(separator: ", ")
|
|
90
|
+
emitted.append("public static let _classTypes: [SharedObject.Type] = [\(raw: entries)]")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
93
|
// View classes are registered by type, not spliced as definitions: an `@ExpoView` emits no
|
|
94
94
|
// definition function, so there is nothing to call on each entry. Core reads a view's props
|
|
95
95
|
// type from its `Props` typealias and its event names from that type's `_eventNames`, both
|
|
@@ -119,15 +119,6 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Always emitted: the definition is derived from the class name and `@JS` members,
|
|
123
|
-
// independent of any `definition()` the user wrote.
|
|
124
|
-
let method: DeclSyntax = """
|
|
125
|
-
public func _synthesizedDefinition() -> [AnyDefinition] {
|
|
126
|
-
\(raw: body)
|
|
127
|
-
}
|
|
128
|
-
"""
|
|
129
|
-
emitted.append(method)
|
|
130
|
-
|
|
131
122
|
// Direct JSI binding: one `_decorateModule` that binds each `@JS func` (inlined `setProperty`
|
|
132
123
|
// closure) and each `@JS var` (a `defineProperty` get/set accessor) into the module's JS object.
|
|
133
124
|
// Only emitted when there's at least one member to bind.
|
|
@@ -154,12 +145,14 @@ extension ExpoModuleMacro: MemberAttributeMacro {
|
|
|
154
145
|
|
|
155
146
|
// `@JS` sync members run on the JS thread; stamp `@JavaScriptActor` so isolation is
|
|
156
147
|
// checked at compile time. Skipped when the member already chose an isolation
|
|
157
|
-
// (`
|
|
148
|
+
// (`nonisolated` or another global actor), or when `.concurrent` or `.withAsync("name")` runs
|
|
149
|
+
// it on the concurrent pool. See `shouldStampJavaScriptActor`.
|
|
158
150
|
// `@Event(sync: true)` members get the stamp too: a sync event dispatches inline, so the
|
|
159
151
|
// isolation forces its call site onto the JS thread. Async events (the default) are
|
|
160
152
|
// deliberately left unstamped — their `emit` schedules onto the JS thread itself.
|
|
161
153
|
if memberHasJSAttribute(member) || isSyncEventMember(member),
|
|
162
|
-
shouldStampJavaScriptActor(on: member, enclosedBy: declaration)
|
|
154
|
+
shouldStampJavaScriptActor(on: member, enclosedBy: declaration)
|
|
155
|
+
{
|
|
163
156
|
attributes.append("@JavaScriptActor")
|
|
164
157
|
}
|
|
165
158
|
|
|
@@ -174,7 +167,8 @@ extension ExpoModuleMacro: MemberAttributeMacro {
|
|
|
174
167
|
// they already wrote `@ModuleDefinitionBuilder` themselves, which would otherwise be
|
|
175
168
|
// a duplicate attribute.
|
|
176
169
|
if isModuleDefinitionFunction(member),
|
|
177
|
-
!memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") })
|
|
170
|
+
!memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") })
|
|
171
|
+
{
|
|
178
172
|
attributes.append("@ModuleDefinitionBuilder")
|
|
179
173
|
}
|
|
180
174
|
|
|
@@ -186,7 +180,8 @@ private func isModuleDefinitionFunction(_ member: some DeclSyntaxProtocol) -> Bo
|
|
|
186
180
|
guard let funcDecl = member.as(FunctionDeclSyntax.self),
|
|
187
181
|
funcDecl.name.text == "definition",
|
|
188
182
|
funcDecl.signature.parameterClause.parameters.isEmpty,
|
|
189
|
-
let returnType = funcDecl.signature.returnClause?.type
|
|
183
|
+
let returnType = funcDecl.signature.returnClause?.type
|
|
184
|
+
else {
|
|
190
185
|
return false
|
|
191
186
|
}
|
|
192
187
|
return baseIdentifier(of: returnType) == "ModuleDefinition"
|
|
@@ -215,7 +210,8 @@ extension ExpoModuleMacro: ExtensionMacro {
|
|
|
215
210
|
// These base types already supply `AnyModule`; emitting a second conformance here
|
|
216
211
|
// would be redundant and error.
|
|
217
212
|
if let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
218
|
-
inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
|
|
213
|
+
inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
|
|
214
|
+
{
|
|
219
215
|
return []
|
|
220
216
|
}
|
|
221
217
|
let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
|
|
@@ -232,7 +228,8 @@ private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String)
|
|
|
232
228
|
}
|
|
233
229
|
for binding in varDecl.bindings {
|
|
234
230
|
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
235
|
-
identifier.identifier.text == name
|
|
231
|
+
identifier.identifier.text == name
|
|
232
|
+
{
|
|
236
233
|
return true
|
|
237
234
|
}
|
|
238
235
|
}
|
|
@@ -240,14 +237,12 @@ private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String)
|
|
|
240
237
|
return false
|
|
241
238
|
}
|
|
242
239
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
syntactic match on the spelled name.
|
|
250
|
-
*/
|
|
240
|
+
/// True if the class declares an initializer that satisfies the `init(appContext:)` protocol
|
|
241
|
+
/// requirement: a single parameter labeled `appContext` whose type is written as `AppContext`
|
|
242
|
+
/// (or `….AppContext`). Both must match — the label is part of the requirement, so
|
|
243
|
+
/// `init(c: AppContext)` does *not* satisfy it (we still synthesize ours), and the type guards
|
|
244
|
+
/// against a same-labeled init of an unrelated type. A macro can't resolve types, so this is a
|
|
245
|
+
/// syntactic match on the spelled name.
|
|
251
246
|
private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
252
247
|
for member in classDecl.memberBlock.members {
|
|
253
248
|
guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else {
|