@expo/expo-modules-macros-plugin 0.5.0 → 0.6.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 +47 -109
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +14 -17
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +43 -22
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +6 -24
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +4 -5
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +50 -27
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +88 -34
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +23 -7
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +186 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +47 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +307 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +255 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +5 -5
- package/apple/Sources/ExpoModulesScannerCLI/main.swift +4 -3
- package/package.json +1 -1
|
Binary file
|
|
@@ -50,12 +50,11 @@ internal struct JSFunction {
|
|
|
50
50
|
/// The decode-call-encode statements that form the host-function body, indented with the given
|
|
51
51
|
/// prefix. Receiver unwrap (shared objects only), then an arity guard (an exact check when every
|
|
52
52
|
/// parameter is required, otherwise a range check) throwing `Exceptions.ArgumentsRangeMismatch`;
|
|
53
|
-
/// then the decode of the always-present required prefix
|
|
54
|
-
///
|
|
55
|
-
/// `
|
|
56
|
-
/// `
|
|
57
|
-
///
|
|
58
|
-
/// has — `arguments[i]` traps past `count`, so a slot the caller didn't pass is never indexed.
|
|
53
|
+
/// then the decode of the always-present required prefix via `JavaScriptDecodable.decode` on a
|
|
54
|
+
/// zero-copy `arguments.unownedValue(at:)`; then the call and result encode via
|
|
55
|
+
/// `JavaScriptEncodable.encode`. When a trailing run of parameters is omittable the call branches
|
|
56
|
+
/// on `arguments.count`, decoding only the slots that branch actually has — `unownedValue(at:)` is
|
|
57
|
+
/// unchecked, so a slot the caller didn't pass is never indexed.
|
|
59
58
|
private func bodyStatements(receiver: Receiver, indent: String) -> String {
|
|
60
59
|
let required = requiredArgumentCount
|
|
61
60
|
let maximum = parameters.count
|
|
@@ -119,18 +118,14 @@ internal struct JSFunction {
|
|
|
119
118
|
.joined(separator: "\n")
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/// `let arg<index> = …` decoding the slot at `index` by its static type
|
|
123
|
-
///
|
|
124
|
-
///
|
|
125
|
-
///
|
|
126
|
-
///
|
|
121
|
+
/// `let arg<index> = …` decoding the slot at `index` by its static type through
|
|
122
|
+
/// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
|
|
123
|
+
/// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
|
|
124
|
+
/// primitive's `decode` is `@inlinable` and lowers to the same direct accessor a hand-rolled fast
|
|
125
|
+
/// path would use.
|
|
127
126
|
private func decodeStatement(at index: Int) -> String {
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
return "let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()"
|
|
131
|
-
}
|
|
132
|
-
let exprType = expressionType(type)
|
|
133
|
-
return "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)"
|
|
127
|
+
let exprType = expressionType(parameters[index].type.trimmedDescription)
|
|
128
|
+
return "let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)"
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
/// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
|
|
@@ -178,17 +173,17 @@ internal struct JSFunction {
|
|
|
178
173
|
return lines
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
/// Encode the `result` local back to JS and return it
|
|
182
|
-
///
|
|
183
|
-
///
|
|
176
|
+
/// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode`, the same
|
|
177
|
+
/// for every type. Unlike the decode side there's no primitive fast path: `encode` produces the same
|
|
178
|
+
/// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
|
|
179
|
+
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
180
|
+
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
181
|
+
/// no-return function returns `.undefined` instead.
|
|
184
182
|
private func encodeResultLines() -> [String] {
|
|
185
183
|
guard let returnType else {
|
|
186
184
|
return ["return .undefined"]
|
|
187
185
|
}
|
|
188
|
-
|
|
189
|
-
return ["return result.toJavaScriptValue(in: runtime)"]
|
|
190
|
-
}
|
|
191
|
-
return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
|
|
186
|
+
return ["return try \(expressionType(returnType)).encode(result, in: runtime)"]
|
|
192
187
|
}
|
|
193
188
|
|
|
194
189
|
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
@@ -201,10 +196,6 @@ internal struct JSFunction {
|
|
|
201
196
|
/// the host-function closure is what keeps the native callable alive for as long as JS can invoke
|
|
202
197
|
/// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
|
|
203
198
|
/// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
|
|
204
|
-
/// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
|
|
205
|
-
/// the app context. When no argument or return value goes through the dynamic-type converter the
|
|
206
|
-
/// body never references `appContext`, so the capture and guard are omitted to avoid the
|
|
207
|
-
/// unused-capture warning.
|
|
208
199
|
func decorateStatements(receiver: Receiver) -> String {
|
|
209
200
|
// Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
|
|
210
201
|
// `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
|
|
@@ -215,42 +206,19 @@ internal struct JSFunction {
|
|
|
215
206
|
// on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
|
|
216
207
|
// shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
|
|
217
208
|
// escapes into the task anyway.
|
|
218
|
-
let captures = receiver.captureClause
|
|
209
|
+
let captures = receiver.captureClause
|
|
219
210
|
let parameters =
|
|
220
211
|
isAsync
|
|
221
212
|
? "this, arguments"
|
|
222
213
|
: "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
223
214
|
|
|
224
215
|
let object = receiver.decoratedObject
|
|
225
|
-
if usesAppContext {
|
|
226
|
-
return """
|
|
227
|
-
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
228
|
-
guard let appContext else {
|
|
229
|
-
throw Exceptions.AppContextLost()
|
|
230
|
-
}
|
|
231
|
-
\(bodyStatements(receiver: receiver, indent: " "))
|
|
232
|
-
}
|
|
233
|
-
"""
|
|
234
|
-
}
|
|
235
216
|
return """
|
|
236
217
|
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
237
218
|
\(bodyStatements(receiver: receiver, indent: " "))
|
|
238
219
|
}
|
|
239
220
|
"""
|
|
240
221
|
}
|
|
241
|
-
|
|
242
|
-
/// True when the host-function body references `appContext` — i.e. some parameter or the return
|
|
243
|
-
/// type lacks a fast accessor and decodes/encodes through `getDynamicType()`, which threads
|
|
244
|
-
/// `appContext` in.
|
|
245
|
-
private var usesAppContext: Bool {
|
|
246
|
-
if parameters.contains(where: { fastDecodeAccessor(for: $0.type.trimmedDescription) == nil }) {
|
|
247
|
-
return true
|
|
248
|
-
}
|
|
249
|
-
if let returnType, fastDecodeAccessor(for: returnType) == nil {
|
|
250
|
-
return true
|
|
251
|
-
}
|
|
252
|
-
return false
|
|
253
|
-
}
|
|
254
222
|
}
|
|
255
223
|
|
|
256
224
|
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
@@ -263,9 +231,8 @@ internal struct JSFunction {
|
|
|
263
231
|
///
|
|
264
232
|
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
265
233
|
/// 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
|
|
267
|
-
///
|
|
268
|
-
/// the `getDynamicType()` converter).
|
|
234
|
+
/// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
|
|
235
|
+
/// `JavaScriptEncodable.encode`, the same uniform path as functions.
|
|
269
236
|
internal struct JSProperty {
|
|
270
237
|
let swiftName: String
|
|
271
238
|
let jsName: String
|
|
@@ -282,18 +249,11 @@ internal struct JSProperty {
|
|
|
282
249
|
/// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
|
|
283
250
|
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
284
251
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
285
|
-
/// instance
|
|
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.
|
|
252
|
+
/// instance. Getter and setter are gated independently.
|
|
289
253
|
func decorateStatements(receiver: Receiver) -> String {
|
|
290
254
|
let descriptorName = "\(swiftName)Descriptor"
|
|
291
255
|
let callee = receiver.callee
|
|
292
256
|
let object = receiver.decoratedObject
|
|
293
|
-
// A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
|
|
294
|
-
// references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
|
|
295
|
-
// getter, which also doesn't use it.
|
|
296
|
-
let usesAppContext = valueType.map { fastDecodeAccessor(for: $0) == nil } ?? false
|
|
297
257
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
298
258
|
// reads `self` directly. The unwrap leads each accessor body.
|
|
299
259
|
let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
|
|
@@ -302,39 +262,28 @@ internal struct JSProperty {
|
|
|
302
262
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
303
263
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
304
264
|
|
|
305
|
-
// Getter: read `<callee>.<name>` and encode the result back to JS
|
|
265
|
+
// Getter: read `<callee>.<name>` and encode the result back to JS through `encode`. When the value
|
|
266
|
+
// type couldn't be inferred (no annotation and no literal default, rare for a stored var) there's
|
|
267
|
+
// no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
|
|
306
268
|
let getEncode: String
|
|
307
|
-
if let valueType
|
|
308
|
-
getEncode = "return \(callee).\(swiftName)
|
|
309
|
-
} else if let valueType {
|
|
310
|
-
getEncode =
|
|
311
|
-
"return try \(expressionType(valueType)).getDynamicType().castToJS(\(callee).\(swiftName), appContext: appContext, in: runtime)"
|
|
269
|
+
if let valueType {
|
|
270
|
+
getEncode = "return try \(expressionType(valueType)).encode(\(callee).\(swiftName), in: runtime)"
|
|
312
271
|
} else {
|
|
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
272
|
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
317
273
|
}
|
|
318
274
|
lines.append(
|
|
319
|
-
accessorClosure(
|
|
320
|
-
descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
|
|
275
|
+
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)"))
|
|
321
276
|
|
|
322
|
-
// Setter: decode argument 0 by the static type and write `<callee>.<name>`. A
|
|
323
|
-
// a known value type; when the type couldn't be inferred the property is bound
|
|
324
|
-
// settable var with neither an annotation nor a literal default is rare and can't
|
|
277
|
+
// Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
|
|
278
|
+
// typed setter needs a known value type; when the type couldn't be inferred the property is bound
|
|
279
|
+
// getter-only (a settable var with neither an annotation nor a literal default is rare and can't
|
|
280
|
+
// be decoded).
|
|
325
281
|
if isSettable, let valueType {
|
|
326
|
-
let
|
|
327
|
-
|
|
328
|
-
setDecode = "\(callee).\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
|
|
329
|
-
} else {
|
|
330
|
-
let exprType = expressionType(valueType)
|
|
331
|
-
setDecode =
|
|
332
|
-
"\(callee).\(swiftName) = try \(exprType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(exprType)"
|
|
333
|
-
}
|
|
282
|
+
let exprType = expressionType(valueType)
|
|
283
|
+
let setDecode = "\(callee).\(swiftName) = try \(exprType).decode(arguments.unownedValue(at: 0), in: runtime)"
|
|
334
284
|
lines.append(
|
|
335
285
|
accessorClosure(
|
|
336
|
-
descriptorName, "set", receiver: receiver,
|
|
337
|
-
body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
286
|
+
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
338
287
|
}
|
|
339
288
|
|
|
340
289
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
@@ -346,15 +295,13 @@ internal struct JSProperty {
|
|
|
346
295
|
}
|
|
347
296
|
|
|
348
297
|
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry. The capture list follows the
|
|
349
|
-
/// receiver
|
|
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.
|
|
298
|
+
/// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
|
|
352
299
|
private func accessorClosure(
|
|
353
|
-
_ descriptorName: String, _ key: String, receiver: Receiver,
|
|
300
|
+
_ descriptorName: String, _ key: String, receiver: Receiver, body: String
|
|
354
301
|
) -> String {
|
|
355
|
-
let captures = receiver.captureClause
|
|
356
|
-
// Indent each line of a (possibly multi-line) body to sit one level inside the closure
|
|
357
|
-
//
|
|
302
|
+
let captures = receiver.captureClause
|
|
303
|
+
// Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
|
|
304
|
+
// `\(body)` interpolation would only indent the first line.
|
|
358
305
|
let indentedBody = body
|
|
359
306
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
360
307
|
.map { " \($0)" }
|
|
@@ -365,16 +312,6 @@ internal struct JSProperty {
|
|
|
365
312
|
// `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
|
|
366
313
|
// a shared object unwraps it in the body.
|
|
367
314
|
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
368
|
-
if usesAppContext {
|
|
369
|
-
return """
|
|
370
|
-
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
371
|
-
guard let appContext else {
|
|
372
|
-
throw Exceptions.AppContextLost()
|
|
373
|
-
}
|
|
374
|
-
\(indentedBody)
|
|
375
|
-
}
|
|
376
|
-
"""
|
|
377
|
-
}
|
|
378
315
|
return """
|
|
379
316
|
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
380
317
|
\(indentedBody)
|
|
@@ -411,10 +348,11 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
411
348
|
|
|
412
349
|
/// The shared-object counterpart of `_decorateModule`. Core supplies the class `prototype`; this binds
|
|
413
350
|
/// 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
|
|
415
|
-
///
|
|
416
|
-
/// rather than capturing a singleton `self`.
|
|
417
|
-
///
|
|
351
|
+
/// distinct native instance behind each JS object, the bindings recover the typed receiver from the JS
|
|
352
|
+
/// `this` per call (`try SharedObject.native(from: this.asObject(in: runtime), as: <Type>.self)`)
|
|
353
|
+
/// rather than capturing a singleton `self`. Overrides the base `SharedObject` class method so core can
|
|
354
|
+
/// dispatch to it through the concrete type's metatype. The first parameter is `prototype` (not `object`
|
|
355
|
+
/// as on `_decorateModule`) because it's the shared class prototype, not an instance. The constructor is
|
|
418
356
|
/// bound separately (see `JSConstructor.buildConstructor`). Only emitted when the type has at least one
|
|
419
357
|
/// `@JS func`/`var`.
|
|
420
358
|
internal func buildDecorateSharedObject(
|
|
@@ -423,7 +361,7 @@ internal func buildDecorateSharedObject(
|
|
|
423
361
|
let body = decorateBody(functions: functions, properties: properties, receiver: .sharedObject(typeName: typeName))
|
|
424
362
|
return """
|
|
425
363
|
@JavaScriptActor
|
|
426
|
-
public
|
|
364
|
+
public override class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
427
365
|
\(raw: body)
|
|
428
366
|
}
|
|
429
367
|
"""
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
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
|
|
5
|
-
///
|
|
6
|
-
/// unlike the method/property bindings it produces the native instance rather than
|
|
4
|
+
/// have a single constructor). Instead of a `Constructor { … }` DSL entry, the macro synthesizes an
|
|
5
|
+
/// override of `SharedObject._constructSharedObject(...)` that decodes the JS arguments and returns a
|
|
6
|
+
/// fresh instance; unlike the method/property bindings it produces the native instance rather than
|
|
7
|
+
/// recovering one.
|
|
7
8
|
internal struct JSConstructor {
|
|
8
9
|
let parameters: [FunctionParameterSyntax]
|
|
9
10
|
|
|
@@ -11,8 +12,9 @@ internal struct JSConstructor {
|
|
|
11
12
|
self.parameters = Array(initDecl.signature.parameterClause.parameters)
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/// The body statements, indented with `indent`: arity guard, per-argument decode
|
|
15
|
-
///
|
|
15
|
+
/// The body statements, indented with `indent`: arity guard, per-argument decode through
|
|
16
|
+
/// `JavaScriptDecodable.decode` on a zero-copy `arguments.unownedValue(at:)` (the arity guard proves
|
|
17
|
+
/// each index is in bounds), then `return <Type>(label: arg0, …)`.
|
|
16
18
|
private func bodyStatements(typeName: String, indent: String) -> String {
|
|
17
19
|
var lines: [String] = []
|
|
18
20
|
|
|
@@ -25,15 +27,8 @@ internal struct JSConstructor {
|
|
|
25
27
|
|
|
26
28
|
var callArguments: [String] = []
|
|
27
29
|
for (index, parameter) in parameters.enumerated() {
|
|
28
|
-
let
|
|
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
|
-
}
|
|
30
|
+
let exprType = expressionType(parameter.type.trimmedDescription)
|
|
31
|
+
lines.append("let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)")
|
|
37
32
|
|
|
38
33
|
let label = parameter.firstName.text
|
|
39
34
|
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
@@ -47,12 +42,14 @@ internal struct JSConstructor {
|
|
|
47
42
|
.joined(separator: "\n")
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
/// The
|
|
51
|
-
/// arguments
|
|
45
|
+
/// The `_constructSharedObject` entry point the runtime calls to build an instance from JS
|
|
46
|
+
/// arguments. Overrides the base `SharedObject` class method so core can dispatch to it through the
|
|
47
|
+
/// concrete type's metatype; the body returns the concrete instance, which promotes to the base
|
|
48
|
+
/// `SharedObject?` return type. `this`/`appContext` may go unreferenced, which is harmless.
|
|
52
49
|
func buildConstructor(typeName: String) -> DeclSyntax {
|
|
53
50
|
return """
|
|
54
51
|
@JavaScriptActor
|
|
55
|
-
public
|
|
52
|
+
public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime, appContext: AppContext) throws -> SharedObject? {
|
|
56
53
|
\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
57
54
|
}
|
|
58
55
|
"""
|
|
@@ -6,11 +6,13 @@ import SwiftSyntaxMacros
|
|
|
6
6
|
/// corresponding `Function` / `AsyncFunction` / `Property` / `Constructor` registrations; that part
|
|
7
7
|
/// of the expansion lives in those macros.
|
|
8
8
|
///
|
|
9
|
-
/// On its own, `@JS` emits one thing: a never-called peer that asserts
|
|
10
|
-
/// boundary is
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
///
|
|
9
|
+
/// On its own, `@JS` emits one thing: a never-called peer that asserts each type crossing the JS
|
|
10
|
+
/// boundary is convertible in the direction it travels — arguments are `JavaScriptDecodable`, return
|
|
11
|
+
/// values are `JavaScriptEncodable` (a settable property's value type is both). Because it's a
|
|
12
|
+
/// **peer** of the marked member, a non-conforming type produces a compile error located on the
|
|
13
|
+
/// user's own `@JS` declaration rather than on the enclosing `@ExpoModule`. The assertion mechanism
|
|
14
|
+
/// itself is shared (see `directionalConformanceAssertion`); `@JS` only supplies the boundary types,
|
|
15
|
+
/// split by direction, that it reads off the declaration.
|
|
14
16
|
///
|
|
15
17
|
/// Usage:
|
|
16
18
|
///
|
|
@@ -29,8 +31,10 @@ public struct JSMacro: PeerMacro {
|
|
|
29
31
|
in context: some MacroExpansionContext
|
|
30
32
|
) throws -> [DeclSyntax] {
|
|
31
33
|
guard let member = boundaryMember(of: declaration),
|
|
32
|
-
let assertion =
|
|
33
|
-
|
|
34
|
+
let assertion = directionalConformanceAssertion(
|
|
35
|
+
name: member.name,
|
|
36
|
+
decodableTypes: member.decodableTypes,
|
|
37
|
+
encodableType: member.encodableType,
|
|
34
38
|
isStatic: member.isStatic
|
|
35
39
|
) else {
|
|
36
40
|
return []
|
|
@@ -39,37 +43,54 @@ public struct JSMacro: PeerMacro {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
/// What an assertion peer needs about the `@JS` member it sits beside: a name (to keep the peer
|
|
43
|
-
///
|
|
46
|
+
/// What an assertion peer needs about the `@JS` member it sits beside: a name (to keep the peer unique
|
|
47
|
+
/// among siblings), the boundary types split by conversion direction, and whether the member is
|
|
48
|
+
/// type-level. Arguments (and a settable property's incoming value) are decoded; return values (and a
|
|
49
|
+
/// property's outgoing value) are encoded, so each is asserted against the protocol for its direction.
|
|
44
50
|
private struct BoundaryMember {
|
|
45
51
|
let name: String
|
|
46
|
-
///
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
/// Types decoded from JS: function/constructor arguments, and a settable property's value type.
|
|
53
|
+
let decodableTypes: [String]
|
|
54
|
+
/// The single type encoded to JS: a function's return type, or a property's value type on read;
|
|
55
|
+
/// `nil` when the member produces nothing JS-visible (a `Void` function).
|
|
56
|
+
let encodableType: String?
|
|
49
57
|
/// True for `static`/`class` members, so the peer is emitted in the same metatype context.
|
|
50
58
|
let isStatic: Bool
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
/// Reads the boundary member off a `@JS` declaration
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
61
|
+
/// Reads the boundary member off a `@JS` declaration, splitting its types by conversion direction. A
|
|
62
|
+
/// function contributes its parameter types (decodable) and its return type when non-Void (encodable);
|
|
63
|
+
/// a property contributes its value type as encodable (the getter) and also as decodable when settable
|
|
64
|
+
/// (the setter). Composed types (`[Int]`, `String?`, …) are kept verbatim, their conditional
|
|
65
|
+
/// conformances transitively constraining the elements. Returns `nil` for declaration kinds `@JS`
|
|
66
|
+
/// doesn't read types from, or a property whose type isn't spelled out (a syntactic macro can't
|
|
67
|
+
/// recover it).
|
|
57
68
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
58
69
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
let decodableTypes = funcDecl.signature.parameterClause.parameters.map { $0.type.trimmedDescription }
|
|
71
|
+
let returnType = funcDecl.signature.returnClause?.type
|
|
72
|
+
let encodableType = returnType.flatMap { isVoidType($0) ? nil : $0.trimmedDescription }
|
|
73
|
+
return BoundaryMember(
|
|
74
|
+
name: funcDecl.name.text,
|
|
75
|
+
decodableTypes: decodableTypes,
|
|
76
|
+
encodableType: encodableType,
|
|
77
|
+
isStatic: isTypeLevel(funcDecl.modifiers)
|
|
78
|
+
)
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
67
82
|
let binding = varDecl.bindings.first,
|
|
68
83
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
69
84
|
let type = binding.typeAnnotation?.type {
|
|
85
|
+
// A `let`, or a `var` with no setter, is read-only (encodable only). A settable `var` is also
|
|
86
|
+
// decoded on write, so its value type is asserted in both directions.
|
|
87
|
+
let typeText = type.trimmedDescription
|
|
88
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
89
|
+
let isSettable = isVar && bindingIsSettable(binding)
|
|
70
90
|
return BoundaryMember(
|
|
71
91
|
name: identifier.identifier.text,
|
|
72
|
-
|
|
92
|
+
decodableTypes: isSettable ? [typeText] : [],
|
|
93
|
+
encodableType: typeText,
|
|
73
94
|
isStatic: isTypeLevel(varDecl.modifiers)
|
|
74
95
|
)
|
|
75
96
|
}
|
|
@@ -246,10 +246,11 @@ extension AttributeListSyntax {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
/// A type spelled so it's valid in expression position (before `.
|
|
250
|
-
/// Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
251
|
-
/// `!` is rewritten to `?` (`T!` and `T?` are both
|
|
252
|
-
/// treats identically). Other type spellings pass through
|
|
249
|
+
/// A type spelled so it's valid in expression position (before `.decode`/`.encode`, before
|
|
250
|
+
/// `.getDynamicType()`, or after `as!`). Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
251
|
+
/// type-annotation position, so a trailing `!` is rewritten to `?` (`T!` and `T?` are both
|
|
252
|
+
/// `Optional<T>`, which the conversion layer treats identically). Other type spellings pass through
|
|
253
|
+
/// unchanged.
|
|
253
254
|
internal func expressionType(_ type: String) -> String {
|
|
254
255
|
guard type.hasSuffix("!") else {
|
|
255
256
|
return type
|
|
@@ -292,7 +293,7 @@ internal func collectProperties(
|
|
|
292
293
|
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
293
294
|
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
294
295
|
/// observers imply stored storage, which is also settable.
|
|
295
|
-
|
|
296
|
+
internal func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
296
297
|
guard let accessorBlock = binding.accessorBlock else {
|
|
297
298
|
return true
|
|
298
299
|
}
|
|
@@ -310,22 +311,3 @@ private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
|
310
311
|
return false
|
|
311
312
|
}
|
|
312
313
|
}
|
|
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
|
-
}
|
|
@@ -45,14 +45,13 @@ internal enum Receiver {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
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.
|
|
49
|
-
|
|
50
|
-
func captureClause(usesAppContext: Bool) -> String {
|
|
48
|
+
/// captures `self` strong; a shared object captures nothing of the instance.
|
|
49
|
+
var captureClause: String {
|
|
51
50
|
switch self {
|
|
52
51
|
case .module:
|
|
53
|
-
return
|
|
52
|
+
return "[self] "
|
|
54
53
|
case .sharedObject:
|
|
55
|
-
return
|
|
54
|
+
return ""
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
}
|