@expo/expo-modules-macros-plugin 0.4.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/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +137 -107
- 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/package.json +1 -1
|
Binary file
|
|
@@ -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,42 +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
|
-
|
|
204
|
-
// Synchronous `@JS`
|
|
205
|
-
//
|
|
206
|
-
// `
|
|
207
|
-
// `
|
|
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
|
|
208
213
|
// JavaScriptUnownedValue` to select that (otherwise `@_disfavoredOverload`) overload — which
|
|
209
214
|
// requires the *parenthesized, fully typed* parameter list, since Swift rejects a type annotation
|
|
210
215
|
// on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
|
|
211
216
|
// shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
|
|
212
217
|
// escapes into the task anyway.
|
|
213
|
-
let captures = usesAppContext
|
|
218
|
+
let captures = receiver.captureClause(usesAppContext: usesAppContext)
|
|
214
219
|
let parameters =
|
|
215
220
|
isAsync
|
|
216
221
|
? "this, arguments"
|
|
217
222
|
: "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
218
223
|
|
|
224
|
+
let object = receiver.decoratedObject
|
|
219
225
|
if usesAppContext {
|
|
220
226
|
return """
|
|
221
|
-
object.setProperty("\(jsName)") { \(captures)
|
|
227
|
+
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
222
228
|
guard let appContext else {
|
|
223
229
|
throw Exceptions.AppContextLost()
|
|
224
230
|
}
|
|
225
|
-
\(bodyStatements(indent: " "))
|
|
231
|
+
\(bodyStatements(receiver: receiver, indent: " "))
|
|
226
232
|
}
|
|
227
233
|
"""
|
|
228
234
|
}
|
|
229
235
|
return """
|
|
230
|
-
object.setProperty("\(jsName)") { \(captures)
|
|
231
|
-
\(bodyStatements(indent: " "))
|
|
236
|
+
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
237
|
+
\(bodyStatements(receiver: receiver, indent: " "))
|
|
232
238
|
}
|
|
233
239
|
"""
|
|
234
240
|
}
|
|
@@ -248,17 +254,18 @@ internal struct JSFunction {
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
251
|
-
/// `Property(...)` DSL entry,
|
|
252
|
-
///
|
|
253
|
-
/// when the property is settable) and installs it with
|
|
254
|
-
/// mirroring core's `PropertyDefinition.buildDescriptor`.
|
|
255
|
-
/// installed the same way `@JS func`s are — the closure-taking
|
|
256
|
-
/// 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.
|
|
257
263
|
///
|
|
258
|
-
/// The receiver is the module's
|
|
259
|
-
///
|
|
260
|
-
///
|
|
261
|
-
///
|
|
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).
|
|
262
269
|
internal struct JSProperty {
|
|
263
270
|
let swiftName: String
|
|
264
271
|
let jsName: String
|
|
@@ -271,54 +278,66 @@ internal struct JSProperty {
|
|
|
271
278
|
let isSettable: Bool
|
|
272
279
|
|
|
273
280
|
/// The statements that install this property's accessor on the JS object, indented for the
|
|
274
|
-
///
|
|
275
|
-
///
|
|
276
|
-
///
|
|
277
|
-
///
|
|
278
|
-
///
|
|
279
|
-
///
|
|
280
|
-
/// warning. Getter and setter are gated
|
|
281
|
-
|
|
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 {
|
|
282
290
|
let descriptorName = "\(swiftName)Descriptor"
|
|
291
|
+
let callee = receiver.callee
|
|
292
|
+
let object = receiver.decoratedObject
|
|
283
293
|
// A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
|
|
284
294
|
// references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
|
|
285
295
|
// getter, which also doesn't use it.
|
|
286
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" } ?? ""
|
|
287
300
|
var lines: [String] = []
|
|
288
301
|
|
|
289
302
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
290
303
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
291
304
|
|
|
292
|
-
// Getter: read
|
|
305
|
+
// Getter: read `<callee>.<name>` and encode the result back to JS.
|
|
293
306
|
let getEncode: String
|
|
294
307
|
if let valueType, fastDecodeAccessor(for: valueType) != nil {
|
|
295
|
-
getEncode = "return
|
|
308
|
+
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
296
309
|
} else if let valueType {
|
|
297
310
|
getEncode =
|
|
298
|
-
"return try \(valueType).getDynamicType().castToJS(
|
|
311
|
+
"return try \(expressionType(valueType)).getDynamicType().castToJS(\(callee).\(swiftName), appContext: appContext, in: runtime)"
|
|
299
312
|
} else {
|
|
300
|
-
// No known type: fall back to converting whatever
|
|
301
|
-
// declaration has neither an annotation nor a literal default, which is rare for a stored
|
|
302
|
-
|
|
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)"
|
|
303
317
|
}
|
|
304
|
-
lines.append(
|
|
318
|
+
lines.append(
|
|
319
|
+
accessorClosure(
|
|
320
|
+
descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
|
|
305
321
|
|
|
306
|
-
// Setter: decode argument 0 by the static type and write
|
|
307
|
-
// 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
|
|
308
324
|
// settable var with neither an annotation nor a literal default is rare and can't be decoded).
|
|
309
325
|
if isSettable, let valueType {
|
|
310
326
|
let setDecode: String
|
|
311
327
|
if let accessor = fastDecodeAccessor(for: valueType) {
|
|
312
|
-
setDecode = "
|
|
328
|
+
setDecode = "\(callee).\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
|
|
313
329
|
} else {
|
|
330
|
+
let exprType = expressionType(valueType)
|
|
314
331
|
setDecode =
|
|
315
|
-
"
|
|
332
|
+
"\(callee).\(swiftName) = try \(exprType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(exprType)"
|
|
316
333
|
}
|
|
317
334
|
lines.append(
|
|
318
|
-
accessorClosure(
|
|
335
|
+
accessorClosure(
|
|
336
|
+
descriptorName, "set", receiver: receiver, usesAppContext: usesAppContext,
|
|
337
|
+
body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
319
338
|
}
|
|
320
339
|
|
|
321
|
-
lines.append("object.defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
340
|
+
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
322
341
|
|
|
323
342
|
return lines
|
|
324
343
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
@@ -326,26 +345,29 @@ internal struct JSProperty {
|
|
|
326
345
|
.joined(separator: "\n")
|
|
327
346
|
}
|
|
328
347
|
|
|
329
|
-
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry.
|
|
330
|
-
///
|
|
331
|
-
///
|
|
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.
|
|
332
352
|
private func accessorClosure(
|
|
333
|
-
_ descriptorName: String, _ key: String, usesAppContext: Bool, body: String
|
|
353
|
+
_ descriptorName: String, _ key: String, receiver: Receiver, usesAppContext: Bool, body: String
|
|
334
354
|
) -> String {
|
|
355
|
+
let captures = receiver.captureClause(usesAppContext: usesAppContext)
|
|
335
356
|
// Indent each line of a (possibly multi-line) body to sit one level inside the closure, aligned
|
|
336
357
|
// with the `guard`; a bare `\(body)` interpolation would only indent the first line.
|
|
337
358
|
let indentedBody = body
|
|
338
359
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
339
360
|
.map { " \($0)" }
|
|
340
361
|
.joined(separator: "\n")
|
|
341
|
-
// Property `get`/`set` accessors are always synchronous
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
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.
|
|
345
367
|
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
346
368
|
if usesAppContext {
|
|
347
369
|
return """
|
|
348
|
-
\(descriptorName).setProperty("\(key)") {
|
|
370
|
+
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
349
371
|
guard let appContext else {
|
|
350
372
|
throw Exceptions.AppContextLost()
|
|
351
373
|
}
|
|
@@ -354,24 +376,31 @@ internal struct JSProperty {
|
|
|
354
376
|
"""
|
|
355
377
|
}
|
|
356
378
|
return """
|
|
357
|
-
\(descriptorName).setProperty("\(key)") {
|
|
379
|
+
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
358
380
|
\(indentedBody)
|
|
359
381
|
}
|
|
360
382
|
"""
|
|
361
383
|
}
|
|
362
384
|
}
|
|
363
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
|
+
|
|
364
395
|
/// The single generated function that decorates the module's JS object. Core supplies the object;
|
|
365
396
|
/// this binds every `@JS func` (via an inlined `setProperty` closure) and every `@JS var` (via a
|
|
366
397
|
/// `defineProperty` accessor) into it. Mirrors core's `ObjectDefinition.decorate(object:)`, including
|
|
367
398
|
/// its `borrowing` object parameter (it mutates through the reference without reassigning or taking
|
|
368
399
|
/// ownership). Named `_decorateModule` with the leading-underscore convention for synthesized members
|
|
369
400
|
/// the **runtime calls by name**; the `ExpoModule` suffix names the `@ExpoModule` macro it came from (a
|
|
370
|
-
/// shared object's counterpart is `_decorateSharedObject`).
|
|
401
|
+
/// shared object's counterpart is `_decorateSharedObject`). The bindings call into the module `self`.
|
|
371
402
|
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
372
|
-
let
|
|
373
|
-
let propertyBody = properties.map { $0.decorateStatements }
|
|
374
|
-
let body = (functionBody + propertyBody).joined(separator: "\n")
|
|
403
|
+
let body = decorateBody(functions: functions, properties: properties, receiver: .module)
|
|
375
404
|
return """
|
|
376
405
|
@JavaScriptActor
|
|
377
406
|
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
@@ -380,23 +409,24 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
380
409
|
"""
|
|
381
410
|
}
|
|
382
411
|
|
|
383
|
-
/// The
|
|
384
|
-
///
|
|
385
|
-
///
|
|
386
|
-
///
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
"""
|
|
400
430
|
}
|
|
401
431
|
|
|
402
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
|
+
}
|
|
@@ -245,3 +245,87 @@ extension AttributeListSyntax {
|
|
|
245
245
|
return nil
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
|
+
|
|
249
|
+
/// A type spelled so it's valid in expression position (before `.getDynamicType()` or after `as!`).
|
|
250
|
+
/// Implicitly-unwrapped optionals (`T!`) are only allowed in type-annotation position, so a trailing
|
|
251
|
+
/// `!` is rewritten to `?` (`T!` and `T?` are both `Optional<T>`, which the dynamic-type / cast layer
|
|
252
|
+
/// treats identically). Other type spellings pass through unchanged.
|
|
253
|
+
internal func expressionType(_ type: String) -> String {
|
|
254
|
+
guard type.hasSuffix("!") else {
|
|
255
|
+
return type
|
|
256
|
+
}
|
|
257
|
+
return type.dropLast() + "?"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// MARK: - @JS property collection
|
|
261
|
+
|
|
262
|
+
/// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
|
|
263
|
+
/// Shared between `@ExpoModule` and `@SharedObject` — the resulting properties are receiver-agnostic;
|
|
264
|
+
/// the decorator that emits them picks the receiver (module `self` vs. shared-object `_self`).
|
|
265
|
+
internal func collectProperties(
|
|
266
|
+
varDecl: VariableDeclSyntax,
|
|
267
|
+
attribute: AttributeSyntax
|
|
268
|
+
) -> [JSProperty] {
|
|
269
|
+
let jsNameOverride = jsNameArgument(of: attribute)
|
|
270
|
+
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
271
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
272
|
+
|
|
273
|
+
return varDecl.bindings.compactMap { binding in
|
|
274
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
275
|
+
return nil
|
|
276
|
+
}
|
|
277
|
+
let swiftName = ident.identifier.text
|
|
278
|
+
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
279
|
+
// when there's none. `nil` falls back to inference at the use site.
|
|
280
|
+
let valueType = binding.typeAnnotation?.type.trimmedDescription
|
|
281
|
+
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
282
|
+
return JSProperty(
|
|
283
|
+
swiftName: swiftName,
|
|
284
|
+
jsName: jsNameOverride ?? swiftName,
|
|
285
|
+
valueType: valueType,
|
|
286
|
+
isSettable: isVar && bindingIsSettable(binding)
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
|
|
292
|
+
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
293
|
+
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
294
|
+
/// observers imply stored storage, which is also settable.
|
|
295
|
+
private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
296
|
+
guard let accessorBlock = binding.accessorBlock else {
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
switch accessorBlock.accessors {
|
|
300
|
+
case .accessors(let accessors):
|
|
301
|
+
return accessors.contains { accessor in
|
|
302
|
+
switch accessor.accessorSpecifier.tokenKind {
|
|
303
|
+
case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
|
|
304
|
+
return true
|
|
305
|
+
default:
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
case .getter:
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly
|
|
315
|
+
/// (`asDouble()` for `Double`, etc.), bypassing the dynamic-type converter. Returns `nil` for types
|
|
316
|
+
/// without a dedicated accessor (arrays, records, optionals, shared objects, other numeric widths),
|
|
317
|
+
/// which decode through `getDynamicType().cast(...)`.
|
|
318
|
+
func fastDecodeAccessor(for type: String) -> String? {
|
|
319
|
+
switch type {
|
|
320
|
+
case "Bool":
|
|
321
|
+
return "asBool"
|
|
322
|
+
case "Int":
|
|
323
|
+
return "asInt"
|
|
324
|
+
case "Double":
|
|
325
|
+
return "asDouble"
|
|
326
|
+
case "String":
|
|
327
|
+
return "asString"
|
|
328
|
+
default:
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// Where a directly-bound closure gets the Swift value it calls into. A module is a singleton, so its
|
|
4
|
+
/// bindings call `self` and ignore the JS `this`; a shared object has a distinct native instance per JS
|
|
5
|
+
/// object, so its bindings recover the typed receiver from `this`.
|
|
6
|
+
internal enum Receiver {
|
|
7
|
+
/// The module singleton; the closure captures `self` strong.
|
|
8
|
+
case module
|
|
9
|
+
/// A shared object of the given concrete type; the closure captures nothing and recovers the receiver
|
|
10
|
+
/// from `this` per call.
|
|
11
|
+
case sharedObject(typeName: String)
|
|
12
|
+
|
|
13
|
+
/// The expression the body calls members on: `self` for a module, `_self` (bound by `unwrapStatement`)
|
|
14
|
+
/// for a shared object. The leading underscore avoids colliding with a user member like `var owner`.
|
|
15
|
+
var callee: String {
|
|
16
|
+
switch self {
|
|
17
|
+
case .module:
|
|
18
|
+
return "self"
|
|
19
|
+
case .sharedObject:
|
|
20
|
+
return "_self"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The JS object the decorator binds members onto, matching its first parameter: `object` for a
|
|
25
|
+
/// module (its own JS object), `prototype` for a shared object (the shared class prototype).
|
|
26
|
+
var decoratedObject: String {
|
|
27
|
+
switch self {
|
|
28
|
+
case .module:
|
|
29
|
+
return "object"
|
|
30
|
+
case .sharedObject:
|
|
31
|
+
return "prototype"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// The leading body line binding the receiver, or `nil` for a module (it reads `self` directly). For a
|
|
36
|
+
/// shared object, `native(from:as:)` recovers the typed instance from the borrowed `this`, throwing on
|
|
37
|
+
/// a foreign object or a type mismatch.
|
|
38
|
+
var unwrapStatement: String? {
|
|
39
|
+
switch self {
|
|
40
|
+
case .module:
|
|
41
|
+
return nil
|
|
42
|
+
case .sharedObject(let typeName):
|
|
43
|
+
return "let _self = try SharedObject.native(from: this.asObject(in: runtime), as: \(typeName).self)"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The capture-clause fragment (with a trailing space, or empty when nothing is captured). A module
|
|
48
|
+
/// captures `self` strong; a shared object captures nothing of the instance. `appContext`, when used,
|
|
49
|
+
/// is captured weak in both cases.
|
|
50
|
+
func captureClause(usesAppContext: Bool) -> String {
|
|
51
|
+
switch self {
|
|
52
|
+
case .module:
|
|
53
|
+
return usesAppContext ? "[weak appContext, self] " : "[self] "
|
|
54
|
+
case .sharedObject:
|
|
55
|
+
return usesAppContext ? "[weak appContext] " : ""
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -316,7 +316,8 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
316
316
|
var lines: [String] = []
|
|
317
317
|
for property in properties {
|
|
318
318
|
let valueVar = "\(property.name)JSValue"
|
|
319
|
-
let
|
|
319
|
+
let exprType = expressionType(property.type)
|
|
320
|
+
let cast = "try \(exprType).getDynamicType().cast(jsValue: \(valueVar), appContext: appContext) as! \(exprType)"
|
|
320
321
|
lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
|
|
321
322
|
if property.isRequired {
|
|
322
323
|
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
@@ -337,7 +338,8 @@ private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
337
338
|
var lines: [String] = []
|
|
338
339
|
for property in properties {
|
|
339
340
|
let valueVar = "\(property.name)Value"
|
|
340
|
-
let
|
|
341
|
+
let exprType = expressionType(property.type)
|
|
342
|
+
let cast = "try \(exprType).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(exprType)"
|
|
341
343
|
lines.append(" let \(valueVar) = dictionary[\"\(property.name)\"]")
|
|
342
344
|
if property.isRequired {
|
|
343
345
|
lines.append(" guard let \(valueVar) else {")
|
|
@@ -399,7 +401,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
|
|
|
399
401
|
lines.append(" let object = try appContext.runtime.createObject()")
|
|
400
402
|
}
|
|
401
403
|
for property in properties {
|
|
402
|
-
lines.append(" object.setProperty(\"\(property.name)\", value: try \(property.type).getDynamicType().convertToJS(self.\(property.name), appContext: appContext))")
|
|
404
|
+
lines.append(" object.setProperty(\"\(property.name)\", value: try \(expressionType(property.type)).getDynamicType().convertToJS(self.\(property.name), appContext: appContext))")
|
|
403
405
|
}
|
|
404
406
|
lines.append(" return object")
|
|
405
407
|
let body = lines.joined(separator: "\n")
|
|
@@ -41,35 +41,38 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
41
41
|
let typeName = classDecl.name.text
|
|
42
42
|
let jsName = jsNameArgument(of: node) ?? typeName
|
|
43
43
|
|
|
44
|
-
var
|
|
45
|
-
|
|
44
|
+
// `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
|
|
45
|
+
// the synthesized `_decorateSharedObject` / `_constructSharedObject` rather than described with a
|
|
46
|
+
// `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so they're collected here
|
|
47
|
+
// instead of appended to the `Class` block. The block keeps only non-`@JS` definitions (none are
|
|
48
|
+
// collected today), so it's empty when every member is `@JS`.
|
|
49
|
+
let entries: [String] = []
|
|
50
|
+
var functions: [JSFunction] = []
|
|
51
|
+
var properties: [JSProperty] = []
|
|
52
|
+
var constructor: JSConstructor?
|
|
46
53
|
|
|
47
54
|
for member in classDecl.memberBlock.members {
|
|
48
55
|
let decl = member.decl
|
|
49
56
|
|
|
50
57
|
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
51
58
|
initDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
52
|
-
if
|
|
59
|
+
if constructor != nil {
|
|
53
60
|
throw MacroExpansionErrorMessage(
|
|
54
61
|
"@SharedObject classes can have at most one @JS initializer; JavaScript classes have a single constructor.")
|
|
55
62
|
}
|
|
56
|
-
|
|
57
|
-
entries.append(buildConstructorEntry(initDecl: initDecl, typeName: typeName))
|
|
63
|
+
constructor = JSConstructor(initDecl: initDecl)
|
|
58
64
|
continue
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
62
68
|
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
63
|
-
|
|
64
|
-
buildClassFunctionEntry(funcDecl: funcDecl, attribute: attribute, typeName: typeName))
|
|
69
|
+
functions.append(JSFunction(funcDecl: funcDecl, attribute: attribute))
|
|
65
70
|
continue
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
69
74
|
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
70
|
-
|
|
71
|
-
contentsOf: buildClassPropertyEntries(
|
|
72
|
-
varDecl: varDecl, attribute: attribute, typeName: typeName))
|
|
75
|
+
properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -78,13 +81,27 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
78
81
|
? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
|
|
79
82
|
: " return Class(\"\(jsName)\", \(typeName).self) {\n\(lines)\n }"
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
var emitted: [DeclSyntax] = [
|
|
85
|
+
"""
|
|
82
86
|
public static func _synthesizedClassDefinition() -> ClassDefinition {
|
|
83
87
|
\(raw: body)
|
|
84
88
|
}
|
|
85
89
|
"""
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
// Direct JSI binding: one `_decorateSharedObject` that binds each `@JS func`/`var` onto the JS
|
|
93
|
+
// object (unwrapping the per-call receiver from `this`), and a `_constructSharedObject` that
|
|
94
|
+
// builds an instance from the `@JS init` arguments. Each is emitted only when it has something
|
|
95
|
+
// to do.
|
|
96
|
+
if !functions.isEmpty || !properties.isEmpty {
|
|
97
|
+
emitted.append(
|
|
98
|
+
buildDecorateSharedObject(functions: functions, properties: properties, typeName: typeName))
|
|
99
|
+
}
|
|
100
|
+
if let constructor {
|
|
101
|
+
emitted.append(constructor.buildConstructor(typeName: typeName))
|
|
102
|
+
}
|
|
86
103
|
|
|
87
|
-
return
|
|
104
|
+
return emitted
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
|
|
@@ -111,81 +128,3 @@ extension SharedObjectMacro: MemberAttributeMacro {
|
|
|
111
128
|
private func inheritsFromSharedObject(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
112
129
|
return inheritsFromAny(classDecl, names: ["SharedObject"])
|
|
113
130
|
}
|
|
114
|
-
|
|
115
|
-
// MARK: - Class-scope entry builders
|
|
116
|
-
|
|
117
|
-
private func buildClassFunctionEntry(
|
|
118
|
-
funcDecl: FunctionDeclSyntax,
|
|
119
|
-
attribute: AttributeSyntax,
|
|
120
|
-
typeName: String
|
|
121
|
-
) -> String {
|
|
122
|
-
let swiftName = funcDecl.name.text
|
|
123
|
-
let jsName = jsNameArgument(of: attribute) ?? swiftName
|
|
124
|
-
let effects = funcDecl.signature.effectSpecifiers
|
|
125
|
-
let isAsync = effects?.asyncSpecifier != nil
|
|
126
|
-
let isThrowing = effects?.throwsClause?.throwsSpecifier != nil
|
|
127
|
-
let dslEntry = isAsync ? "AsyncFunction" : "Function"
|
|
128
|
-
|
|
129
|
-
let params = funcDecl.signature.parameterClause.parameters
|
|
130
|
-
let closureParamList: String
|
|
131
|
-
let callArgList: String
|
|
132
|
-
if params.isEmpty {
|
|
133
|
-
closureParamList = "(this: \(typeName))"
|
|
134
|
-
callArgList = ""
|
|
135
|
-
} else {
|
|
136
|
-
let typedParams = params.enumerated().map { index, param in
|
|
137
|
-
"_ arg\(index): \(param.type.trimmedDescription)"
|
|
138
|
-
}.joined(separator: ", ")
|
|
139
|
-
closureParamList = "(this: \(typeName), \(typedParams))"
|
|
140
|
-
|
|
141
|
-
callArgList = params.enumerated().map { index, param in
|
|
142
|
-
let label = param.firstName.text
|
|
143
|
-
return label == "_" ? "arg\(index)" : "\(label): arg\(index)"
|
|
144
|
-
}.joined(separator: ", ")
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let awaitKeyword = isAsync ? "await " : ""
|
|
148
|
-
let tryKeyword = (isAsync || isThrowing) ? "try " : ""
|
|
149
|
-
let callExpr = "\(tryKeyword)\(awaitKeyword)this.\(swiftName)(\(callArgList))"
|
|
150
|
-
|
|
151
|
-
return "\(dslEntry)(\"\(jsName)\") { \(closureParamList) in \(callExpr) }"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private func buildClassPropertyEntries(
|
|
155
|
-
varDecl: VariableDeclSyntax,
|
|
156
|
-
attribute: AttributeSyntax,
|
|
157
|
-
typeName: String
|
|
158
|
-
) -> [String] {
|
|
159
|
-
let jsNameOverride = jsNameArgument(of: attribute)
|
|
160
|
-
|
|
161
|
-
return varDecl.bindings.compactMap { binding in
|
|
162
|
-
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
163
|
-
return nil
|
|
164
|
-
}
|
|
165
|
-
let swiftName = ident.identifier.text
|
|
166
|
-
let jsName = jsNameOverride ?? swiftName
|
|
167
|
-
return "Property(\"\(jsName)\") { (this: \(typeName)) in this.\(swiftName) }"
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private func buildConstructorEntry(
|
|
172
|
-
initDecl: InitializerDeclSyntax,
|
|
173
|
-
typeName: String
|
|
174
|
-
) -> String {
|
|
175
|
-
let params = initDecl.signature.parameterClause.parameters
|
|
176
|
-
|
|
177
|
-
if params.isEmpty {
|
|
178
|
-
return "Constructor { \(typeName)() }"
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let argList = params.enumerated().map { index, param in
|
|
182
|
-
"_ arg\(index): \(param.type.trimmedDescription)"
|
|
183
|
-
}.joined(separator: ", ")
|
|
184
|
-
|
|
185
|
-
let callArgs = params.enumerated().map { index, param in
|
|
186
|
-
let label = param.firstName.text
|
|
187
|
-
return label == "_" ? "arg\(index)" : "\(label): arg\(index)"
|
|
188
|
-
}.joined(separator: ", ")
|
|
189
|
-
|
|
190
|
-
return "Constructor { (\(argList)) in \(typeName)(\(callArgs)) }"
|
|
191
|
-
}
|