@expo/expo-modules-macros-plugin 0.3.0 → 0.5.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/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Package.swift +27 -2
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +148 -98
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +0 -52
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +60 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +84 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +58 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +5 -3
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +29 -90
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +61 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +109 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +137 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +68 -0
- package/apple/Sources/ExpoModulesScannerCLI/main.swift +70 -0
- package/package.json +1 -1
|
Binary file
|
package/apple/Package.swift
CHANGED
|
@@ -8,7 +8,12 @@ import PackageDescription
|
|
|
8
8
|
let package = Package(
|
|
9
9
|
name: "ExpoModulesMacros",
|
|
10
10
|
platforms: [.macOS(.v13)],
|
|
11
|
-
products: [
|
|
11
|
+
products: [
|
|
12
|
+
// The scanner CLI. Named `ExpoModulesScanner` (the user-facing tool name) while its target is
|
|
13
|
+
// `ExpoModulesScannerCLI`; the detection logic lives in the importable `ExpoModulesScanner`
|
|
14
|
+
// library that both the CLI and the tests depend on.
|
|
15
|
+
.executable(name: "ExpoModulesScanner", targets: ["ExpoModulesScannerCLI"]),
|
|
16
|
+
],
|
|
12
17
|
dependencies: [
|
|
13
18
|
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0-latest")
|
|
14
19
|
],
|
|
@@ -19,7 +24,18 @@ let package = Package(
|
|
|
19
24
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
|
20
25
|
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
|
21
26
|
]
|
|
22
|
-
)
|
|
27
|
+
),
|
|
28
|
+
.target(
|
|
29
|
+
name: "ExpoModulesScanner",
|
|
30
|
+
dependencies: [
|
|
31
|
+
.product(name: "SwiftSyntax", package: "swift-syntax"),
|
|
32
|
+
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
33
|
+
]
|
|
34
|
+
),
|
|
35
|
+
.executableTarget(
|
|
36
|
+
name: "ExpoModulesScannerCLI",
|
|
37
|
+
dependencies: ["ExpoModulesScanner"]
|
|
38
|
+
),
|
|
23
39
|
]
|
|
24
40
|
)
|
|
25
41
|
|
|
@@ -36,4 +52,13 @@ if FileManager.default.fileExists(atPath: Context.packageDirectory + "/Tests") {
|
|
|
36
52
|
]
|
|
37
53
|
)
|
|
38
54
|
)
|
|
55
|
+
package.targets.append(
|
|
56
|
+
.testTarget(
|
|
57
|
+
name: "ExpoModulesScannerTests",
|
|
58
|
+
dependencies: [
|
|
59
|
+
"ExpoModulesScanner",
|
|
60
|
+
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
)
|
|
39
64
|
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
|
|
14
|
-
*/
|
|
3
|
+
/// A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
|
|
4
|
+
/// `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call, the enclosing
|
|
5
|
+
/// macro synthesizes a decorator (`_decorateModule` / `_decorateSharedObject`) that binds each such
|
|
6
|
+
/// function into the JS object via the closure-taking `JavaScriptObject.setProperty(_:)`, with the
|
|
7
|
+
/// decode-call-encode body inlined into the closure. This omits the `[Any]`/`toTuple` dynamic-call path:
|
|
8
|
+
/// every argument is decoded individually by its static type.
|
|
9
|
+
///
|
|
10
|
+
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
11
|
+
/// unwrapped from the JS `this` for a shared-object binding. An `async` `@JS func` produces an `async`
|
|
12
|
+
/// closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
|
|
15
13
|
internal struct JSFunction {
|
|
16
14
|
let swiftName: String
|
|
17
15
|
let jsName: String
|
|
@@ -50,19 +48,23 @@ internal struct JSFunction {
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
/// The decode-call-encode statements that form the host-function body, indented with the given
|
|
53
|
-
/// prefix.
|
|
54
|
-
/// check) throwing `Exceptions.ArgumentsRangeMismatch`;
|
|
55
|
-
/// required prefix (primitives via a direct typed accessor
|
|
56
|
-
/// `arguments.unownedValue(at:)`, others via
|
|
57
|
-
///
|
|
58
|
-
///
|
|
59
|
-
/// the
|
|
60
|
-
/// didn't pass is never indexed.
|
|
61
|
-
private func bodyStatements(indent: String) -> String {
|
|
51
|
+
/// prefix. Receiver unwrap (shared objects only), then an arity guard (an exact check when every
|
|
52
|
+
/// parameter is required, otherwise a range check) throwing `Exceptions.ArgumentsRangeMismatch`;
|
|
53
|
+
/// then the decode of the always-present required prefix (primitives via a direct typed accessor
|
|
54
|
+
/// like `asDouble()` on a zero-copy `arguments.unownedValue(at:)`, others via
|
|
55
|
+
/// `getDynamicType().cast(...)`); then the call and result encode (primitives via
|
|
56
|
+
/// `toJavaScriptValue(in:)`, others via `castToJS(...)`). When a trailing run of parameters is
|
|
57
|
+
/// omittable the call branches on `arguments.count`, decoding only the slots that branch actually
|
|
58
|
+
/// has — `arguments[i]` traps past `count`, so a slot the caller didn't pass is never indexed.
|
|
59
|
+
private func bodyStatements(receiver: Receiver, indent: String) -> String {
|
|
62
60
|
let required = requiredArgumentCount
|
|
63
61
|
let maximum = parameters.count
|
|
64
62
|
var lines: [String] = []
|
|
65
63
|
|
|
64
|
+
if let unwrap = receiver.unwrapStatement {
|
|
65
|
+
lines.append(unwrap)
|
|
66
|
+
}
|
|
67
|
+
|
|
66
68
|
if required == maximum {
|
|
67
69
|
lines.append(
|
|
68
70
|
"""
|
|
@@ -87,7 +89,7 @@ internal struct JSFunction {
|
|
|
87
89
|
|
|
88
90
|
if required == maximum {
|
|
89
91
|
// No omittable trailing run: a single flat call with every argument decoded.
|
|
90
|
-
lines.append(contentsOf: callAndEncodeLines(arity: maximum, decodingFrom: required))
|
|
92
|
+
lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
|
|
91
93
|
} else {
|
|
92
94
|
// One call shape per accepted arity. Each branch decodes only the trailing slots it has and
|
|
93
95
|
// fills the rest (defaulted params drop their label so Swift applies the default; optional
|
|
@@ -105,7 +107,7 @@ internal struct JSFunction {
|
|
|
105
107
|
for index in required..<arity {
|
|
106
108
|
lines.append(" " + decodeStatement(at: index))
|
|
107
109
|
}
|
|
108
|
-
lines.append(" \(callExpression(arity: arity))")
|
|
110
|
+
lines.append(" \(callExpression(receiver: receiver, arity: arity))")
|
|
109
111
|
}
|
|
110
112
|
lines.append("}")
|
|
111
113
|
lines.append(contentsOf: encodeResultLines())
|
|
@@ -127,13 +129,14 @@ internal struct JSFunction {
|
|
|
127
129
|
if let accessor = fastDecodeAccessor(for: type) {
|
|
128
130
|
return "let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()"
|
|
129
131
|
}
|
|
130
|
-
|
|
132
|
+
let exprType = expressionType(type)
|
|
133
|
+
return "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)"
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
/// The
|
|
136
|
+
/// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
|
|
134
137
|
/// `arg<i>`; a trailing optional-without-default slot that this arity omits is passed `nil`; a
|
|
135
138
|
/// trailing defaulted slot that this arity omits is dropped entirely so Swift applies its default.
|
|
136
|
-
private func callExpression(arity: Int) -> String {
|
|
139
|
+
private func callExpression(receiver: Receiver, arity: Int) -> String {
|
|
137
140
|
var callArguments: [String] = []
|
|
138
141
|
for (index, parameter) in parameters.enumerated() {
|
|
139
142
|
let label = parameter.firstName.text
|
|
@@ -154,21 +157,22 @@ internal struct JSFunction {
|
|
|
154
157
|
}
|
|
155
158
|
let tryKeyword = (isThrowing || isAsync) ? "try " : ""
|
|
156
159
|
let awaitKeyword = isAsync ? "await " : ""
|
|
157
|
-
return "\(tryKeyword)\(awaitKeyword)
|
|
160
|
+
return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments.joined(separator: ", ")))"
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
/// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
|
|
161
|
-
/// `let result =
|
|
162
|
-
|
|
164
|
+
/// `let result = <callee>.f(...)` then the return encode (or the no-return `<callee>.f(...)` +
|
|
165
|
+
/// `.undefined`).
|
|
166
|
+
private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
|
|
163
167
|
var lines: [String] = []
|
|
164
168
|
for index in decodingFrom..<arity {
|
|
165
169
|
lines.append(decodeStatement(at: index))
|
|
166
170
|
}
|
|
167
171
|
if returnType != nil {
|
|
168
|
-
lines.append("let result = \(callExpression(arity: arity))")
|
|
172
|
+
lines.append("let result = \(callExpression(receiver: receiver, arity: arity))")
|
|
169
173
|
lines.append(contentsOf: encodeResultLines())
|
|
170
174
|
} else {
|
|
171
|
-
lines.append(callExpression(arity: arity))
|
|
175
|
+
lines.append(callExpression(receiver: receiver, arity: arity))
|
|
172
176
|
lines.append("return .undefined")
|
|
173
177
|
}
|
|
174
178
|
return lines
|
|
@@ -184,7 +188,7 @@ internal struct JSFunction {
|
|
|
184
188
|
if fastDecodeAccessor(for: returnType) != nil {
|
|
185
189
|
return ["return result.toJavaScriptValue(in: runtime)"]
|
|
186
190
|
}
|
|
187
|
-
return ["return try \(returnType).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
|
|
191
|
+
return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
@@ -193,27 +197,44 @@ internal struct JSFunction {
|
|
|
193
197
|
/// function the body `await`s the call, which selects the async `setProperty` overload (so JS
|
|
194
198
|
/// receives a promise).
|
|
195
199
|
///
|
|
196
|
-
/// Capture mirrors core's `SyncFunctionDefinition.build`: `self`
|
|
197
|
-
///
|
|
198
|
-
///
|
|
200
|
+
/// Capture mirrors core's `SyncFunctionDefinition.build`: a module captures its `self` **strong** —
|
|
201
|
+
/// the host-function closure is what keeps the native callable alive for as long as JS can invoke
|
|
202
|
+
/// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
|
|
203
|
+
/// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
|
|
199
204
|
/// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
|
|
200
205
|
/// the app context. When no argument or return value goes through the dynamic-type converter the
|
|
201
206
|
/// body never references `appContext`, so the capture and guard are omitted to avoid the
|
|
202
207
|
/// unused-capture warning.
|
|
203
|
-
|
|
208
|
+
func decorateStatements(receiver: Receiver) -> String {
|
|
209
|
+
// Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
|
|
210
|
+
// `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
|
|
211
|
+
// `JavaScriptValue` and forming its `weak`-runtime reference on every call. A module ignores
|
|
212
|
+
// `this`; a shared object unwraps it (still borrowed). The first parameter is typed `borrowing
|
|
213
|
+
// JavaScriptUnownedValue` to select that (otherwise `@_disfavoredOverload`) overload — which
|
|
214
|
+
// requires the *parenthesized, fully typed* parameter list, since Swift rejects a type annotation
|
|
215
|
+
// on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
|
|
216
|
+
// shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
|
|
217
|
+
// escapes into the task anyway.
|
|
218
|
+
let captures = receiver.captureClause(usesAppContext: usesAppContext)
|
|
219
|
+
let parameters =
|
|
220
|
+
isAsync
|
|
221
|
+
? "this, arguments"
|
|
222
|
+
: "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
223
|
+
|
|
224
|
+
let object = receiver.decoratedObject
|
|
204
225
|
if usesAppContext {
|
|
205
226
|
return """
|
|
206
|
-
object.setProperty("\(jsName)") {
|
|
227
|
+
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
207
228
|
guard let appContext else {
|
|
208
229
|
throw Exceptions.AppContextLost()
|
|
209
230
|
}
|
|
210
|
-
\(bodyStatements(indent: " "))
|
|
231
|
+
\(bodyStatements(receiver: receiver, indent: " "))
|
|
211
232
|
}
|
|
212
233
|
"""
|
|
213
234
|
}
|
|
214
235
|
return """
|
|
215
|
-
object.setProperty("\(jsName)") {
|
|
216
|
-
\(bodyStatements(indent: " "))
|
|
236
|
+
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
237
|
+
\(bodyStatements(receiver: receiver, indent: " "))
|
|
217
238
|
}
|
|
218
239
|
"""
|
|
219
240
|
}
|
|
@@ -233,17 +254,18 @@ internal struct JSFunction {
|
|
|
233
254
|
}
|
|
234
255
|
|
|
235
256
|
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
236
|
-
/// `Property(...)` DSL entry,
|
|
237
|
-
///
|
|
238
|
-
/// when the property is settable) and installs it with
|
|
239
|
-
/// mirroring core's `PropertyDefinition.buildDescriptor`.
|
|
240
|
-
/// installed the same way `@JS func`s are — the closure-taking
|
|
241
|
-
/// read/write body inlined into the closure.
|
|
257
|
+
/// `Property(...)` DSL entry, the enclosing macro synthesizes a get/set accessor into the JS object
|
|
258
|
+
/// inside its decorator (`_decorateModule` / `_decorateSharedObject`): it builds a descriptor object
|
|
259
|
+
/// (`enumerable` + `get`, and `set` when the property is settable) and installs it with
|
|
260
|
+
/// `object.defineProperty(name, descriptor:)`, mirroring core's `PropertyDefinition.buildDescriptor`.
|
|
261
|
+
/// The `get`/`set` host functions are installed the same way `@JS func`s are — the closure-taking
|
|
262
|
+
/// `setProperty(_:)` overload, with the read/write body inlined into the closure.
|
|
242
263
|
///
|
|
243
|
-
/// The receiver is the module's
|
|
244
|
-
///
|
|
245
|
-
///
|
|
246
|
-
///
|
|
264
|
+
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
265
|
+
/// unwrapped from the JS `this` for a shared object. The getter reads `<callee>.<name>` and the setter
|
|
266
|
+
/// writes `<callee>.<name> = …`. Decode/encode of the value reuse the same static-type fast path as
|
|
267
|
+
/// functions (primitives through a direct typed accessor / `toJavaScriptValue`, other types through
|
|
268
|
+
/// the `getDynamicType()` converter).
|
|
247
269
|
internal struct JSProperty {
|
|
248
270
|
let swiftName: String
|
|
249
271
|
let jsName: String
|
|
@@ -256,54 +278,66 @@ internal struct JSProperty {
|
|
|
256
278
|
let isSettable: Bool
|
|
257
279
|
|
|
258
280
|
/// The statements that install this property's accessor on the JS object, indented for the
|
|
259
|
-
///
|
|
260
|
-
///
|
|
261
|
-
///
|
|
262
|
-
///
|
|
263
|
-
///
|
|
264
|
-
///
|
|
265
|
-
/// warning. Getter and setter are gated
|
|
266
|
-
|
|
281
|
+
/// decorator body. Builds a descriptor object (`enumerable` + `get`, and `set` when settable) via
|
|
282
|
+
/// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
|
|
283
|
+
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
284
|
+
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
285
|
+
/// instance; `appContext` is weak + guarded — and, like functions, the `appContext` capture + guard
|
|
286
|
+
/// are omitted from an accessor whose body never references it (a primitive value, decoded/encoded
|
|
287
|
+
/// without the dynamic converter), to avoid the unused-capture warning. Getter and setter are gated
|
|
288
|
+
/// independently.
|
|
289
|
+
func decorateStatements(receiver: Receiver) -> String {
|
|
267
290
|
let descriptorName = "\(swiftName)Descriptor"
|
|
291
|
+
let callee = receiver.callee
|
|
292
|
+
let object = receiver.decoratedObject
|
|
268
293
|
// A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
|
|
269
294
|
// references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
|
|
270
295
|
// getter, which also doesn't use it.
|
|
271
296
|
let usesAppContext = valueType.map { fastDecodeAccessor(for: $0) == nil } ?? false
|
|
297
|
+
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
298
|
+
// reads `self` directly. The unwrap leads each accessor body.
|
|
299
|
+
let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
|
|
272
300
|
var lines: [String] = []
|
|
273
301
|
|
|
274
302
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
275
303
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
276
304
|
|
|
277
|
-
// Getter: read
|
|
305
|
+
// Getter: read `<callee>.<name>` and encode the result back to JS.
|
|
278
306
|
let getEncode: String
|
|
279
307
|
if let valueType, fastDecodeAccessor(for: valueType) != nil {
|
|
280
|
-
getEncode = "return
|
|
308
|
+
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
281
309
|
} else if let valueType {
|
|
282
310
|
getEncode =
|
|
283
|
-
"return try \(valueType).getDynamicType().castToJS(
|
|
311
|
+
"return try \(expressionType(valueType)).getDynamicType().castToJS(\(callee).\(swiftName), appContext: appContext, in: runtime)"
|
|
284
312
|
} else {
|
|
285
|
-
// No known type: fall back to converting whatever
|
|
286
|
-
// declaration has neither an annotation nor a literal default, which is rare for a stored
|
|
287
|
-
|
|
313
|
+
// No known type: fall back to converting whatever `<callee>.<name>` is. This only happens when
|
|
314
|
+
// the declaration has neither an annotation nor a literal default, which is rare for a stored
|
|
315
|
+
// var.
|
|
316
|
+
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
288
317
|
}
|
|
289
|
-
lines.append(
|
|
318
|
+
lines.append(
|
|
319
|
+
accessorClosure(
|
|
320
|
+
descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
|
|
290
321
|
|
|
291
|
-
// Setter: decode argument 0 by the static type and write
|
|
292
|
-
// known value type; when the type couldn't be inferred the property is bound getter-only (a
|
|
322
|
+
// Setter: decode argument 0 by the static type and write `<callee>.<name>`. A typed setter needs
|
|
323
|
+
// a known value type; when the type couldn't be inferred the property is bound getter-only (a
|
|
293
324
|
// settable var with neither an annotation nor a literal default is rare and can't be decoded).
|
|
294
325
|
if isSettable, let valueType {
|
|
295
326
|
let setDecode: String
|
|
296
327
|
if let accessor = fastDecodeAccessor(for: valueType) {
|
|
297
|
-
setDecode = "
|
|
328
|
+
setDecode = "\(callee).\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
|
|
298
329
|
} else {
|
|
330
|
+
let exprType = expressionType(valueType)
|
|
299
331
|
setDecode =
|
|
300
|
-
"
|
|
332
|
+
"\(callee).\(swiftName) = try \(exprType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(exprType)"
|
|
301
333
|
}
|
|
302
334
|
lines.append(
|
|
303
|
-
accessorClosure(
|
|
335
|
+
accessorClosure(
|
|
336
|
+
descriptorName, "set", receiver: receiver, usesAppContext: usesAppContext,
|
|
337
|
+
body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
304
338
|
}
|
|
305
339
|
|
|
306
|
-
lines.append("object.defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
340
|
+
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
307
341
|
|
|
308
342
|
return lines
|
|
309
343
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
@@ -311,21 +345,29 @@ internal struct JSProperty {
|
|
|
311
345
|
.joined(separator: "\n")
|
|
312
346
|
}
|
|
313
347
|
|
|
314
|
-
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry.
|
|
315
|
-
///
|
|
316
|
-
///
|
|
348
|
+
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry. The capture list follows the
|
|
349
|
+
/// receiver (a module captures `self` strong; a shared object captures nothing of the instance) and,
|
|
350
|
+
/// when `usesAppContext`, adds `appContext` weak + guarded (matching the function bindings);
|
|
351
|
+
/// otherwise the guard is omitted so a primitive accessor doesn't warn on an unused capture.
|
|
317
352
|
private func accessorClosure(
|
|
318
|
-
_ descriptorName: String, _ key: String, usesAppContext: Bool, body: String
|
|
353
|
+
_ descriptorName: String, _ key: String, receiver: Receiver, usesAppContext: Bool, body: String
|
|
319
354
|
) -> String {
|
|
355
|
+
let captures = receiver.captureClause(usesAppContext: usesAppContext)
|
|
320
356
|
// Indent each line of a (possibly multi-line) body to sit one level inside the closure, aligned
|
|
321
357
|
// with the `guard`; a bare `\(body)` interpolation would only indent the first line.
|
|
322
358
|
let indentedBody = body
|
|
323
359
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
324
360
|
.map { " \($0)" }
|
|
325
361
|
.joined(separator: "\n")
|
|
362
|
+
// Property `get`/`set` accessors are always synchronous, so they bind through the unowned-`this`
|
|
363
|
+
// `setProperty` overload like sync functions. The parameter list is parenthesized and fully typed
|
|
364
|
+
// because Swift rejects a type annotation on a shorthand closure parameter; the explicit
|
|
365
|
+
// `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
|
|
366
|
+
// a shared object unwraps it in the body.
|
|
367
|
+
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
326
368
|
if usesAppContext {
|
|
327
369
|
return """
|
|
328
|
-
\(descriptorName).setProperty("\(key)") {
|
|
370
|
+
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
329
371
|
guard let appContext else {
|
|
330
372
|
throw Exceptions.AppContextLost()
|
|
331
373
|
}
|
|
@@ -334,24 +376,31 @@ internal struct JSProperty {
|
|
|
334
376
|
"""
|
|
335
377
|
}
|
|
336
378
|
return """
|
|
337
|
-
\(descriptorName).setProperty("\(key)") {
|
|
379
|
+
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
338
380
|
\(indentedBody)
|
|
339
381
|
}
|
|
340
382
|
"""
|
|
341
383
|
}
|
|
342
384
|
}
|
|
343
385
|
|
|
386
|
+
/// The body shared by both decorators: every `@JS func` bound via an inlined `setProperty` closure
|
|
387
|
+
/// and every `@JS var` via a `defineProperty` accessor, joined for the function body. The `receiver`
|
|
388
|
+
/// selects how each binding reaches its Swift value (module `self` vs. shared-object `_self`).
|
|
389
|
+
private func decorateBody(functions: [JSFunction], properties: [JSProperty], receiver: Receiver) -> String {
|
|
390
|
+
let functionBody = functions.map { $0.decorateStatements(receiver: receiver) }
|
|
391
|
+
let propertyBody = properties.map { $0.decorateStatements(receiver: receiver) }
|
|
392
|
+
return (functionBody + propertyBody).joined(separator: "\n")
|
|
393
|
+
}
|
|
394
|
+
|
|
344
395
|
/// The single generated function that decorates the module's JS object. Core supplies the object;
|
|
345
396
|
/// this binds every `@JS func` (via an inlined `setProperty` closure) and every `@JS var` (via a
|
|
346
397
|
/// `defineProperty` accessor) into it. Mirrors core's `ObjectDefinition.decorate(object:)`, including
|
|
347
398
|
/// its `borrowing` object parameter (it mutates through the reference without reassigning or taking
|
|
348
399
|
/// ownership). Named `_decorateModule` with the leading-underscore convention for synthesized members
|
|
349
400
|
/// the **runtime calls by name**; the `ExpoModule` suffix names the `@ExpoModule` macro it came from (a
|
|
350
|
-
/// shared object's counterpart is `_decorateSharedObject`).
|
|
401
|
+
/// shared object's counterpart is `_decorateSharedObject`). The bindings call into the module `self`.
|
|
351
402
|
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
352
|
-
let
|
|
353
|
-
let propertyBody = properties.map { $0.decorateStatements }
|
|
354
|
-
let body = (functionBody + propertyBody).joined(separator: "\n")
|
|
403
|
+
let body = decorateBody(functions: functions, properties: properties, receiver: .module)
|
|
355
404
|
return """
|
|
356
405
|
@JavaScriptActor
|
|
357
406
|
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
@@ -360,23 +409,24 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
360
409
|
"""
|
|
361
410
|
}
|
|
362
411
|
|
|
363
|
-
/// The
|
|
364
|
-
///
|
|
365
|
-
///
|
|
366
|
-
///
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
412
|
+
/// The shared-object counterpart of `_decorateModule`. Core supplies the class `prototype`; this binds
|
|
413
|
+
/// every `@JS func` and `@JS var` of the given shared-object type onto it. Because a shared object has a
|
|
414
|
+
/// distinct native instance behind each JS object, the bindings are **static** and recover the typed
|
|
415
|
+
/// receiver from the JS `this` per call (`try SharedObject.native(from: this.asObject(in: runtime), as: <Type>.self)`)
|
|
416
|
+
/// rather than capturing a singleton `self`. The first parameter is `prototype` (not `object` as on
|
|
417
|
+
/// `_decorateModule`) because it's the shared class prototype, not an instance. The constructor is
|
|
418
|
+
/// bound separately (see `JSConstructor.buildConstructor`). Only emitted when the type has at least one
|
|
419
|
+
/// `@JS func`/`var`.
|
|
420
|
+
internal func buildDecorateSharedObject(
|
|
421
|
+
functions: [JSFunction], properties: [JSProperty], typeName: String
|
|
422
|
+
) -> DeclSyntax {
|
|
423
|
+
let body = decorateBody(functions: functions, properties: properties, receiver: .sharedObject(typeName: typeName))
|
|
424
|
+
return """
|
|
425
|
+
@JavaScriptActor
|
|
426
|
+
public static func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
427
|
+
\(raw: body)
|
|
428
|
+
}
|
|
429
|
+
"""
|
|
380
430
|
}
|
|
381
431
|
|
|
382
432
|
/// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
|
|
@@ -244,55 +244,3 @@ private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
|
244
244
|
}
|
|
245
245
|
return false
|
|
246
246
|
}
|
|
247
|
-
|
|
248
|
-
// MARK: - Member builders
|
|
249
|
-
|
|
250
|
-
private func collectProperties(
|
|
251
|
-
varDecl: VariableDeclSyntax,
|
|
252
|
-
attribute: AttributeSyntax
|
|
253
|
-
) -> [JSProperty] {
|
|
254
|
-
let jsNameOverride = jsNameArgument(of: attribute)
|
|
255
|
-
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
256
|
-
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
257
|
-
|
|
258
|
-
return varDecl.bindings.compactMap { binding in
|
|
259
|
-
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
260
|
-
return nil
|
|
261
|
-
}
|
|
262
|
-
let swiftName = ident.identifier.text
|
|
263
|
-
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
264
|
-
// when there's none. `nil` falls back to inference at the use site.
|
|
265
|
-
let valueType = binding.typeAnnotation?.type.trimmedDescription
|
|
266
|
-
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
267
|
-
return JSProperty(
|
|
268
|
-
swiftName: swiftName,
|
|
269
|
-
jsName: jsNameOverride ?? swiftName,
|
|
270
|
-
valueType: valueType,
|
|
271
|
-
isSettable: isVar && bindingIsSettable(binding)
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
|
|
277
|
-
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
278
|
-
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
279
|
-
/// observers imply stored storage, which is also settable.
|
|
280
|
-
private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
281
|
-
guard let accessorBlock = binding.accessorBlock else {
|
|
282
|
-
return true
|
|
283
|
-
}
|
|
284
|
-
switch accessorBlock.accessors {
|
|
285
|
-
case .accessors(let accessors):
|
|
286
|
-
return accessors.contains { accessor in
|
|
287
|
-
switch accessor.accessorSpecifier.tokenKind {
|
|
288
|
-
case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
|
|
289
|
-
return true
|
|
290
|
-
default:
|
|
291
|
-
return false
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
case .getter:
|
|
295
|
-
return false
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// A `@JS init` collected for direct JSI binding. A shared-object type has at most one (JS classes
|
|
4
|
+
/// have a single constructor). Instead of a `Constructor { … }` DSL entry, the macro synthesizes a
|
|
5
|
+
/// static `_constructSharedObject(...)` that decodes the JS arguments and returns a fresh instance;
|
|
6
|
+
/// unlike the method/property bindings it produces the native instance rather than recovering one.
|
|
7
|
+
internal struct JSConstructor {
|
|
8
|
+
let parameters: [FunctionParameterSyntax]
|
|
9
|
+
|
|
10
|
+
init(initDecl: InitializerDeclSyntax) {
|
|
11
|
+
self.parameters = Array(initDecl.signature.parameterClause.parameters)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// The body statements, indented with `indent`: arity guard, per-argument decode (primitives via a
|
|
15
|
+
/// typed accessor, others via the dynamic converter), then `return <Type>(label: arg0, …)`.
|
|
16
|
+
private func bodyStatements(typeName: String, indent: String) -> String {
|
|
17
|
+
var lines: [String] = []
|
|
18
|
+
|
|
19
|
+
lines.append(
|
|
20
|
+
"""
|
|
21
|
+
guard arguments.count == \(parameters.count) else {
|
|
22
|
+
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(typeName)", received: arguments.count, required: \(parameters.count), maximum: \(parameters.count)))
|
|
23
|
+
}
|
|
24
|
+
""")
|
|
25
|
+
|
|
26
|
+
var callArguments: [String] = []
|
|
27
|
+
for (index, parameter) in parameters.enumerated() {
|
|
28
|
+
let type = parameter.type.trimmedDescription
|
|
29
|
+
|
|
30
|
+
if let accessor = fastDecodeAccessor(for: type) {
|
|
31
|
+
lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
|
|
32
|
+
} else {
|
|
33
|
+
let exprType = expressionType(type)
|
|
34
|
+
lines.append(
|
|
35
|
+
"let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let label = parameter.firstName.text
|
|
39
|
+
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
|
|
43
|
+
|
|
44
|
+
return lines
|
|
45
|
+
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
46
|
+
.map { indent + $0 }
|
|
47
|
+
.joined(separator: "\n")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The static `_constructSharedObject` entry point the runtime calls to build an instance from JS
|
|
51
|
+
/// arguments, returning the concrete type. `this`/`appContext` may go unreferenced, which is harmless.
|
|
52
|
+
func buildConstructor(typeName: String) -> DeclSyntax {
|
|
53
|
+
return """
|
|
54
|
+
@JavaScriptActor
|
|
55
|
+
public static func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime, appContext: AppContext) throws -> \(raw: typeName) {
|
|
56
|
+
\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
}
|
|
60
|
+
}
|