@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.
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
@@ -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: 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).
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 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)"
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: 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.
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
- 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)"]
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(usesAppContext: usesAppContext)
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 reuse the same static-type fast path as
267
- /// functions (primitives through a direct typed accessor / `toJavaScriptValue`, other types through
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; `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.
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, 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)"
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 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).
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 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
- }
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, usesAppContext: usesAppContext,
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 (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.
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, usesAppContext: Bool, body: String
300
+ _ descriptorName: String, _ key: String, receiver: Receiver, body: String
354
301
  ) -> 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.
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 are **static** and recover the typed
415
- /// receiver from the JS `this` per call (`try SharedObject.native(from: this.asObject(in: runtime), as: <Type>.self)`)
416
- /// rather than capturing a singleton `self`. The first parameter is `prototype` (not `object` as on
417
- /// `_decorateModule`) because it's the shared class prototype, not an instance. The constructor is
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 static func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
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 a
5
- /// static `_constructSharedObject(...)` that decodes the JS arguments and returns a fresh instance;
6
- /// unlike the method/property bindings it produces the native instance rather than recovering one.
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 (primitives via a
15
- /// 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, …)`.
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 type = parameter.type.trimmedDescription
29
-
30
- if let accessor = fastDecodeAccessor(for: type) {
31
- lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
32
- } else {
33
- let exprType = expressionType(type)
34
- lines.append(
35
- "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)")
36
- }
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 static `_constructSharedObject` entry point the runtime calls to build an instance from JS
51
- /// arguments, returning the concrete type. `this`/`appContext` may go unreferenced, which is harmless.
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 static func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime, appContext: AppContext) throws -> \(raw: typeName) {
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 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
  }