@expo/expo-modules-macros-plugin 0.5.1 → 0.6.1

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
@@ -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 (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.
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
@@ -91,23 +90,21 @@ internal struct JSFunction {
91
90
  // No omittable trailing run: a single flat call with every argument decoded.
92
91
  lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
93
92
  } else {
94
- // One call shape per accepted arity. Each branch decodes only the trailing slots it has and
95
- // fills the rest (defaulted params drop their label so Swift applies the default; optional
96
- // params are passed `nil`). A value-returning function binds the result from a `switch`
97
- // expression and encodes once after it; a no-return one calls inline in a `switch` statement
98
- // and returns `.undefined`.
99
- if returnType != nil {
100
- lines.append("let result = switch arguments.count {")
101
- } else {
102
- lines.append("switch arguments.count {")
93
+ // One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
94
+ // trailing slot before the call, so this is a `switch` statement (not an expression): a
95
+ // value-returning function declares `result` up front and each branch assigns it.
96
+ if let returnType {
97
+ lines.append("let result: \(expressionType(returnType))")
103
98
  }
99
+ lines.append("switch arguments.count {")
104
100
  for arity in required...maximum {
105
101
  let label = arity == maximum ? "default:" : "case \(arity):"
106
102
  lines.append(label)
107
103
  for index in required..<arity {
108
104
  lines.append(" " + decodeStatement(at: index))
109
105
  }
110
- lines.append(" \(callExpression(receiver: receiver, arity: arity))")
106
+ let assignment = returnType != nil ? "result = " : ""
107
+ lines.append(" \(assignment)\(callExpression(receiver: receiver, arity: arity))")
111
108
  }
112
109
  lines.append("}")
113
110
  lines.append(contentsOf: encodeResultLines())
@@ -119,18 +116,14 @@ internal struct JSFunction {
119
116
  .joined(separator: "\n")
120
117
  }
121
118
 
122
- /// `let arg<index> = …` decoding the slot at `index` by its static type: a primitive through a
123
- /// direct typed accessor on a borrowed `JavaScriptUnownedValue` (no owning value, no `jsi::Value`
124
- /// copy, no `getDynamicType()` allocation, no `Any` boxing, no force-cast still validating and
125
- /// throwing `TypeError` on a mismatch), any other type through the dynamic converter (which needs
126
- /// an owning value, so it indexes the buffer directly).
119
+ /// `let arg<index> = …` decoding the slot at `index` by its static type through
120
+ /// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` no owning value, no
121
+ /// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
122
+ /// primitive's `decode` is `@inlinable` and lowers to the same direct accessor a hand-rolled fast
123
+ /// path would use.
127
124
  private func decodeStatement(at index: Int) -> String {
128
- let type = parameters[index].type.trimmedDescription
129
- if let accessor = fastDecodeAccessor(for: type) {
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)"
125
+ let exprType = expressionType(parameters[index].type.trimmedDescription)
126
+ return "let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)"
134
127
  }
135
128
 
136
129
  /// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
@@ -178,17 +171,17 @@ internal struct JSFunction {
178
171
  return lines
179
172
  }
180
173
 
181
- /// Encode the `result` local back to JS and return it: a primitive through `toJavaScriptValue(in:)`
182
- /// (the typed `JavaScriptRepresentable` conversion no `Any`, no dynamic-type allocation), any
183
- /// other type through the dynamic converter. A no-return function returns `.undefined` instead.
174
+ /// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode`, the same
175
+ /// for every type. Unlike the decode side there's no primitive fast path: `encode` produces the same
176
+ /// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
177
+ /// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
178
+ /// catchable error is the right behavior, and matches how non-primitive integers already encode. A
179
+ /// no-return function returns `.undefined` instead.
184
180
  private func encodeResultLines() -> [String] {
185
181
  guard let returnType else {
186
182
  return ["return .undefined"]
187
183
  }
188
- if fastDecodeAccessor(for: returnType) != nil {
189
- return ["return result.toJavaScriptValue(in: runtime)"]
190
- }
191
- return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
184
+ return ["return try \(expressionType(returnType)).encode(result, in: runtime)"]
192
185
  }
193
186
 
194
187
  /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
@@ -201,10 +194,6 @@ internal struct JSFunction {
201
194
  /// the host-function closure is what keeps the native callable alive for as long as JS can invoke
202
195
  /// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
203
196
  /// 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
197
  func decorateStatements(receiver: Receiver) -> String {
209
198
  // Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
210
199
  // `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
@@ -215,42 +204,19 @@ internal struct JSFunction {
215
204
  // on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
216
205
  // shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
217
206
  // escapes into the task anyway.
218
- let captures = receiver.captureClause(usesAppContext: usesAppContext)
207
+ let captures = receiver.captureClause
219
208
  let parameters =
220
209
  isAsync
221
210
  ? "this, arguments"
222
211
  : "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
223
212
 
224
213
  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
214
  return """
236
215
  \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
237
216
  \(bodyStatements(receiver: receiver, indent: " "))
238
217
  }
239
218
  """
240
219
  }
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
220
  }
255
221
 
256
222
  /// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
@@ -263,9 +229,8 @@ internal struct JSFunction {
263
229
  ///
264
230
  /// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
265
231
  /// 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).
232
+ /// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
233
+ /// `JavaScriptEncodable.encode`, the same uniform path as functions.
269
234
  internal struct JSProperty {
270
235
  let swiftName: String
271
236
  let jsName: String
@@ -282,18 +247,11 @@ internal struct JSProperty {
282
247
  /// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
283
248
  /// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
284
249
  /// 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.
250
+ /// instance. Getter and setter are gated independently.
289
251
  func decorateStatements(receiver: Receiver) -> String {
290
252
  let descriptorName = "\(swiftName)Descriptor"
291
253
  let callee = receiver.callee
292
254
  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
255
  // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
298
256
  // reads `self` directly. The unwrap leads each accessor body.
299
257
  let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
@@ -302,39 +260,28 @@ internal struct JSProperty {
302
260
  lines.append("let \(descriptorName) = runtime.createObject()")
303
261
  lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
304
262
 
305
- // Getter: read `<callee>.<name>` and encode the result back to JS.
263
+ // Getter: read `<callee>.<name>` and encode the result back to JS through `encode`. When the value
264
+ // type couldn't be inferred (no annotation and no literal default, rare for a stored var) there's
265
+ // no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
306
266
  let getEncode: String
307
- if let valueType, fastDecodeAccessor(for: valueType) != nil {
308
- getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
309
- } else if let valueType {
310
- getEncode =
311
- "return try \(expressionType(valueType)).getDynamicType().castToJS(\(callee).\(swiftName), appContext: appContext, in: runtime)"
267
+ if let valueType {
268
+ getEncode = "return try \(expressionType(valueType)).encode(\(callee).\(swiftName), in: runtime)"
312
269
  } 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
270
  getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
317
271
  }
318
272
  lines.append(
319
- accessorClosure(
320
- descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
273
+ accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)"))
321
274
 
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
324
- // settable var with neither an annotation nor a literal default is rare and can't be decoded).
275
+ // Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
276
+ // typed setter needs a known value type; when the type couldn't be inferred the property is bound
277
+ // getter-only (a settable var with neither an annotation nor a literal default is rare and can't
278
+ // be decoded).
325
279
  if isSettable, let valueType {
326
- let setDecode: String
327
- if let accessor = fastDecodeAccessor(for: valueType) {
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
- }
280
+ let exprType = expressionType(valueType)
281
+ let setDecode = "\(callee).\(swiftName) = try \(exprType).decode(arguments.unownedValue(at: 0), in: runtime)"
334
282
  lines.append(
335
283
  accessorClosure(
336
- descriptorName, "set", receiver: receiver, usesAppContext: usesAppContext,
337
- body: "\(unwrap)\(setDecode)\nreturn .undefined"))
284
+ descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
338
285
  }
339
286
 
340
287
  lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
@@ -346,15 +293,13 @@ internal struct JSProperty {
346
293
  }
347
294
 
348
295
  /// 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.
296
+ /// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
352
297
  private func accessorClosure(
353
- _ descriptorName: String, _ key: String, receiver: Receiver, usesAppContext: Bool, body: String
298
+ _ descriptorName: String, _ key: String, receiver: Receiver, body: String
354
299
  ) -> String {
355
- let captures = receiver.captureClause(usesAppContext: usesAppContext)
356
- // Indent each line of a (possibly multi-line) body to sit one level inside the closure, aligned
357
- // with the `guard`; a bare `\(body)` interpolation would only indent the first line.
300
+ let captures = receiver.captureClause
301
+ // Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
302
+ // `\(body)` interpolation would only indent the first line.
358
303
  let indentedBody = body
359
304
  .split(separator: "\n", omittingEmptySubsequences: false)
360
305
  .map { " \($0)" }
@@ -365,16 +310,6 @@ internal struct JSProperty {
365
310
  // `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
366
311
  // a shared object unwraps it in the body.
367
312
  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
313
  return """
379
314
  \(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
380
315
  \(indentedBody)
@@ -403,7 +338,7 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
403
338
  let body = decorateBody(functions: functions, properties: properties, receiver: .module)
404
339
  return """
405
340
  @JavaScriptActor
406
- public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
341
+ public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
407
342
  \(raw: body)
408
343
  }
409
344
  """
@@ -424,7 +359,7 @@ internal func buildDecorateSharedObject(
424
359
  let body = decorateBody(functions: functions, properties: properties, receiver: .sharedObject(typeName: typeName))
425
360
  return """
426
361
  @JavaScriptActor
427
- public override class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
362
+ public override class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
428
363
  \(raw: body)
429
364
  }
430
365
  """
@@ -12,8 +12,9 @@ internal struct JSConstructor {
12
12
  self.parameters = Array(initDecl.signature.parameterClause.parameters)
13
13
  }
14
14
 
15
- /// The body statements, indented with `indent`: arity guard, per-argument decode (primitives via a
16
- /// typed accessor, others via the dynamic converter), then `return <Type>(label: arg0, …)`.
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, …)`.
17
18
  private func bodyStatements(typeName: String, indent: String) -> String {
18
19
  var lines: [String] = []
19
20
 
@@ -26,15 +27,8 @@ internal struct JSConstructor {
26
27
 
27
28
  var callArguments: [String] = []
28
29
  for (index, parameter) in parameters.enumerated() {
29
- let type = parameter.type.trimmedDescription
30
-
31
- if let accessor = fastDecodeAccessor(for: type) {
32
- lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
33
- } else {
34
- let exprType = expressionType(type)
35
- lines.append(
36
- "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)")
37
- }
30
+ let exprType = expressionType(parameter.type.trimmedDescription)
31
+ lines.append("let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)")
38
32
 
39
33
  let label = parameter.firstName.text
40
34
  callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
@@ -48,14 +42,12 @@ internal struct JSConstructor {
48
42
  .joined(separator: "\n")
49
43
  }
50
44
 
51
- /// The `_constructSharedObject` entry point the runtime calls to build an instance from JS
52
- /// arguments. Overrides the base `SharedObject` class method so core can dispatch to it through the
53
- /// concrete type's metatype; the body returns the concrete instance, which promotes to the base
54
- /// `SharedObject?` return type. `this`/`appContext` may go unreferenced, which is harmless.
45
+ /// Builds an instance from the JS arguments. Overrides the base `SharedObject` class method; returns
46
+ /// the concrete instance, which promotes to the base `SharedObject?` return type.
55
47
  func buildConstructor(typeName: String) -> DeclSyntax {
56
48
  return """
57
49
  @JavaScriptActor
58
- public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime, appContext: AppContext) throws -> SharedObject? {
50
+ public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
59
51
  \(raw: bodyStatements(typeName: typeName, indent: " "))
60
52
  }
61
53
  """
@@ -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 every type crossing the JS
10
- /// boundary is JS-convertible. Because it's a **peer** of the marked member, a non-conforming type
11
- /// produces a compile error located on the user's own `@JS` declaration rather than on the enclosing
12
- /// `@ExpoModule`. The assertion mechanism itself is shared (see `typeConformanceAssertion`); `@JS`
13
- /// only supplies the boundary types it reads off the declaration.
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 = typeConformanceAssertion(
33
- for: ConformanceAssertion(name: member.name, types: member.types),
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
- /// unique among siblings), the types crossing the JS boundary, and whether the member is type-level.
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
- /// Boundary types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
47
- /// conditional conformances transitively constrain the elements.
48
- let types: [String]
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. A function contributes its parameter types
54
- /// plus the return type (when non-Void); a property contributes its declared type. Returns `nil` for
55
- /// declaration kinds `@JS` doesn't read types from, or a property whose type isn't spelled out (a
56
- /// syntactic macro can't recover it), so no assertion is emitted there.
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
- var types = funcDecl.signature.parameterClause.parameters.map { $0.type.trimmedDescription }
60
- if let returnType = funcDecl.signature.returnClause?.type, !isVoidType(returnType) {
61
- types.append(returnType.trimmedDescription)
62
- }
63
- return BoundaryMember(name: funcDecl.name.text, types: types, isStatic: isTypeLevel(funcDecl.modifiers))
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
- types: [type.trimmedDescription],
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 `.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.
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
- private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
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. `appContext`, when used,
49
- /// is captured weak in both cases.
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 usesAppContext ? "[weak appContext, self] " : "[self] "
52
+ return "[self] "
54
53
  case .sharedObject:
55
- return usesAppContext ? "[weak appContext] " : ""
54
+ return ""
56
55
  }
57
56
  }
58
57
  }