expo-modules-macros 0.23.1 → 0.25.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
@@ -141,7 +141,17 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
141
141
  let parameters = closure.parameters.indices.map { "p\($0)" }
142
142
  let header = parameters.isEmpty ? "{ @Sendable in" : "{ @Sendable \(parameters.joined(separator: ", ")) in"
143
143
 
144
+ // A sync closure with parameters passes them as one value, a tuple when there are several, and an
145
+ // `emplacing` closure moves each one into the arguments buffer at its index. Capturing the
146
+ // parameters instead would send non-`Sendable` values to the `@JavaScriptActor` closure, which
147
+ // Swift 6 rejects. The closure captures nothing, so it needs no allocation. Without parameters
148
+ // there is nothing to emplace, and `invokeAsync` takes the parameters variadically.
149
+ let emplaces = !closure.isAsync && !parameters.isEmpty
144
150
  var arguments = parameters
151
+ if emplaces {
152
+ let values = parameters.count == 1 ? "p0" : "(" + parameters.joined(separator: ", ") + ")"
153
+ arguments = [values, "argumentCount: \(parameters.count)"]
154
+ }
145
155
  if let returnType = closure.returnType {
146
156
  arguments.append("returning: \(expressionType(returnType)).self")
147
157
  }
@@ -156,6 +166,14 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
156
166
  call = "\(callback).invokeDetached(\(argumentList))"
157
167
  }
158
168
 
169
+ if emplaces {
170
+ var lines = [header, " \(call) { _values, _arguments, _runtime in"]
171
+ for index in parameters.indices {
172
+ let value = parameters.count == 1 ? "_values" : "_values.\(index)"
173
+ lines.append(" try _arguments.emplace(\(value), at: \(index), in: _runtime)")
174
+ }
175
+ return lines + [" }", "}"]
176
+ }
159
177
  guard closure.isAsync && !closure.isThrowing else {
160
178
  return [header, " " + call, "}"]
161
179
  }
@@ -152,9 +152,9 @@ internal struct JSFunction {
152
152
  } else {
153
153
  // One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
154
154
  // trailing slot before the call, so this is a `switch` statement (not an expression): a
155
- // value-returning function declares `result` up front and each branch assigns it.
155
+ // value-returning function declares `value` up front and each branch assigns it.
156
156
  if let returnType {
157
- lines.append("let result: \(expressionType(returnType))")
157
+ lines.append("let value: \(expressionType(returnType))")
158
158
  }
159
159
  lines.append("switch arguments.count {")
160
160
  for arity in required...maximum {
@@ -163,7 +163,7 @@ internal struct JSFunction {
163
163
  for index in required..<arity {
164
164
  lines.append(contentsOf: decodeLines(at: index).map { " " + $0 })
165
165
  }
166
- let assignment = returnType != nil ? "result = " : ""
166
+ let assignment = returnType != nil ? "value = " : ""
167
167
  lines.append(" \(assignment)\(callExpression(receiver: receiver, arity: arity))")
168
168
  }
169
169
  lines.append("}")
@@ -268,7 +268,7 @@ internal struct JSFunction {
268
268
  }
269
269
 
270
270
  /// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
271
- /// `let result = <callee>.f(...)` then the return encode (or just `<callee>.f(...)` for a function
271
+ /// `let value = <callee>.f(...)` then the encode (or just `<callee>.f(...)` for a function
272
272
  /// that returns nothing).
273
273
  private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
274
274
  var lines: [String] = []
@@ -276,7 +276,7 @@ internal struct JSFunction {
276
276
  lines.append(contentsOf: decodeLines(at: index))
277
277
  }
278
278
  if returnType != nil {
279
- lines.append("let result = \(callExpression(receiver: receiver, arity: arity))")
279
+ lines.append("let value = \(callExpression(receiver: receiver, arity: arity))")
280
280
  lines.append(contentsOf: encodeResultLines())
281
281
  } else {
282
282
  lines.append(callExpression(receiver: receiver, arity: arity))
@@ -290,8 +290,9 @@ internal struct JSFunction {
290
290
  /// `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
291
291
  /// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
292
292
  /// catchable error is the right behavior, and matches how non-primitive integers already encode. A
293
- /// no-return function has nothing to encode: its binding closure returns `Void` (see
294
- /// `decorateStatements(object:receiver:)`).
293
+ /// no-return function has nothing to encode, so its binding closure leaves the result slot empty (see
294
+ /// `decorateStatements(object:receiver:)`). A synchronous function encodes straight into the
295
+ /// engine's result slot, so a number, a boolean or a string never becomes a `JavaScriptValue`.
295
296
  ///
296
297
  /// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
297
298
  /// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
@@ -303,15 +304,15 @@ internal struct JSFunction {
303
304
  guard let returnType else {
304
305
  return []
305
306
  }
306
- let encode = "try \(encodeCall(returnType, freeForm: returnFreeForm, of: "result"))"
307
307
  if isAsync {
308
+ let encode = "try \(encodeCall(returnType, freeForm: returnFreeForm, of: "result"))"
308
309
  return [
309
310
  "return try await runtime.execute {",
310
311
  " return \(encode)",
311
312
  "}",
312
313
  ]
313
314
  }
314
- return ["return \(encode)"]
315
+ return [encodeIntoSlotStatement(returnType, freeForm: returnFreeForm, of: "value")]
315
316
  }
316
317
 
317
318
  /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
@@ -331,19 +332,18 @@ internal struct JSFunction {
331
332
  // differ only in what they return. A module ignores `this`; a shared object unwraps it (still
332
333
  // borrowed) — an async binding does so in its synchronous decode phase, before the borrow ends.
333
334
  // The parameter list is parenthesized and fully typed, since Swift rejects a type annotation on
334
- // a shorthand `{ [capture] name, name in }` parameter; for a sync binding the explicit
335
- // `JavaScriptUnownedValue` also selects the (otherwise `@_disfavoredOverload`) unowned-`this`
336
- // overload.
337
- // A sync function that returns nothing binds through the `Void` overload, which leaves JS's
338
- // result as `undefined` without building and writing a value. The closure declares `-> Void`
339
- // because a body of several statements without a `return` is otherwise ambiguous between the
340
- // two unowned-`this` overloads.
335
+ // a shorthand `{ [capture] name, name in }` parameter.
336
+ // A sync function binds through the result-slot overload (`JavaScriptRuntime.HostFunctionClosure`):
337
+ // it encodes its result into the slot and gets the runtime as a parameter. A function that returns
338
+ // nothing writes nothing, so JS gets `undefined` without a value being built.
341
339
  let captures = receiver.captureClause
342
- let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
343
- let returnClause = returnType == nil && !isAsync ? " -> Void" : ""
340
+ let parameters =
341
+ isAsync
342
+ ? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
343
+ : resultSlotClosureParameters
344
344
 
345
345
  return """
346
- \(object).setProperty("\(jsName)") { \(captures)\(parameters)\(returnClause) in
346
+ \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
347
347
  \(bodyStatements(receiver: receiver, indent: " "))
348
348
  }
349
349
  """
@@ -419,8 +419,16 @@ internal struct JSProperty {
419
419
  } else {
420
420
  encode = "\(callee).\(swiftName).toJavaScriptValue(in: runtime)"
421
421
  }
422
+ // A getter encodes straight into the engine's result slot. A lazy getter needs the encoded value as
423
+ // a `JavaScriptValue` to define it on `this` as well, and then moves it into the slot.
422
424
  let getBody: String
423
- if isLazy {
425
+ if !isLazy {
426
+ if let valueType {
427
+ getBody = encodeIntoSlotStatement(valueType, freeForm: freeForm, of: "\(callee).\(swiftName)")
428
+ } else {
429
+ getBody = "result.emplace(\(encode))"
430
+ }
431
+ } else {
424
432
  let define = "this.getObject(in: runtime).defineProperty(\"\(jsName)\", value: value, options: [.enumerable])"
425
433
  // A module getter ignores `this` otherwise, so it can be anything, and only an object can hold the
426
434
  // value. A shared object's unwrap has already checked that `this` is an object.
@@ -428,9 +436,7 @@ internal struct JSProperty {
428
436
  receiver.unwrapStatement == nil
429
437
  ? "if this.isObject() {\n \(define)\n}"
430
438
  : define
431
- getBody = "let value = \(encode)\n\(defineStatement)\nreturn value"
432
- } else {
433
- getBody = "return \(encode)"
439
+ getBody = "let value = \(encode)\n\(defineStatement)\nresult.emplace(value)"
434
440
  }
435
441
  lines.append(
436
442
  accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getBody)")
@@ -444,13 +450,7 @@ internal struct JSProperty {
444
450
  let decode = decodeCall(valueType, freeForm: freeForm, from: "arguments.unownedValue(at: 0)")
445
451
  let setDecode = "\(callee).\(swiftName) = try \(decode)"
446
452
  lines.append(
447
- accessorClosure(
448
- descriptorName,
449
- "set",
450
- receiver: receiver,
451
- returnsVoid: true,
452
- body: "\(unwrap)\(setDecode)"
453
- )
453
+ accessorClosure(descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)")
454
454
  )
455
455
  }
456
456
 
@@ -465,13 +465,10 @@ internal struct JSProperty {
465
465
 
466
466
  /// One `descriptor.setProperty("get"/"set") { … }` accessor entry. The capture list follows the
467
467
  /// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
468
- /// A setter passes `returnsVoid`, so its closure declares `-> Void` and binds through the `Void`
469
- /// overload, like a function that returns nothing.
470
468
  private func accessorClosure(
471
469
  _ descriptorName: String,
472
470
  _ key: String,
473
471
  receiver: Receiver,
474
- returnsVoid: Bool = false,
475
472
  body: String
476
473
  ) -> String {
477
474
  let captures = receiver.captureClause
@@ -482,21 +479,36 @@ internal struct JSProperty {
482
479
  .split(separator: "\n", omittingEmptySubsequences: false)
483
480
  .map { " \($0)" }
484
481
  .joined(separator: "\n")
485
- // Property `get`/`set` accessors are always synchronous, so they bind through the unowned-`this`
482
+ // Property `get`/`set` accessors are always synchronous, so they bind through the result-slot
486
483
  // `setProperty` overload like sync functions. The parameter list is parenthesized and fully typed
487
- // because Swift rejects a type annotation on a shorthand closure parameter; the explicit
488
- // `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
489
- // a shared object unwraps it in the body.
490
- let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
491
- let returnClause = returnsVoid ? " -> Void" : ""
484
+ // because Swift rejects a type annotation on a shorthand closure parameter. A module ignores `this`;
485
+ // a shared object unwraps it in the body. A setter writes nothing, so JS gets `undefined`.
492
486
  return """
493
- \(descriptorName).setProperty("\(key)") { \(captures)\(parameters)\(returnClause) in
487
+ \(descriptorName).setProperty("\(key)") { \(captures)\(resultSlotClosureParameters) in
494
488
  \(indentedBody)
495
489
  }
496
490
  """
497
491
  }
498
492
  }
499
493
 
494
+ /// The parameter list of a binding closure that writes its result into the engine's result slot
495
+ /// (`JavaScriptRuntime.HostFunctionClosure`). The closure gets the runtime as a parameter, which shadows
496
+ /// the decorator's `runtime`, so it doesn't capture it.
497
+ private let resultSlotClosureParameters =
498
+ "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer, "
499
+ + "result: borrowing JavaScriptValue.Slot, runtime: borrowing JavaScriptRuntime)"
500
+
501
+ /// The statement that encodes `valueExpression` of a boundary type into the result slot `result`. A type
502
+ /// that mentions `Any` has no `encode(_:into:in:)`, so its free-form encode result is moved into the slot.
503
+ private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String)
504
+ -> String
505
+ {
506
+ if freeForm != nil {
507
+ return "result.emplace(try \(encodeCall(type, freeForm: freeForm, of: valueExpression)))"
508
+ }
509
+ return "try \(expressionType(type)).encode(\(valueExpression), into: result, in: runtime)"
510
+ }
511
+
500
512
  /// The body shared by every decorator phase: every `@JS func` bound via an inlined `setProperty`
501
513
  /// closure and every `@JS var` via a `defineProperty` accessor, joined for the function body. `object`
502
514
  /// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
@@ -477,7 +477,9 @@ private func toObjectMethod() -> DeclSyntax {
477
477
  /// property's declared default (inlined) when undefined; optional properties become `nil` when
478
478
  /// undefined/null. It's the unowned overload: an owning value decodes through `JavaScriptDecodable`'s
479
479
  /// default, which forwards to it. `encode` builds the JS object, writing each property. Its parameter
480
- /// names the type, because a class can't take `Self` as a parameter type.
480
+ /// names the type, because a class can't take `Self` as a parameter type. `encode(_:into:in:)` builds
481
+ /// the same object and moves it into the slot (an array element, an argument or a result), with no
482
+ /// `JavaScriptValue` in between.
481
483
  private func javaScriptCodableWitnesses(
482
484
  properties: [RecordProperty],
483
485
  typeName: String,
@@ -491,11 +493,12 @@ private func javaScriptCodableWitnesses(
491
493
  let lookupLines = cachesPropNames ? propNamesLookupLines() : []
492
494
  let readLines = lookupLines + jsObjectReadLines(properties: properties, cachesPropNames: cachesPropNames)
493
495
  let decodeBody = objectLine + "\n" + factoryBody(properties: properties, readLines: readLines)
494
- let encodeLines =
496
+ let objectLines =
495
497
  [" let object = runtime.createObject()"]
496
498
  + lookupLines
497
499
  + objectWriteLines(properties: properties, of: "value", cachesPropNames: cachesPropNames)
498
- + [" return object.takeValue()"]
500
+ let encodeLines = objectLines + [" return object.takeValue()"]
501
+ let encodeIntoSlotLines = objectLines + [" slot.emplace(object)"]
499
502
  return [
500
503
  """
501
504
  @JavaScriptActor
@@ -509,12 +512,23 @@ private func javaScriptCodableWitnesses(
509
512
  \(raw: encodeLines.joined(separator: "\n"))
510
513
  }
511
514
  """,
515
+ """
516
+ @JavaScriptActor
517
+ public static func encode(
518
+ _ value: \(raw: typeName),
519
+ into slot: borrowing JavaScriptValue.Slot,
520
+ in runtime: borrowing JavaScriptRuntime
521
+ ) throws {
522
+ \(raw: encodeIntoSlotLines.joined(separator: "\n"))
523
+ }
524
+ """,
512
525
  ]
513
526
  }
514
527
 
515
528
  /// The statements that write each property of `record` (`self` or the `encode` parameter) into the
516
- /// `object` local, encoding it with `JavaScriptEncodable.encode` or, for a free-form property, the
517
- /// matching `JavaScriptValue` free-form method.
529
+ /// `object` local. A property is encoded straight into the engine value that `setProperty` writes, through
530
+ /// `JavaScriptEncodable.encode(_:into:in:)`, so a number, a boolean or a string never becomes a
531
+ /// `JavaScriptValue`. A free-form property goes through the matching `JavaScriptValue` free-form method.
518
532
  private func objectWriteLines(properties: [RecordProperty], of record: String, cachesPropNames: Bool) -> [String] {
519
533
  return properties.enumerated().map { index, property in
520
534
  let value = "\(record).\(property.name)"
@@ -529,7 +543,8 @@ private func objectWriteLines(properties: [RecordProperty], of record: String, c
529
543
  // The explicit `as Any` keeps an optional property from warning about its implicit coercion.
530
544
  encode = "try JavaScriptValue.encodeAny(\(value) as Any, in: runtime)"
531
545
  case nil:
532
- encode = "try \(expressionType(property.type)).encode(\(value), in: runtime)"
546
+ let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
547
+ return " try object.setProperty(\(propName), encoding: \(value), in: runtime)"
533
548
  }
534
549
  let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
535
550
  return " object.setProperty(\(propName), value: \(encode))"
@@ -64,6 +64,7 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
64
64
  members.append(decodableKindsProperty(union: union))
65
65
  members.append(decodeMethod(union: union))
66
66
  members.append(encodeMethod(union: union))
67
+ members.append(encodeIntoSlotMethod(union: union))
67
68
  members.append(contentsOf: accessorMethods(union: union))
68
69
  members.append(payloadTypeNameProperty(union: union))
69
70
  return members
@@ -322,6 +323,30 @@ private func encodeMethod(union: UnionType) -> DeclSyntax {
322
323
  """
323
324
  }
324
325
 
326
+ /// `encode(_:into:in:)`: the same `switch` as `encode(_:in:)`, with each payload encoded straight into the
327
+ /// slot through the payload type's own `encode(_:into:in:)`. A payload that is a number, a boolean or a
328
+ /// string is then constructed in the slot (an array element, an argument or a result), with no
329
+ /// `JavaScriptValue` in between.
330
+ private func encodeIntoSlotMethod(union: UnionType) -> DeclSyntax {
331
+ var lines: [String] = [" switch value {"]
332
+ for unionCase in union.cases {
333
+ lines.append(" case \(unionCase.pattern):")
334
+ lines.append(" try \(expressionType(unionCase.payloadType)).encode(payload, into: slot, in: runtime)")
335
+ }
336
+ lines.append(" }")
337
+ let body = lines.joined(separator: "\n")
338
+ return """
339
+ @JavaScriptActor
340
+ public static func encode(
341
+ _ value: Self,
342
+ into slot: borrowing JavaScriptValue.Slot,
343
+ in runtime: borrowing JavaScriptRuntime
344
+ ) throws {
345
+ \(raw: body)
346
+ }
347
+ """
348
+ }
349
+
325
350
  /// `as(_:)`: a typed accessor per case, selected by the payload's metatype rather than the case name,
326
351
  /// so a caller that only knows the type it wants writes `try value.as(String.self)` and gets a `String`.
327
352
  /// When
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-macros",
3
- "version": "0.23.1",
3
+ "version": "0.25.0",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -26,9 +26,9 @@
26
26
  "typescript": "^5.4.0"
27
27
  },
28
28
  "optionalDependencies": {
29
- "@expo/modules-macros-linux-arm64": "0.23.1",
30
- "@expo/modules-macros-linux-x64": "0.23.1",
31
- "@expo/modules-macros-win32-arm64": "0.23.1",
32
- "@expo/modules-macros-win32-x64": "0.23.1"
29
+ "@expo/modules-macros-linux-arm64": "0.25.0",
30
+ "@expo/modules-macros-linux-x64": "0.25.0",
31
+ "@expo/modules-macros-win32-arm64": "0.25.0",
32
+ "@expo/modules-macros-win32-x64": "0.25.0"
33
33
  }
34
34
  }