@expo/expo-modules-macros-plugin 0.7.0 → 0.8.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.
Binary file
@@ -4,12 +4,15 @@ import SwiftSyntax
4
4
  /// `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call, the enclosing
5
5
  /// macro synthesizes a decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`) that binds each such
6
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:
7
+ /// decode-call-encode body inlined into the closure. This omits the dynamic-call path entirely:
8
8
  /// every argument is decoded individually by its static type.
9
9
  ///
10
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).
11
+ /// unwrapped from the JS `this` for a shared-object binding. An `async` `@JS func` binds through the
12
+ /// two-phase async `setProperty(_:)` overload: the closure is the synchronous decode phase (receiver
13
+ /// unwrap, arity guard, argument decode, all while `this` and the arguments are still valid)
14
+ /// and returns the async body (call and result encode), so nothing JSI-owned crosses the asynchronous
15
+ /// boundary and JS gets a promise.
13
16
  internal struct JSFunction {
14
17
  let swiftName: String
15
18
  let jsName: String
@@ -60,7 +63,7 @@ internal struct JSFunction {
60
63
  let maximum = parameters.count
61
64
  var lines: [String] = []
62
65
 
63
- if let unwrap = receiver.unwrapStatement(isAsync: isAsync) {
66
+ if let unwrap = receiver.unwrapStatement {
64
67
  lines.append(unwrap)
65
68
  }
66
69
 
@@ -87,8 +90,31 @@ internal struct JSFunction {
87
90
  }
88
91
 
89
92
  if required == maximum {
90
- // No omittable trailing run: a single flat call with every argument decoded.
91
- lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
93
+ // No omittable trailing run: a single flat call with every argument decoded. An async
94
+ // function decodes any remaining slots here (still the synchronous phase) and returns its
95
+ // body; a sync one calls and encodes directly.
96
+ if isAsync {
97
+ for index in required..<maximum {
98
+ lines.append(decodeStatement(at: index))
99
+ }
100
+ lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: maximum))
101
+ } else {
102
+ lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
103
+ }
104
+ } else if isAsync {
105
+ // One body per accepted arity, branching on `arguments.count`. A branch decodes its trailing
106
+ // slots synchronously and returns the async body for that call shape, so each `return` ends
107
+ // the decode phase for its arity.
108
+ lines.append("switch arguments.count {")
109
+ for arity in required...maximum {
110
+ let label = arity == maximum ? "default:" : "case \(arity):"
111
+ lines.append(label)
112
+ for index in required..<arity {
113
+ lines.append(" " + decodeStatement(at: index))
114
+ }
115
+ lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: arity).map { " " + $0 })
116
+ }
117
+ lines.append("}")
92
118
  } else {
93
119
  // One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
94
120
  // trailing slot before the call, so this is a `switch` statement (not an expression): a
@@ -116,6 +142,24 @@ internal struct JSFunction {
116
142
  .joined(separator: "\n")
117
143
  }
118
144
 
145
+ /// The `return { … }` statement closing an async binding's decode phase: the returned closure is
146
+ /// the function's async body (`AsyncFunctionBody`), awaiting the call and encoding the result. It
147
+ /// captures only what the call needs — the decoded `arg<i>` locals, the receiver, and `runtime`
148
+ /// for the encode — never `this` or the arguments buffer, which are only valid for the duration
149
+ /// of the host call.
150
+ private func asyncBodyLines(receiver: Receiver, arity: Int) -> [String] {
151
+ var lines: [String] = ["return {"]
152
+ if returnType != nil {
153
+ lines.append(" let result = \(callExpression(receiver: receiver, arity: arity))")
154
+ lines.append(contentsOf: encodeResultLines().map { " " + $0 })
155
+ } else {
156
+ lines.append(" \(callExpression(receiver: receiver, arity: arity))")
157
+ lines.append(" return .undefined")
158
+ }
159
+ lines.append("}")
160
+ return lines
161
+ }
162
+
119
163
  /// `let arg<index> = …` decoding the slot at `index` by its static type through
120
164
  /// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
121
165
  /// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
@@ -202,28 +246,25 @@ internal struct JSFunction {
202
246
  /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
203
247
  /// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
204
248
  /// (which creates the host function under the hood) — no separate named binding. For an `async`
205
- /// function the body `await`s the call, which selects the async `setProperty` overload (so JS
206
- /// receives a promise).
249
+ /// function the closure is the synchronous decode phase and returns the async body, which selects
250
+ /// the async `setProperty` overload (so JS receives a promise).
207
251
  ///
208
- /// Capture mirrors core's `SyncFunctionDefinition.build`: a module captures its `self` **strong** —
252
+ /// A module captures its `self` **strong** —
209
253
  /// the host-function closure is what keeps the native callable alive for as long as JS can invoke
210
254
  /// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
211
255
  /// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
212
256
  func decorateStatements(object: String, receiver: Receiver) -> String {
213
- // Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
214
- // `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
215
- // `JavaScriptValue` and forming its `weak`-runtime reference on every call. A module ignores
216
- // `this`; a shared object unwraps it (still borrowed). The first parameter is typed `borrowing
217
- // JavaScriptUnownedValue` to select that (otherwise `@_disfavoredOverload`) overload which
218
- // requires the *parenthesized, fully typed* parameter list, since Swift rejects a type annotation
219
- // on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
220
- // shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
221
- // escapes into the task anyway.
257
+ // Every `@JS` binding hands `this` in as a borrowed `JavaScriptUnownedValue` instead of
258
+ // allocating an owning `JavaScriptValue` and forming its `weak`-runtime reference on every call,
259
+ // and consumes the arguments buffer sync and async closures share one parameter shape and
260
+ // differ only in what they return. A module ignores `this`; a shared object unwraps it (still
261
+ // borrowed) an async binding does so in its synchronous decode phase, before the borrow ends.
262
+ // The parameter list is parenthesized and fully typed, since Swift rejects a type annotation on
263
+ // a shorthand `{ [capture] name, name in }` parameter; for a sync binding the explicit
264
+ // `JavaScriptUnownedValue` also selects the (otherwise `@_disfavoredOverload`) unowned-`this`
265
+ // overload.
222
266
  let captures = receiver.captureClause
223
- let parameters =
224
- isAsync
225
- ? "this, arguments"
226
- : "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
267
+ let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
227
268
 
228
269
  return """
229
270
  \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
@@ -237,7 +278,7 @@ internal struct JSFunction {
237
278
  /// `Property(...)` DSL entry, the enclosing macro synthesizes a get/set accessor into the JS object
238
279
  /// inside its decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`): it builds a descriptor object
239
280
  /// (`enumerable` + `get`, and `set` when the property is settable) and installs it with
240
- /// `object.defineProperty(name, descriptor:)`, mirroring core's `PropertyDefinition.buildDescriptor`.
281
+ /// `object.defineProperty(name, descriptor:)`.
241
282
  /// The `get`/`set` host functions are installed the same way `@JS func`s are — the closure-taking
242
283
  /// `setProperty(_:)` overload, with the read/write body inlined into the closure.
243
284
  ///
@@ -268,7 +309,7 @@ internal struct JSProperty {
268
309
  // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
269
310
  // reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
270
311
  // they take the borrowed unowned `this`.
271
- let unwrap = receiver.unwrapStatement(isAsync: false).map { "\($0)\n" } ?? ""
312
+ let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
272
313
  var lines: [String] = []
273
314
 
274
315
  lines.append("let \(descriptorName) = runtime.createObject()")
@@ -348,9 +389,9 @@ private func decorateBody(
348
389
  /// The module decorator: a `_decorateModule(object:)` that binds every `@JS func` (via an inlined
349
390
  /// `setProperty` closure) and every `@JS var` (via a `defineProperty` accessor) into the module's own
350
391
  /// JS object. Core supplies the object; the bindings call into the module `self`. It's an *instance*
351
- /// method (a module is a singleton, satisfying the `AnyModule` requirement of the same name), mirroring
352
- /// core's `ObjectDefinition.decorate(object:)` including the `borrowing` object parameter (it mutates
353
- /// through the reference without reassigning or taking ownership). Only emitted when there's at least
392
+ /// method (a module is a singleton, satisfying the `AnyModule` requirement of the same name), with a
393
+ /// `borrowing` object parameter (it mutates through the reference without reassigning or taking
394
+ /// ownership). Only emitted when there's at least
354
395
  /// one member to bind. Uses the shared body generation with the module `object:` phase and `self`
355
396
  /// receiver; the shared-object counterpart is `buildDecorateSharedObjectPhase`.
356
397
  internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
@@ -39,16 +39,15 @@ internal enum Receiver {
39
39
  /// `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign object or a type
40
40
  /// mismatch.
41
41
  ///
42
- /// The `this` object comes from the borrowed `JavaScriptUnownedValue` in a sync binding (`asObject(in:)`)
43
- /// and from the owning `JavaScriptValue` in an async one (`asObject()`). An async binding must take an
44
- /// owning `this` because a borrowed unowned value can't survive the closure's suspension points.
45
- func unwrapStatement(isAsync: Bool) -> String? {
42
+ /// The `this` object always comes from the borrowed `JavaScriptUnownedValue` (`asObject(in:)`): an
43
+ /// async binding unwraps in its synchronous decode phase, before the borrowed value's lifetime ends,
44
+ /// and only the recovered native instance crosses into the async body.
45
+ var unwrapStatement: String? {
46
46
  switch self {
47
47
  case .module, .staticMember:
48
48
  return nil
49
49
  case .sharedObject(let typeName):
50
- let thisObject = isAsync ? "this.asObject()" : "this.asObject(in: runtime)"
51
- return "let _self = try SharedObject.native(from: \(thisObject), as: \(typeName).self)"
50
+ return "let _self = try SharedObject.native(from: this.asObject(in: runtime), as: \(typeName).self)"
52
51
  }
53
52
  }
54
53
 
@@ -67,7 +66,7 @@ internal enum Receiver {
67
66
  /// Which JS object a set of bindings is installed on: the second orthogonal axis alongside `Receiver`.
68
67
  /// It selects the decorator entry point's argument label and the local name the body binds members onto,
69
68
  /// mirroring JS class semantics (a class has a constructor function whose `.prototype` carries instance
70
- /// members) and core's `ClassDefinition.decorate`.
69
+ /// members).
71
70
  /// The raw value is the argument label of the decorator entry point, which is also the local name the
72
71
  /// body binds members onto.
73
72
  internal enum Phase: String {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/expo-modules-macros-plugin",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "license": "MIT",
6
6
  "author": "650 Industries, Inc.",