@expo/expo-modules-macros-plugin 0.2.0 → 0.2.2
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/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +297 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +47 -14
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +82 -18
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +21 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +12 -23
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +105 -0
- package/package.json +1 -1
- package/apple/Sources/ExpoModulesMacros/DecorateFunctionBuilder.swift +0 -174
|
Binary file
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
|
|
5
|
+
`Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call,
|
|
6
|
+
`@ExpoModule` synthesizes a `_decorateModule` that binds each such function into the module's JS object
|
|
7
|
+
via the closure-taking `JavaScriptObject.setProperty(_:)`, with the decode-call-encode body
|
|
8
|
+
inlined into the closure. This omits the `[Any]`/`toTuple` dynamic-call path: every argument is
|
|
9
|
+
decoded individually by its static type.
|
|
10
|
+
|
|
11
|
+
The receiver is the module's real `self` (a module is a singleton instance), so the body calls
|
|
12
|
+
`self.<name>(...)` directly and ignores the JS `this`. An `async` `@JS func` produces an `async`
|
|
13
|
+
closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
|
|
14
|
+
*/
|
|
15
|
+
internal struct JSFunction {
|
|
16
|
+
let swiftName: String
|
|
17
|
+
let jsName: String
|
|
18
|
+
let parameters: [FunctionParameterSyntax]
|
|
19
|
+
/// The declared return type as written, or `nil` when the function returns `Void`/nothing.
|
|
20
|
+
let returnType: String?
|
|
21
|
+
let isThrowing: Bool
|
|
22
|
+
let isAsync: Bool
|
|
23
|
+
|
|
24
|
+
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
25
|
+
self.swiftName = funcDecl.name.text
|
|
26
|
+
self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
|
|
27
|
+
self.parameters = Array(funcDecl.signature.parameterClause.parameters)
|
|
28
|
+
|
|
29
|
+
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
30
|
+
self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
|
|
31
|
+
|
|
32
|
+
let effectSpecifiers = funcDecl.signature.effectSpecifiers
|
|
33
|
+
self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
|
|
34
|
+
self.isAsync = effectSpecifiers?.asyncSpecifier != nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// The decode-call-encode statements that form the host-function body, indented with the given
|
|
38
|
+
/// prefix. Arity guard, then per-argument decode (primitives via a direct typed accessor like
|
|
39
|
+
/// `asDouble()` on a zero-copy `arguments.unownedValue(at:)`, others via `getDynamicType().cast(...)`),
|
|
40
|
+
/// the `self.<name>(...)` call, and the
|
|
41
|
+
/// result encode (primitives via `toJavaScriptValue(in:)`, others via `castToJS(...)`).
|
|
42
|
+
private func bodyStatements(indent: String) -> String {
|
|
43
|
+
var lines: [String] = []
|
|
44
|
+
|
|
45
|
+
lines.append(
|
|
46
|
+
"""
|
|
47
|
+
guard arguments.count == \(parameters.count) else {
|
|
48
|
+
throw Exception(name: "InvalidArgumentCount", description: "Function '\(jsName)' expects \(parameters.count) argument(s), but got \\(arguments.count)")
|
|
49
|
+
}
|
|
50
|
+
""")
|
|
51
|
+
|
|
52
|
+
var callArguments: [String] = []
|
|
53
|
+
for (index, parameter) in parameters.enumerated() {
|
|
54
|
+
let type = parameter.type.trimmedDescription
|
|
55
|
+
|
|
56
|
+
// Primitives decode through a direct typed accessor (`asDouble()`, etc.) on a borrowed
|
|
57
|
+
// `JavaScriptUnownedValue` — no owning `JavaScriptValue` allocation, no `jsi::Value` copy, no
|
|
58
|
+
// `getDynamicType()` allocation, no `Any` boxing, no force-cast — while still validating and
|
|
59
|
+
// throwing `TypeError` on a mismatch. Other types fall back to the dynamic converter, which
|
|
60
|
+
// needs an owning value, so they index the buffer directly.
|
|
61
|
+
if let accessor = fastDecodeAccessor(for: type) {
|
|
62
|
+
lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
|
|
63
|
+
} else {
|
|
64
|
+
lines.append(
|
|
65
|
+
"let arg\(index) = try \(type).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(type)")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let label = parameter.firstName.text
|
|
69
|
+
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let tryKeyword = (isThrowing || isAsync) ? "try " : ""
|
|
73
|
+
let awaitKeyword = isAsync ? "await " : ""
|
|
74
|
+
let callExpression =
|
|
75
|
+
"\(tryKeyword)\(awaitKeyword)self.\(swiftName)(\(callArguments.joined(separator: ", ")))"
|
|
76
|
+
|
|
77
|
+
if let returnType {
|
|
78
|
+
lines.append("let result = \(callExpression)")
|
|
79
|
+
// Primitives encode through `toJavaScriptValue(in:)` (the typed `JavaScriptRepresentable`
|
|
80
|
+
// conversion) — no `Any`, no dynamic-type allocation. Others go through the dynamic converter.
|
|
81
|
+
if fastDecodeAccessor(for: returnType) != nil {
|
|
82
|
+
lines.append("return result.toJavaScriptValue(in: runtime)")
|
|
83
|
+
} else {
|
|
84
|
+
lines.append("return try \(returnType).getDynamicType().castToJS(result, appContext: appContext, in: runtime)")
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
lines.append(callExpression)
|
|
88
|
+
lines.append("return .undefined")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines
|
|
92
|
+
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
93
|
+
.map { indent + $0 }
|
|
94
|
+
.joined(separator: "\n")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
98
|
+
/// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
|
|
99
|
+
/// (which creates the host function under the hood) — no separate named binding. For an `async`
|
|
100
|
+
/// function the body `await`s the call, which selects the async `setProperty` overload (so JS
|
|
101
|
+
/// receives a promise).
|
|
102
|
+
///
|
|
103
|
+
/// Capture mirrors core's `SyncFunctionDefinition.build`: `self` (the module) is captured
|
|
104
|
+
/// **strong** — the host-function closure is what keeps the native callable alive for as long as
|
|
105
|
+
/// JS can invoke it; its lifetime is bounded by the JS VM's garbage collection of the object.
|
|
106
|
+
/// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
|
|
107
|
+
/// the app context. When no argument or return value goes through the dynamic-type converter the
|
|
108
|
+
/// body never references `appContext`, so the capture and guard are omitted to avoid the
|
|
109
|
+
/// unused-capture warning.
|
|
110
|
+
var decorateStatements: String {
|
|
111
|
+
if usesAppContext {
|
|
112
|
+
return """
|
|
113
|
+
object.setProperty("\(jsName)") { [weak appContext, self] this, arguments in
|
|
114
|
+
guard let appContext else {
|
|
115
|
+
throw Exceptions.AppContextLost()
|
|
116
|
+
}
|
|
117
|
+
\(bodyStatements(indent: " "))
|
|
118
|
+
}
|
|
119
|
+
"""
|
|
120
|
+
}
|
|
121
|
+
return """
|
|
122
|
+
object.setProperty("\(jsName)") { [self] this, arguments in
|
|
123
|
+
\(bodyStatements(indent: " "))
|
|
124
|
+
}
|
|
125
|
+
"""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// True when the host-function body references `appContext` — i.e. some parameter or the return
|
|
129
|
+
/// type lacks a fast accessor and decodes/encodes through `getDynamicType()`, which threads
|
|
130
|
+
/// `appContext` in.
|
|
131
|
+
private var usesAppContext: Bool {
|
|
132
|
+
if parameters.contains(where: { fastDecodeAccessor(for: $0.type.trimmedDescription) == nil }) {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
if let returnType, fastDecodeAccessor(for: returnType) == nil {
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
143
|
+
/// `Property(...)` DSL entry, `@ExpoModule` synthesizes a get/set accessor into the module's JS
|
|
144
|
+
/// object inside `_decorateModule`: it builds a descriptor object (`enumerable` + `get`, and `set`
|
|
145
|
+
/// when the property is settable) and installs it with `object.defineProperty(name, descriptor:)`,
|
|
146
|
+
/// mirroring core's `PropertyDefinition.buildDescriptor`. The `get`/`set` host functions are
|
|
147
|
+
/// installed the same way `@JS func`s are — the closure-taking `setProperty(_:)` overload, with the
|
|
148
|
+
/// read/write body inlined into the closure.
|
|
149
|
+
///
|
|
150
|
+
/// The receiver is the module's real `self`, so the getter reads `self.<name>` and the setter writes
|
|
151
|
+
/// `self.<name> = …` directly, ignoring the JS `this`. Decode/encode of the value reuse the same
|
|
152
|
+
/// static-type fast path as functions (primitives through a direct typed accessor / `toJavaScriptValue`,
|
|
153
|
+
/// other types through the `getDynamicType()` converter).
|
|
154
|
+
internal struct JSProperty {
|
|
155
|
+
let swiftName: String
|
|
156
|
+
let jsName: String
|
|
157
|
+
/// The property's value type as written, or `nil` when it couldn't be inferred (no annotation and
|
|
158
|
+
/// no literal default). When `nil` the getter still works (the encode infers from `self.<name>`)
|
|
159
|
+
/// but the setter uses an untyped closure parameter.
|
|
160
|
+
let valueType: String?
|
|
161
|
+
/// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
|
|
162
|
+
/// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
|
|
163
|
+
let isSettable: Bool
|
|
164
|
+
|
|
165
|
+
/// The statements that install this property's accessor on the JS object, indented for the
|
|
166
|
+
/// `_decorateModule` body. Builds a descriptor object (`enumerable` + `get`, and `set` when
|
|
167
|
+
/// settable) via the closure-taking `setProperty(_:)` overload — with the read/write body inlined
|
|
168
|
+
/// into each closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture
|
|
169
|
+
/// matches the function bindings: `self` strong, `appContext` weak + guarded — and, like functions,
|
|
170
|
+
/// the `appContext` capture + guard are omitted from an accessor whose body never references it (a
|
|
171
|
+
/// primitive value, decoded/encoded without the dynamic converter), to avoid the unused-capture
|
|
172
|
+
/// warning. Getter and setter are gated independently.
|
|
173
|
+
var decorateStatements: String {
|
|
174
|
+
let descriptorName = "\(swiftName)Descriptor"
|
|
175
|
+
// A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
|
|
176
|
+
// references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
|
|
177
|
+
// getter, which also doesn't use it.
|
|
178
|
+
let usesAppContext = valueType.map { fastDecodeAccessor(for: $0) == nil } ?? false
|
|
179
|
+
var lines: [String] = []
|
|
180
|
+
|
|
181
|
+
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
182
|
+
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
183
|
+
|
|
184
|
+
// Getter: read `self.<name>` and encode the result back to JS.
|
|
185
|
+
let getEncode: String
|
|
186
|
+
if let valueType, fastDecodeAccessor(for: valueType) != nil {
|
|
187
|
+
getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
|
|
188
|
+
} else if let valueType {
|
|
189
|
+
getEncode =
|
|
190
|
+
"return try \(valueType).getDynamicType().castToJS(self.\(swiftName), appContext: appContext, in: runtime)"
|
|
191
|
+
} else {
|
|
192
|
+
// No known type: fall back to converting whatever `self.<name>` is. This only happens when the
|
|
193
|
+
// declaration has neither an annotation nor a literal default, which is rare for a stored var.
|
|
194
|
+
getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
|
|
195
|
+
}
|
|
196
|
+
lines.append(accessorClosure(descriptorName, "get", usesAppContext: usesAppContext, body: getEncode))
|
|
197
|
+
|
|
198
|
+
// Setter: decode argument 0 by the static type and write `self.<name>`. A typed setter needs a
|
|
199
|
+
// known value type; when the type couldn't be inferred the property is bound getter-only (a
|
|
200
|
+
// settable var with neither an annotation nor a literal default is rare and can't be decoded).
|
|
201
|
+
if isSettable, let valueType {
|
|
202
|
+
let setDecode: String
|
|
203
|
+
if let accessor = fastDecodeAccessor(for: valueType) {
|
|
204
|
+
setDecode = "self.\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
|
|
205
|
+
} else {
|
|
206
|
+
setDecode =
|
|
207
|
+
"self.\(swiftName) = try \(valueType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(valueType)"
|
|
208
|
+
}
|
|
209
|
+
lines.append(
|
|
210
|
+
accessorClosure(descriptorName, "set", usesAppContext: usesAppContext, body: "\(setDecode)\nreturn .undefined"))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
lines.append("object.defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
214
|
+
|
|
215
|
+
return lines
|
|
216
|
+
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
217
|
+
.map { " " + $0 }
|
|
218
|
+
.joined(separator: "\n")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry. Captures `self` strong and, when
|
|
222
|
+
/// `usesAppContext`, `appContext` weak + guarded (matching the function bindings); otherwise the
|
|
223
|
+
/// capture and guard are omitted so a primitive accessor doesn't warn on an unused capture.
|
|
224
|
+
private func accessorClosure(
|
|
225
|
+
_ descriptorName: String, _ key: String, usesAppContext: Bool, body: String
|
|
226
|
+
) -> String {
|
|
227
|
+
// Indent each line of a (possibly multi-line) body to sit one level inside the closure, aligned
|
|
228
|
+
// with the `guard`; a bare `\(body)` interpolation would only indent the first line.
|
|
229
|
+
let indentedBody = body
|
|
230
|
+
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
231
|
+
.map { " \($0)" }
|
|
232
|
+
.joined(separator: "\n")
|
|
233
|
+
if usesAppContext {
|
|
234
|
+
return """
|
|
235
|
+
\(descriptorName).setProperty("\(key)") { [weak appContext, self] this, arguments in
|
|
236
|
+
guard let appContext else {
|
|
237
|
+
throw Exceptions.AppContextLost()
|
|
238
|
+
}
|
|
239
|
+
\(indentedBody)
|
|
240
|
+
}
|
|
241
|
+
"""
|
|
242
|
+
}
|
|
243
|
+
return """
|
|
244
|
+
\(descriptorName).setProperty("\(key)") { [self] this, arguments in
|
|
245
|
+
\(indentedBody)
|
|
246
|
+
}
|
|
247
|
+
"""
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// The single generated function that decorates the module's JS object. Core supplies the object;
|
|
252
|
+
/// this binds every `@JS func` (via an inlined `setProperty` closure) and every `@JS var` (via a
|
|
253
|
+
/// `defineProperty` accessor) into it. Mirrors core's `ObjectDefinition.decorate(object:)`, including
|
|
254
|
+
/// its `borrowing` object parameter (it mutates through the reference without reassigning or taking
|
|
255
|
+
/// ownership). Named `_decorateModule` with the leading-underscore convention for synthesized members
|
|
256
|
+
/// the **runtime calls by name**; the `ExpoModule` suffix names the `@ExpoModule` macro it came from (a
|
|
257
|
+
/// shared object's counterpart is `_decorateSharedObject`).
|
|
258
|
+
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
259
|
+
let functionBody = functions.map { $0.decorateStatements }
|
|
260
|
+
let propertyBody = properties.map { $0.decorateStatements }
|
|
261
|
+
let body = (functionBody + propertyBody).joined(separator: "\n")
|
|
262
|
+
return """
|
|
263
|
+
@JavaScriptActor
|
|
264
|
+
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
265
|
+
\(raw: body)
|
|
266
|
+
}
|
|
267
|
+
"""
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly,
|
|
271
|
+
/// bypassing the dynamic-type converter (`asDouble()` for `Double`, etc.). Returns `nil` for
|
|
272
|
+
/// types without a dedicated accessor — arrays, records, optionals, shared objects, other numeric
|
|
273
|
+
/// widths — which decode through `getDynamicType().cast(...)`.
|
|
274
|
+
private func fastDecodeAccessor(for type: String) -> String? {
|
|
275
|
+
switch type {
|
|
276
|
+
case "Bool":
|
|
277
|
+
return "asBool"
|
|
278
|
+
case "Int":
|
|
279
|
+
return "asInt"
|
|
280
|
+
case "Double":
|
|
281
|
+
return "asDouble"
|
|
282
|
+
case "String":
|
|
283
|
+
return "asString"
|
|
284
|
+
default:
|
|
285
|
+
return nil
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
|
|
290
|
+
/// nothing JS-visible, so the binding returns `.undefined`.
|
|
291
|
+
private func isVoidType(_ type: TypeSyntax?) -> Bool {
|
|
292
|
+
guard let type else {
|
|
293
|
+
return true
|
|
294
|
+
}
|
|
295
|
+
let text = type.trimmedDescription
|
|
296
|
+
return text == "Void" || text == "()"
|
|
297
|
+
}
|
|
@@ -42,11 +42,11 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
42
42
|
let moduleName = jsNameArgument(of: node) ?? classDecl.name.text
|
|
43
43
|
var entries: [String] = ["Name(\"\(moduleName)\")"]
|
|
44
44
|
|
|
45
|
-
// `@JS func`s (sync and async) are bound directly into the JS object by the
|
|
46
|
-
// `_decorateModule` rather than described with a `Function(...)` / `
|
|
47
|
-
// so they're collected here instead of appended to `entries`.
|
|
48
|
-
// the DSL for now.
|
|
45
|
+
// `@JS func`s (sync and async) and `@JS var`s are bound directly into the JS object by the
|
|
46
|
+
// synthesized `_decorateModule` rather than described with a `Function(...)` / `Property(...)`
|
|
47
|
+
// DSL entry, so they're collected here instead of appended to `entries`.
|
|
49
48
|
var functions: [JSFunction] = []
|
|
49
|
+
var properties: [JSProperty] = []
|
|
50
50
|
|
|
51
51
|
for typeName in classListArgument(of: node, label: "classes") {
|
|
52
52
|
entries.append("\(typeName)._synthesizedClassDefinition()")
|
|
@@ -63,7 +63,7 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
63
63
|
|
|
64
64
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
65
65
|
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
66
|
-
|
|
66
|
+
properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -100,11 +100,11 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
100
100
|
"""
|
|
101
101
|
emitted.append(method)
|
|
102
102
|
|
|
103
|
-
// Direct JSI binding: one `_decorateModule` that binds each `@JS func`
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
if !functions.isEmpty {
|
|
107
|
-
emitted.append(buildDecorateJavaScriptObject(functions: functions))
|
|
103
|
+
// Direct JSI binding: one `_decorateModule` that binds each `@JS func` (inlined `setProperty`
|
|
104
|
+
// closure) and each `@JS var` (a `defineProperty` get/set accessor) into the module's JS object.
|
|
105
|
+
// Only emitted when there's at least one member to bind.
|
|
106
|
+
if !functions.isEmpty || !properties.isEmpty {
|
|
107
|
+
emitted.append(buildDecorateJavaScriptObject(functions: functions, properties: properties))
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
return emitted
|
|
@@ -228,19 +228,52 @@ private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
|
228
228
|
|
|
229
229
|
// MARK: - Member builders
|
|
230
230
|
|
|
231
|
-
private func
|
|
231
|
+
private func collectProperties(
|
|
232
232
|
varDecl: VariableDeclSyntax,
|
|
233
233
|
attribute: AttributeSyntax
|
|
234
|
-
) -> [
|
|
234
|
+
) -> [JSProperty] {
|
|
235
235
|
let jsNameOverride = jsNameArgument(of: attribute)
|
|
236
|
+
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
237
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
236
238
|
|
|
237
239
|
return varDecl.bindings.compactMap { binding in
|
|
238
240
|
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
239
241
|
return nil
|
|
240
242
|
}
|
|
241
243
|
let swiftName = ident.identifier.text
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
245
|
+
// when there's none. `nil` falls back to inference at the use site.
|
|
246
|
+
let valueType = binding.typeAnnotation?.type.trimmedDescription
|
|
247
|
+
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
248
|
+
return JSProperty(
|
|
249
|
+
swiftName: swiftName,
|
|
250
|
+
jsName: jsNameOverride ?? swiftName,
|
|
251
|
+
valueType: valueType,
|
|
252
|
+
isSettable: isVar && bindingIsSettable(binding)
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
|
|
258
|
+
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
259
|
+
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
260
|
+
/// observers imply stored storage, which is also settable.
|
|
261
|
+
private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
262
|
+
guard let accessorBlock = binding.accessorBlock else {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
switch accessorBlock.accessors {
|
|
266
|
+
case .accessors(let accessors):
|
|
267
|
+
return accessors.contains { accessor in
|
|
268
|
+
switch accessor.accessorSpecifier.tokenKind {
|
|
269
|
+
case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
|
|
270
|
+
return true
|
|
271
|
+
default:
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
case .getter:
|
|
276
|
+
return false
|
|
244
277
|
}
|
|
245
278
|
}
|
|
246
279
|
|
|
@@ -1,29 +1,93 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
import SwiftSyntaxMacros
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
/// Marker macro applied to module / shared-object members that should be exposed to JavaScript.
|
|
5
|
+
/// `@ExpoModule` and `@SharedObject` discover declarations carrying this attribute and generate the
|
|
6
|
+
/// corresponding `Function` / `AsyncFunction` / `Property` / `Constructor` registrations; that part
|
|
7
|
+
/// of the expansion lives in those macros.
|
|
8
|
+
///
|
|
9
|
+
/// On its own, `@JS` emits one thing: a never-called peer that asserts every type crossing the JS
|
|
10
|
+
/// boundary is JS-convertible. Because it's a **peer** of the marked member, a non-conforming type
|
|
11
|
+
/// produces a compile error located on the user's own `@JS` declaration rather than on the enclosing
|
|
12
|
+
/// `@ExpoModule`. The assertion mechanism itself is shared (see `typeConformanceAssertion`); `@JS`
|
|
13
|
+
/// only supplies the boundary types it reads off the declaration.
|
|
14
|
+
///
|
|
15
|
+
/// Usage:
|
|
16
|
+
///
|
|
17
|
+
/// @JS
|
|
18
|
+
/// func greet(name: String) -> String { ... }
|
|
19
|
+
///
|
|
20
|
+
/// @JS("doWork")
|
|
21
|
+
/// func performWork() async throws { ... }
|
|
22
|
+
///
|
|
23
|
+
/// @JS
|
|
24
|
+
/// var status: String { "ok" }
|
|
21
25
|
public struct JSMacro: PeerMacro {
|
|
22
26
|
public static func expansion(
|
|
23
27
|
of node: AttributeSyntax,
|
|
24
28
|
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
25
29
|
in context: some MacroExpansionContext
|
|
26
30
|
) throws -> [DeclSyntax] {
|
|
27
|
-
|
|
31
|
+
guard let member = boundaryMember(of: declaration),
|
|
32
|
+
let assertion = typeConformanceAssertion(
|
|
33
|
+
for: ConformanceAssertion(name: member.name, types: member.types),
|
|
34
|
+
isStatic: member.isStatic
|
|
35
|
+
) else {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
return [assertion]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// What an assertion peer needs about the `@JS` member it sits beside: a name (to keep the peer
|
|
43
|
+
/// unique among siblings), the types crossing the JS boundary, and whether the member is type-level.
|
|
44
|
+
private struct BoundaryMember {
|
|
45
|
+
let name: String
|
|
46
|
+
/// Boundary types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
|
|
47
|
+
/// conditional conformances transitively constrain the elements.
|
|
48
|
+
let types: [String]
|
|
49
|
+
/// True for `static`/`class` members, so the peer is emitted in the same metatype context.
|
|
50
|
+
let isStatic: Bool
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Reads the boundary member off a `@JS` declaration. A function contributes its parameter types
|
|
54
|
+
/// plus the return type (when non-Void); a property contributes its declared type. Returns `nil` for
|
|
55
|
+
/// declaration kinds `@JS` doesn't read types from, or a property whose type isn't spelled out (a
|
|
56
|
+
/// syntactic macro can't recover it), so no assertion is emitted there.
|
|
57
|
+
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
58
|
+
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
59
|
+
var types = funcDecl.signature.parameterClause.parameters.map { $0.type.trimmedDescription }
|
|
60
|
+
if let returnType = funcDecl.signature.returnClause?.type, !isVoidType(returnType) {
|
|
61
|
+
types.append(returnType.trimmedDescription)
|
|
62
|
+
}
|
|
63
|
+
return BoundaryMember(name: funcDecl.name.text, types: types, isStatic: isTypeLevel(funcDecl.modifiers))
|
|
28
64
|
}
|
|
65
|
+
|
|
66
|
+
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
67
|
+
let binding = varDecl.bindings.first,
|
|
68
|
+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
69
|
+
let type = binding.typeAnnotation?.type {
|
|
70
|
+
return BoundaryMember(
|
|
71
|
+
name: identifier.identifier.text,
|
|
72
|
+
types: [type.trimmedDescription],
|
|
73
|
+
isStatic: isTypeLevel(varDecl.modifiers)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// True when the modifiers make the member type-level (`static` or `class`), so its assertion peer
|
|
81
|
+
/// must be emitted in the same metatype context rather than as an instance member.
|
|
82
|
+
private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
83
|
+
return modifiers.contains {
|
|
84
|
+
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
|
|
89
|
+
/// no conformance assertion. (A missing return clause never reaches here: `returnClause` is `nil`.)
|
|
90
|
+
private func isVoidType(_ type: TypeSyntax) -> Bool {
|
|
91
|
+
let text = type.trimmedDescription
|
|
92
|
+
return text == "Void" || text == "()"
|
|
29
93
|
}
|
|
@@ -155,6 +155,27 @@ private func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool
|
|
|
155
155
|
return name.hasSuffix("Actor")
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/// The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
|
|
159
|
+
/// when the expression isn't one of those literals. Used to recover a property's type when it has no
|
|
160
|
+
/// annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
|
|
161
|
+
/// Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
|
|
162
|
+
/// macro can't know (function calls, collection literals, member access) return `nil`.
|
|
163
|
+
internal func inferredLiteralType(of expression: ExprSyntax) -> String? {
|
|
164
|
+
if expression.is(StringLiteralExprSyntax.self) {
|
|
165
|
+
return "String"
|
|
166
|
+
}
|
|
167
|
+
if expression.is(FloatLiteralExprSyntax.self) {
|
|
168
|
+
return "Double"
|
|
169
|
+
}
|
|
170
|
+
if expression.is(IntegerLiteralExprSyntax.self) {
|
|
171
|
+
return "Int"
|
|
172
|
+
}
|
|
173
|
+
if expression.is(BooleanLiteralExprSyntax.self) {
|
|
174
|
+
return "Bool"
|
|
175
|
+
}
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
|
|
158
179
|
extension AttributeListSyntax {
|
|
159
180
|
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
160
181
|
for element in self {
|
|
@@ -59,6 +59,18 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
59
59
|
let existingInitLabels = initializerParameterLabels(of: declaration)
|
|
60
60
|
|
|
61
61
|
var members: [DeclSyntax] = []
|
|
62
|
+
|
|
63
|
+
// A single never-called member that makes the compiler verify each property type is
|
|
64
|
+
// JS-convertible (the conversions below go through its dynamic-type API). Each property keeps its
|
|
65
|
+
// own named assertion inside, so the compiler's conformance diagnostic names the offending
|
|
66
|
+
// property (see `typeConformanceAssertions`). Emitted first so that, for a non-conforming type,
|
|
67
|
+
// this clear "requires that '…' conform to '…'" error is reported ahead of the noisier
|
|
68
|
+
// "no member 'getDynamicType'" errors from the conversion code below.
|
|
69
|
+
let assertions = properties.map { ConformanceAssertion(name: $0.name, types: [$0.type]) }
|
|
70
|
+
if let assertionMember = typeConformanceAssertions(for: assertions) {
|
|
71
|
+
members.append(assertionMember)
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
if !existingInitLabels.contains([]) {
|
|
63
75
|
if let defaultInit = defaultInit(properties: properties, isClass: isClass) {
|
|
64
76
|
members.append(defaultInit)
|
|
@@ -441,29 +453,6 @@ private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
|
441
453
|
return false
|
|
442
454
|
}
|
|
443
455
|
|
|
444
|
-
/**
|
|
445
|
-
The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
|
|
446
|
-
when the expression isn't one of those literals. Used to recover a property's type when it has no
|
|
447
|
-
annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
|
|
448
|
-
Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
|
|
449
|
-
macro can't know (function calls, collection literals, member access) return `nil`.
|
|
450
|
-
*/
|
|
451
|
-
private func inferredLiteralType(of expression: ExprSyntax) -> String? {
|
|
452
|
-
if expression.is(StringLiteralExprSyntax.self) {
|
|
453
|
-
return "String"
|
|
454
|
-
}
|
|
455
|
-
if expression.is(FloatLiteralExprSyntax.self) {
|
|
456
|
-
return "Double"
|
|
457
|
-
}
|
|
458
|
-
if expression.is(IntegerLiteralExprSyntax.self) {
|
|
459
|
-
return "Int"
|
|
460
|
-
}
|
|
461
|
-
if expression.is(BooleanLiteralExprSyntax.self) {
|
|
462
|
-
return "Bool"
|
|
463
|
-
}
|
|
464
|
-
return nil
|
|
465
|
-
}
|
|
466
|
-
|
|
467
456
|
/**
|
|
468
457
|
True if the type syntax is optional: `T?`, `T!`, or the spelled-out `Optional<T>`.
|
|
469
458
|
*/
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
|
|
4
|
+
/// eventual rename (this is a placeholder name) is a single edit, and shared by every macro that
|
|
5
|
+
/// asserts the conformance (`@JS`, `@Record`, …).
|
|
6
|
+
internal let jsConvertibleProtocolName = "AnyArgument"
|
|
7
|
+
|
|
8
|
+
/// Types we never assert because they're statically known to conform and never reach the dynamic
|
|
9
|
+
/// converter: the JS primitives. Asserting them would only add noise to the expansion. Kept here
|
|
10
|
+
/// (rather than reusing the decode-path's `fastDecodeAccessor`) because "known-to-conform" is a
|
|
11
|
+
/// concept that belongs with the assertion logic, not with how a value is decoded.
|
|
12
|
+
private let knownConformingPrimitives: Set<String> = ["Bool", "Int", "Double", "String"]
|
|
13
|
+
|
|
14
|
+
/// One member's worth of conformance assertion: a name (the member it stands for) and the declared
|
|
15
|
+
/// types crossing the JS boundary for it. The name surfaces verbatim in the compiler's conformance
|
|
16
|
+
/// diagnostic ("local function '<name>' requires that '<Type>' conform to …"), so it identifies the
|
|
17
|
+
/// offending member in the error message on top of the location pointing at the user's declaration.
|
|
18
|
+
internal struct ConformanceAssertion {
|
|
19
|
+
let name: String
|
|
20
|
+
/// Declared types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
|
|
21
|
+
/// conditional conformances transitively constrain the elements.
|
|
22
|
+
let types: [String]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// A single conformance-assertion peer for a `@JS` member: a never-called `private func` whose body
|
|
26
|
+
/// statically asserts the member's boundary types conform. The assertion is compile-time only — the
|
|
27
|
+
/// function is never invoked, but Swift still type-checks its body, so a non-conforming type becomes
|
|
28
|
+
/// a compile error. Emitted as a **peer** of the user's declaration, so that error lands on the
|
|
29
|
+
/// user's own member rather than on the enclosing macro.
|
|
30
|
+
///
|
|
31
|
+
/// `isStatic` makes the peer `static`, mirroring a `static`/`class` member so it's emitted in the
|
|
32
|
+
/// right metatype context (a peer of a type-level member can't be an instance method). `class func`
|
|
33
|
+
/// members collapse to `static` here too: the peer is private and never called or overridden, so
|
|
34
|
+
/// `static` is always sufficient.
|
|
35
|
+
///
|
|
36
|
+
/// Returns `nil` when nothing is left to assert (every type was a known-conforming primitive, or the
|
|
37
|
+
/// list was empty), so the caller emits nothing in that case.
|
|
38
|
+
internal func typeConformanceAssertion(for assertion: ConformanceAssertion, isStatic: Bool) -> DeclSyntax? {
|
|
39
|
+
guard let body = conformanceAssertionBody(assertion) else {
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
42
|
+
let staticKeyword = isStatic ? "static " : ""
|
|
43
|
+
return """
|
|
44
|
+
private \(raw: staticKeyword)func _assertTypesConformance_\(raw: assertion.name)() {
|
|
45
|
+
\(raw: body)
|
|
46
|
+
}
|
|
47
|
+
"""
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// One conformance-assertion peer covering several members' assertions at once — used by `@Record`,
|
|
51
|
+
/// which folds every property into a single `_assertTypesConformance()` rather than emitting a peer
|
|
52
|
+
/// per property. Each assertion keeps its own named nested helper, so the per-member naming in the
|
|
53
|
+
/// diagnostic is preserved even though they share one peer.
|
|
54
|
+
///
|
|
55
|
+
/// Returns `nil` when no assertion has anything left to verify (all primitives / empty), so the
|
|
56
|
+
/// caller emits nothing.
|
|
57
|
+
internal func typeConformanceAssertions(for assertions: [ConformanceAssertion]) -> DeclSyntax? {
|
|
58
|
+
let bodies = assertions.compactMap(conformanceAssertionBody)
|
|
59
|
+
guard !bodies.isEmpty else {
|
|
60
|
+
return nil
|
|
61
|
+
}
|
|
62
|
+
return """
|
|
63
|
+
private func _assertTypesConformance() {
|
|
64
|
+
\(raw: bodies.joined(separator: "\n"))
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// The assertion's body fragment: a nested generic helper named after the member, plus one call per
|
|
70
|
+
/// distinct non-primitive type. Nesting the helper keeps the constraint entirely local — no shared
|
|
71
|
+
/// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
|
|
72
|
+
/// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
|
|
73
|
+
/// a known-conforming primitive or the list was empty.
|
|
74
|
+
private func conformanceAssertionBody(_ assertion: ConformanceAssertion) -> String? {
|
|
75
|
+
// Unwrap top-level optionals to the core type, then dedup so each type is asserted once even when
|
|
76
|
+
// it appears more than once; skip known primitives.
|
|
77
|
+
var seen: Set<String> = []
|
|
78
|
+
var distinct: [String] = []
|
|
79
|
+
for type in assertion.types.map(unwrappedOptional)
|
|
80
|
+
where !knownConformingPrimitives.contains(type) && seen.insert(type).inserted {
|
|
81
|
+
distinct.append(type)
|
|
82
|
+
}
|
|
83
|
+
guard !distinct.isEmpty else {
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var lines = ["func \(assertion.name)<T: \(jsConvertibleProtocolName)>(_: T.Type) {}"]
|
|
88
|
+
lines.append(contentsOf: distinct.map { "\(assertion.name)(\($0).self)" })
|
|
89
|
+
return lines.joined(separator: "\n")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Strips every trailing optional marker (`?`/`!`) so the assertion targets the core wrapped type.
|
|
93
|
+
/// `Optional<W>: AnyArgument` holds exactly when `W: AnyArgument` (and `T!` is just `T?`), so each
|
|
94
|
+
/// layer is conformance-equivalent to its wrapped type. Asserting the core gives a cleaner diagnostic
|
|
95
|
+
/// (the direct `requires that '<type>' conform`, not the conditional-conformance phrasing through
|
|
96
|
+
/// `Optional`) and sidesteps that `T!.self` is invalid in metatype position. Only *trailing* markers
|
|
97
|
+
/// are stripped, so `[Int?]` keeps its inner `?`; a longhand `Optional<W>` isn't peeled but is still
|
|
98
|
+
/// asserted whole, which remains correct.
|
|
99
|
+
private func unwrappedOptional(_ type: String) -> String {
|
|
100
|
+
var result = Substring(type)
|
|
101
|
+
while result.hasSuffix("?") || result.hasSuffix("!") {
|
|
102
|
+
result = result.dropLast()
|
|
103
|
+
}
|
|
104
|
+
return String(result)
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import SwiftSyntax
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
|
|
5
|
-
`Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call,
|
|
6
|
-
`@ExpoModule` synthesizes a `_decorateModule` that binds each such function into the module's JS object
|
|
7
|
-
via the closure-taking `JavaScriptObject.setProperty(_:)`, with the decode-call-encode body
|
|
8
|
-
inlined into the closure. This omits the `[Any]`/`toTuple` dynamic-call path: every argument is
|
|
9
|
-
decoded individually by its static type.
|
|
10
|
-
|
|
11
|
-
The receiver is the module's real `self` (a module is a singleton instance), so the body calls
|
|
12
|
-
`self.<name>(...)` directly and ignores the JS `this`. An `async` `@JS func` produces an `async`
|
|
13
|
-
closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
|
|
14
|
-
*/
|
|
15
|
-
internal struct JSFunction {
|
|
16
|
-
let swiftName: String
|
|
17
|
-
let jsName: String
|
|
18
|
-
let parameters: [FunctionParameterSyntax]
|
|
19
|
-
/// The declared return type as written, or `nil` when the function returns `Void`/nothing.
|
|
20
|
-
let returnType: String?
|
|
21
|
-
let isThrowing: Bool
|
|
22
|
-
let isAsync: Bool
|
|
23
|
-
|
|
24
|
-
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
25
|
-
self.swiftName = funcDecl.name.text
|
|
26
|
-
self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
|
|
27
|
-
self.parameters = Array(funcDecl.signature.parameterClause.parameters)
|
|
28
|
-
|
|
29
|
-
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
30
|
-
self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
|
|
31
|
-
|
|
32
|
-
let effectSpecifiers = funcDecl.signature.effectSpecifiers
|
|
33
|
-
self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
|
|
34
|
-
self.isAsync = effectSpecifiers?.asyncSpecifier != nil
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
The `#name` host-function body: a `@JavaScriptActor private func` matching the
|
|
39
|
-
`createFunction` closure shape `(this, arguments) throws -> JavaScriptValue`, threading
|
|
40
|
-
`appContext`/`runtime` in as parameters. It checks arity, decodes each argument by its static
|
|
41
|
-
type — primitives through a direct typed accessor (`asDouble()`, …) on a borrowed
|
|
42
|
-
`JavaScriptUnownedValue`, other types through the
|
|
43
|
-
`T.getDynamicType()` converter — calls `self.<name>(...)`, and converts the result back to JS.
|
|
44
|
-
*/
|
|
45
|
-
/// The decode-call-encode statements that form the host-function body, indented with the given
|
|
46
|
-
/// prefix. Arity guard, then per-argument decode (primitives via a direct typed accessor like
|
|
47
|
-
/// `asDouble()` on a zero-copy `arguments.unownedValue(at:)`, others via `getDynamicType().cast(...)`),
|
|
48
|
-
/// the `self.<name>(...)` call, and the
|
|
49
|
-
/// result encode (primitives via `toJavaScriptValue(in:)`, others via `castToJS(...)`).
|
|
50
|
-
private func bodyStatements(indent: String) -> String {
|
|
51
|
-
var lines: [String] = []
|
|
52
|
-
|
|
53
|
-
lines.append(
|
|
54
|
-
"""
|
|
55
|
-
guard arguments.count == \(parameters.count) else {
|
|
56
|
-
throw Exception(name: "InvalidArgumentCount", description: "Function '\(jsName)' expects \(parameters.count) argument(s), but got \\(arguments.count)")
|
|
57
|
-
}
|
|
58
|
-
""")
|
|
59
|
-
|
|
60
|
-
var callArguments: [String] = []
|
|
61
|
-
for (index, parameter) in parameters.enumerated() {
|
|
62
|
-
let type = parameter.type.trimmedDescription
|
|
63
|
-
|
|
64
|
-
// Primitives decode through a direct typed accessor (`asDouble()`, etc.) on a borrowed
|
|
65
|
-
// `JavaScriptUnownedValue` — no owning `JavaScriptValue` allocation, no `jsi::Value` copy, no
|
|
66
|
-
// `getDynamicType()` allocation, no `Any` boxing, no force-cast — while still validating and
|
|
67
|
-
// throwing `TypeError` on a mismatch. Other types fall back to the dynamic converter, which
|
|
68
|
-
// needs an owning value, so they index the buffer directly.
|
|
69
|
-
if let accessor = fastDecodeAccessor(for: type) {
|
|
70
|
-
lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
|
|
71
|
-
} else {
|
|
72
|
-
lines.append(
|
|
73
|
-
"let arg\(index) = try \(type).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(type)")
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let label = parameter.firstName.text
|
|
77
|
-
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let tryKeyword = (isThrowing || isAsync) ? "try " : ""
|
|
81
|
-
let awaitKeyword = isAsync ? "await " : ""
|
|
82
|
-
let callExpression =
|
|
83
|
-
"\(tryKeyword)\(awaitKeyword)self.\(swiftName)(\(callArguments.joined(separator: ", ")))"
|
|
84
|
-
|
|
85
|
-
if let returnType {
|
|
86
|
-
lines.append("let result = \(callExpression)")
|
|
87
|
-
// Primitives encode through `toJavaScriptValue(in:)` (the typed `JavaScriptRepresentable`
|
|
88
|
-
// conversion) — no `Any`, no dynamic-type allocation. Others go through the dynamic converter.
|
|
89
|
-
if fastDecodeAccessor(for: returnType) != nil {
|
|
90
|
-
lines.append("return result.toJavaScriptValue(in: runtime)")
|
|
91
|
-
} else {
|
|
92
|
-
lines.append("return try \(returnType).getDynamicType().castToJS(result, appContext: appContext, in: runtime)")
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
lines.append(callExpression)
|
|
96
|
-
lines.append("return .undefined")
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return lines
|
|
100
|
-
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
101
|
-
.map { indent + $0 }
|
|
102
|
-
.joined(separator: "\n")
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
106
|
-
/// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
|
|
107
|
-
/// (which creates the host function under the hood) — no separate named binding. For an `async`
|
|
108
|
-
/// function the body `await`s the call, which selects the async `setProperty` overload (so JS
|
|
109
|
-
/// receives a promise).
|
|
110
|
-
///
|
|
111
|
-
/// Capture mirrors core's `SyncFunctionDefinition.build`: `self` (the module) is captured
|
|
112
|
-
/// **strong** — the host-function closure is what keeps the native callable alive for as long as
|
|
113
|
-
/// JS can invoke it; its lifetime is bounded by the JS VM's garbage collection of the object.
|
|
114
|
-
/// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
|
|
115
|
-
/// the app context.
|
|
116
|
-
var decorateStatements: String {
|
|
117
|
-
return """
|
|
118
|
-
object.setProperty("\(jsName)") { [weak appContext, self] this, arguments in
|
|
119
|
-
guard let appContext else {
|
|
120
|
-
throw Exceptions.AppContextLost()
|
|
121
|
-
}
|
|
122
|
-
\(bodyStatements(indent: " "))
|
|
123
|
-
}
|
|
124
|
-
"""
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
The single generated function that decorates the module's JS object. Core supplies the object;
|
|
130
|
-
this binds every `@JS func` into it via one inlined `setProperty` closure per function. Mirrors
|
|
131
|
-
core's `ObjectDefinition.decorate(object:)`, including its `borrowing` object parameter (it
|
|
132
|
-
mutates through the reference without reassigning or taking ownership). Named `_decorateModule`
|
|
133
|
-
with the leading-underscore convention for synthesized members the **runtime calls by name**; the
|
|
134
|
-
`ExpoModule` suffix names the `@ExpoModule` macro it came from (a shared object's counterpart is
|
|
135
|
-
`_decorateSharedObject`).
|
|
136
|
-
*/
|
|
137
|
-
internal func buildDecorateJavaScriptObject(functions: [JSFunction]) -> DeclSyntax {
|
|
138
|
-
let body = functions.map { $0.decorateStatements }.joined(separator: "\n")
|
|
139
|
-
return """
|
|
140
|
-
@JavaScriptActor
|
|
141
|
-
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
142
|
-
\(raw: body)
|
|
143
|
-
}
|
|
144
|
-
"""
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly,
|
|
148
|
-
/// bypassing the dynamic-type converter (`asDouble()` for `Double`, etc.). Returns `nil` for
|
|
149
|
-
/// types without a dedicated accessor — arrays, records, optionals, shared objects, other numeric
|
|
150
|
-
/// widths — which decode through `getDynamicType().cast(...)`.
|
|
151
|
-
private func fastDecodeAccessor(for type: String) -> String? {
|
|
152
|
-
switch type {
|
|
153
|
-
case "Bool":
|
|
154
|
-
return "asBool"
|
|
155
|
-
case "Int":
|
|
156
|
-
return "asInt"
|
|
157
|
-
case "Double":
|
|
158
|
-
return "asDouble"
|
|
159
|
-
case "String":
|
|
160
|
-
return "asString"
|
|
161
|
-
default:
|
|
162
|
-
return nil
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
|
|
167
|
-
/// nothing JS-visible, so the binding returns `.undefined`.
|
|
168
|
-
private func isVoidType(_ type: TypeSyntax?) -> Bool {
|
|
169
|
-
guard let type else {
|
|
170
|
-
return true
|
|
171
|
-
}
|
|
172
|
-
let text = type.trimmedDescription
|
|
173
|
-
return text == "Void" || text == "()"
|
|
174
|
-
}
|