@expo/expo-modules-macros-plugin 0.3.0 → 0.5.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
@@ -8,7 +8,12 @@ import PackageDescription
8
8
  let package = Package(
9
9
  name: "ExpoModulesMacros",
10
10
  platforms: [.macOS(.v13)],
11
- products: [],
11
+ products: [
12
+ // The scanner CLI. Named `ExpoModulesScanner` (the user-facing tool name) while its target is
13
+ // `ExpoModulesScannerCLI`; the detection logic lives in the importable `ExpoModulesScanner`
14
+ // library that both the CLI and the tests depend on.
15
+ .executable(name: "ExpoModulesScanner", targets: ["ExpoModulesScannerCLI"]),
16
+ ],
12
17
  dependencies: [
13
18
  .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0-latest")
14
19
  ],
@@ -19,7 +24,18 @@ let package = Package(
19
24
  .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
20
25
  .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
21
26
  ]
22
- )
27
+ ),
28
+ .target(
29
+ name: "ExpoModulesScanner",
30
+ dependencies: [
31
+ .product(name: "SwiftSyntax", package: "swift-syntax"),
32
+ .product(name: "SwiftParser", package: "swift-syntax"),
33
+ ]
34
+ ),
35
+ .executableTarget(
36
+ name: "ExpoModulesScannerCLI",
37
+ dependencies: ["ExpoModulesScanner"]
38
+ ),
23
39
  ]
24
40
  )
25
41
 
@@ -36,4 +52,13 @@ if FileManager.default.fileExists(atPath: Context.packageDirectory + "/Tests") {
36
52
  ]
37
53
  )
38
54
  )
55
+ package.targets.append(
56
+ .testTarget(
57
+ name: "ExpoModulesScannerTests",
58
+ dependencies: [
59
+ "ExpoModulesScanner",
60
+ .product(name: "SwiftParser", package: "swift-syntax"),
61
+ ]
62
+ )
63
+ )
39
64
  }
@@ -1,17 +1,15 @@
1
1
  import SwiftSyntax
2
2
 
3
- /**
4
- A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
5
- `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call,
6
- `@ExpoModule` synthesizes a `_decorateModule` that binds each such function into the module's JS object
7
- via the closure-taking `JavaScriptObject.setProperty(_:)`, with the decode-call-encode body
8
- inlined into the closure. This omits the `[Any]`/`toTuple` dynamic-call path: every argument is
9
- decoded individually by its static type.
10
-
11
- The receiver is the module's real `self` (a module is a singleton instance), so the body calls
12
- `self.<name>(...)` directly and ignores the JS `this`. An `async` `@JS func` produces an `async`
13
- closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
14
- */
3
+ /// A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
4
+ /// `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call, the enclosing
5
+ /// macro synthesizes a decorator (`_decorateModule` / `_decorateSharedObject`) that binds each such
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:
8
+ /// every argument is decoded individually by its static type.
9
+ ///
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).
15
13
  internal struct JSFunction {
16
14
  let swiftName: String
17
15
  let jsName: String
@@ -50,19 +48,23 @@ internal struct JSFunction {
50
48
  }
51
49
 
52
50
  /// The decode-call-encode statements that form the host-function body, indented with the given
53
- /// prefix. An arity guard (an exact check when every parameter is required, otherwise a range
54
- /// check) throwing `Exceptions.ArgumentsRangeMismatch`; then the decode of the always-present
55
- /// required prefix (primitives via a direct typed accessor like `asDouble()` on a zero-copy
56
- /// `arguments.unownedValue(at:)`, others via `getDynamicType().cast(...)`); then the call and
57
- /// result encode (primitives via `toJavaScriptValue(in:)`, others via `castToJS(...)`). When a
58
- /// trailing run of parameters is omittable the call branches on `arguments.count`, decoding only
59
- /// the slots that branch actually has — `arguments[i]` traps past `count`, so a slot the caller
60
- /// didn't pass is never indexed.
61
- private func bodyStatements(indent: String) -> String {
51
+ /// prefix. Receiver unwrap (shared objects only), then an arity guard (an exact check when every
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.
59
+ private func bodyStatements(receiver: Receiver, indent: String) -> String {
62
60
  let required = requiredArgumentCount
63
61
  let maximum = parameters.count
64
62
  var lines: [String] = []
65
63
 
64
+ if let unwrap = receiver.unwrapStatement {
65
+ lines.append(unwrap)
66
+ }
67
+
66
68
  if required == maximum {
67
69
  lines.append(
68
70
  """
@@ -87,7 +89,7 @@ internal struct JSFunction {
87
89
 
88
90
  if required == maximum {
89
91
  // No omittable trailing run: a single flat call with every argument decoded.
90
- lines.append(contentsOf: callAndEncodeLines(arity: maximum, decodingFrom: required))
92
+ lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
91
93
  } else {
92
94
  // One call shape per accepted arity. Each branch decodes only the trailing slots it has and
93
95
  // fills the rest (defaulted params drop their label so Swift applies the default; optional
@@ -105,7 +107,7 @@ internal struct JSFunction {
105
107
  for index in required..<arity {
106
108
  lines.append(" " + decodeStatement(at: index))
107
109
  }
108
- lines.append(" \(callExpression(arity: arity))")
110
+ lines.append(" \(callExpression(receiver: receiver, arity: arity))")
109
111
  }
110
112
  lines.append("}")
111
113
  lines.append(contentsOf: encodeResultLines())
@@ -127,13 +129,14 @@ internal struct JSFunction {
127
129
  if let accessor = fastDecodeAccessor(for: type) {
128
130
  return "let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()"
129
131
  }
130
- return "let arg\(index) = try \(type).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(type)"
132
+ let exprType = expressionType(type)
133
+ return "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)"
131
134
  }
132
135
 
133
- /// The `self.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
136
+ /// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
134
137
  /// `arg<i>`; a trailing optional-without-default slot that this arity omits is passed `nil`; a
135
138
  /// trailing defaulted slot that this arity omits is dropped entirely so Swift applies its default.
136
- private func callExpression(arity: Int) -> String {
139
+ private func callExpression(receiver: Receiver, arity: Int) -> String {
137
140
  var callArguments: [String] = []
138
141
  for (index, parameter) in parameters.enumerated() {
139
142
  let label = parameter.firstName.text
@@ -154,21 +157,22 @@ internal struct JSFunction {
154
157
  }
155
158
  let tryKeyword = (isThrowing || isAsync) ? "try " : ""
156
159
  let awaitKeyword = isAsync ? "await " : ""
157
- return "\(tryKeyword)\(awaitKeyword)self.\(swiftName)(\(callArguments.joined(separator: ", ")))"
160
+ return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments.joined(separator: ", ")))"
158
161
  }
159
162
 
160
163
  /// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
161
- /// `let result = self.f(...)` then the return encode (or the no-return `self.f(...)` + `.undefined`).
162
- private func callAndEncodeLines(arity: Int, decodingFrom: Int) -> [String] {
164
+ /// `let result = <callee>.f(...)` then the return encode (or the no-return `<callee>.f(...)` +
165
+ /// `.undefined`).
166
+ private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
163
167
  var lines: [String] = []
164
168
  for index in decodingFrom..<arity {
165
169
  lines.append(decodeStatement(at: index))
166
170
  }
167
171
  if returnType != nil {
168
- lines.append("let result = \(callExpression(arity: arity))")
172
+ lines.append("let result = \(callExpression(receiver: receiver, arity: arity))")
169
173
  lines.append(contentsOf: encodeResultLines())
170
174
  } else {
171
- lines.append(callExpression(arity: arity))
175
+ lines.append(callExpression(receiver: receiver, arity: arity))
172
176
  lines.append("return .undefined")
173
177
  }
174
178
  return lines
@@ -184,7 +188,7 @@ internal struct JSFunction {
184
188
  if fastDecodeAccessor(for: returnType) != nil {
185
189
  return ["return result.toJavaScriptValue(in: runtime)"]
186
190
  }
187
- return ["return try \(returnType).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
191
+ return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
188
192
  }
189
193
 
190
194
  /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
@@ -193,27 +197,44 @@ internal struct JSFunction {
193
197
  /// function the body `await`s the call, which selects the async `setProperty` overload (so JS
194
198
  /// receives a promise).
195
199
  ///
196
- /// Capture mirrors core's `SyncFunctionDefinition.build`: `self` (the module) is captured
197
- /// **strong** — the host-function closure is what keeps the native callable alive for as long as
198
- /// JS can invoke it; its lifetime is bounded by the JS VM's garbage collection of the object.
200
+ /// Capture mirrors core's `SyncFunctionDefinition.build`: a module captures its `self` **strong**
201
+ /// the host-function closure is what keeps the native callable alive for as long as JS can invoke
202
+ /// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
203
+ /// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
199
204
  /// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
200
205
  /// the app context. When no argument or return value goes through the dynamic-type converter the
201
206
  /// body never references `appContext`, so the capture and guard are omitted to avoid the
202
207
  /// unused-capture warning.
203
- var decorateStatements: String {
208
+ func decorateStatements(receiver: Receiver) -> String {
209
+ // Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
210
+ // `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
211
+ // `JavaScriptValue` and forming its `weak`-runtime reference on every call. A module ignores
212
+ // `this`; a shared object unwraps it (still borrowed). The first parameter is typed `borrowing
213
+ // JavaScriptUnownedValue` to select that (otherwise `@_disfavoredOverload`) overload — which
214
+ // requires the *parenthesized, fully typed* parameter list, since Swift rejects a type annotation
215
+ // on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
216
+ // shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
217
+ // escapes into the task anyway.
218
+ let captures = receiver.captureClause(usesAppContext: usesAppContext)
219
+ let parameters =
220
+ isAsync
221
+ ? "this, arguments"
222
+ : "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
223
+
224
+ let object = receiver.decoratedObject
204
225
  if usesAppContext {
205
226
  return """
206
- object.setProperty("\(jsName)") { [weak appContext, self] this, arguments in
227
+ \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
207
228
  guard let appContext else {
208
229
  throw Exceptions.AppContextLost()
209
230
  }
210
- \(bodyStatements(indent: " "))
231
+ \(bodyStatements(receiver: receiver, indent: " "))
211
232
  }
212
233
  """
213
234
  }
214
235
  return """
215
- object.setProperty("\(jsName)") { [self] this, arguments in
216
- \(bodyStatements(indent: " "))
236
+ \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
237
+ \(bodyStatements(receiver: receiver, indent: " "))
217
238
  }
218
239
  """
219
240
  }
@@ -233,17 +254,18 @@ internal struct JSFunction {
233
254
  }
234
255
 
235
256
  /// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
236
- /// `Property(...)` DSL entry, `@ExpoModule` synthesizes a get/set accessor into the module's JS
237
- /// object inside `_decorateModule`: it builds a descriptor object (`enumerable` + `get`, and `set`
238
- /// when the property is settable) and installs it with `object.defineProperty(name, descriptor:)`,
239
- /// mirroring core's `PropertyDefinition.buildDescriptor`. The `get`/`set` host functions are
240
- /// installed the same way `@JS func`s are — the closure-taking `setProperty(_:)` overload, with the
241
- /// read/write body inlined into the closure.
257
+ /// `Property(...)` DSL entry, the enclosing macro synthesizes a get/set accessor into the JS object
258
+ /// inside its decorator (`_decorateModule` / `_decorateSharedObject`): it builds a descriptor object
259
+ /// (`enumerable` + `get`, and `set` when the property is settable) and installs it with
260
+ /// `object.defineProperty(name, descriptor:)`, mirroring core's `PropertyDefinition.buildDescriptor`.
261
+ /// The `get`/`set` host functions are installed the same way `@JS func`s are — the closure-taking
262
+ /// `setProperty(_:)` overload, with the read/write body inlined into the closure.
242
263
  ///
243
- /// The receiver is the module's real `self`, so the getter reads `self.<name>` and the setter writes
244
- /// `self.<name> = …` directly, ignoring the JS `this`. Decode/encode of the value reuse the same
245
- /// static-type fast path as functions (primitives through a direct typed accessor / `toJavaScriptValue`,
246
- /// other types through the `getDynamicType()` converter).
264
+ /// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
265
+ /// 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).
247
269
  internal struct JSProperty {
248
270
  let swiftName: String
249
271
  let jsName: String
@@ -256,54 +278,66 @@ internal struct JSProperty {
256
278
  let isSettable: Bool
257
279
 
258
280
  /// The statements that install this property's accessor on the JS object, indented for the
259
- /// `_decorateModule` body. Builds a descriptor object (`enumerable` + `get`, and `set` when
260
- /// settable) via the closure-taking `setProperty(_:)` overload — with the read/write body inlined
261
- /// into each closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture
262
- /// matches the function bindings: `self` strong, `appContext` weak + guarded and, like functions,
263
- /// the `appContext` capture + guard are omitted from an accessor whose body never references it (a
264
- /// primitive value, decoded/encoded without the dynamic converter), to avoid the unused-capture
265
- /// warning. Getter and setter are gated independently.
266
- var decorateStatements: String {
281
+ /// decorator body. Builds a descriptor object (`enumerable` + `get`, and `set` when settable) via
282
+ /// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
283
+ /// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
284
+ /// 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.
289
+ func decorateStatements(receiver: Receiver) -> String {
267
290
  let descriptorName = "\(swiftName)Descriptor"
291
+ let callee = receiver.callee
292
+ let object = receiver.decoratedObject
268
293
  // A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
269
294
  // references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
270
295
  // getter, which also doesn't use it.
271
296
  let usesAppContext = valueType.map { fastDecodeAccessor(for: $0) == nil } ?? false
297
+ // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
298
+ // reads `self` directly. The unwrap leads each accessor body.
299
+ let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
272
300
  var lines: [String] = []
273
301
 
274
302
  lines.append("let \(descriptorName) = runtime.createObject()")
275
303
  lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
276
304
 
277
- // Getter: read `self.<name>` and encode the result back to JS.
305
+ // Getter: read `<callee>.<name>` and encode the result back to JS.
278
306
  let getEncode: String
279
307
  if let valueType, fastDecodeAccessor(for: valueType) != nil {
280
- getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
308
+ getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
281
309
  } else if let valueType {
282
310
  getEncode =
283
- "return try \(valueType).getDynamicType().castToJS(self.\(swiftName), appContext: appContext, in: runtime)"
311
+ "return try \(expressionType(valueType)).getDynamicType().castToJS(\(callee).\(swiftName), appContext: appContext, in: runtime)"
284
312
  } else {
285
- // No known type: fall back to converting whatever `self.<name>` is. This only happens when the
286
- // declaration has neither an annotation nor a literal default, which is rare for a stored var.
287
- getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
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
+ getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
288
317
  }
289
- lines.append(accessorClosure(descriptorName, "get", usesAppContext: usesAppContext, body: getEncode))
318
+ lines.append(
319
+ accessorClosure(
320
+ descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
290
321
 
291
- // Setter: decode argument 0 by the static type and write `self.<name>`. A typed setter needs a
292
- // known value type; when the type couldn't be inferred the property is bound getter-only (a
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
293
324
  // settable var with neither an annotation nor a literal default is rare and can't be decoded).
294
325
  if isSettable, let valueType {
295
326
  let setDecode: String
296
327
  if let accessor = fastDecodeAccessor(for: valueType) {
297
- setDecode = "self.\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
328
+ setDecode = "\(callee).\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
298
329
  } else {
330
+ let exprType = expressionType(valueType)
299
331
  setDecode =
300
- "self.\(swiftName) = try \(valueType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(valueType)"
332
+ "\(callee).\(swiftName) = try \(exprType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(exprType)"
301
333
  }
302
334
  lines.append(
303
- accessorClosure(descriptorName, "set", usesAppContext: usesAppContext, body: "\(setDecode)\nreturn .undefined"))
335
+ accessorClosure(
336
+ descriptorName, "set", receiver: receiver, usesAppContext: usesAppContext,
337
+ body: "\(unwrap)\(setDecode)\nreturn .undefined"))
304
338
  }
305
339
 
306
- lines.append("object.defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
340
+ lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
307
341
 
308
342
  return lines
309
343
  .flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
@@ -311,21 +345,29 @@ internal struct JSProperty {
311
345
  .joined(separator: "\n")
312
346
  }
313
347
 
314
- /// One `descriptor.setProperty("get"/"set") { … }` accessor entry. Captures `self` strong and, when
315
- /// `usesAppContext`, `appContext` weak + guarded (matching the function bindings); otherwise the
316
- /// capture and guard are omitted so a primitive accessor doesn't warn on an unused capture.
348
+ /// 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.
317
352
  private func accessorClosure(
318
- _ descriptorName: String, _ key: String, usesAppContext: Bool, body: String
353
+ _ descriptorName: String, _ key: String, receiver: Receiver, usesAppContext: Bool, body: String
319
354
  ) -> String {
355
+ let captures = receiver.captureClause(usesAppContext: usesAppContext)
320
356
  // Indent each line of a (possibly multi-line) body to sit one level inside the closure, aligned
321
357
  // with the `guard`; a bare `\(body)` interpolation would only indent the first line.
322
358
  let indentedBody = body
323
359
  .split(separator: "\n", omittingEmptySubsequences: false)
324
360
  .map { " \($0)" }
325
361
  .joined(separator: "\n")
362
+ // Property `get`/`set` accessors are always synchronous, so they bind through the unowned-`this`
363
+ // `setProperty` overload like sync functions. The parameter list is parenthesized and fully typed
364
+ // because Swift rejects a type annotation on a shorthand closure parameter; the explicit
365
+ // `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
366
+ // a shared object unwraps it in the body.
367
+ let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
326
368
  if usesAppContext {
327
369
  return """
328
- \(descriptorName).setProperty("\(key)") { [weak appContext, self] this, arguments in
370
+ \(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
329
371
  guard let appContext else {
330
372
  throw Exceptions.AppContextLost()
331
373
  }
@@ -334,24 +376,31 @@ internal struct JSProperty {
334
376
  """
335
377
  }
336
378
  return """
337
- \(descriptorName).setProperty("\(key)") { [self] this, arguments in
379
+ \(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
338
380
  \(indentedBody)
339
381
  }
340
382
  """
341
383
  }
342
384
  }
343
385
 
386
+ /// The body shared by both decorators: every `@JS func` bound via an inlined `setProperty` closure
387
+ /// and every `@JS var` via a `defineProperty` accessor, joined for the function body. The `receiver`
388
+ /// selects how each binding reaches its Swift value (module `self` vs. shared-object `_self`).
389
+ private func decorateBody(functions: [JSFunction], properties: [JSProperty], receiver: Receiver) -> String {
390
+ let functionBody = functions.map { $0.decorateStatements(receiver: receiver) }
391
+ let propertyBody = properties.map { $0.decorateStatements(receiver: receiver) }
392
+ return (functionBody + propertyBody).joined(separator: "\n")
393
+ }
394
+
344
395
  /// The single generated function that decorates the module's JS object. Core supplies the object;
345
396
  /// this binds every `@JS func` (via an inlined `setProperty` closure) and every `@JS var` (via a
346
397
  /// `defineProperty` accessor) into it. Mirrors core's `ObjectDefinition.decorate(object:)`, including
347
398
  /// its `borrowing` object parameter (it mutates through the reference without reassigning or taking
348
399
  /// ownership). Named `_decorateModule` with the leading-underscore convention for synthesized members
349
400
  /// the **runtime calls by name**; the `ExpoModule` suffix names the `@ExpoModule` macro it came from (a
350
- /// shared object's counterpart is `_decorateSharedObject`).
401
+ /// shared object's counterpart is `_decorateSharedObject`). The bindings call into the module `self`.
351
402
  internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
352
- let functionBody = functions.map { $0.decorateStatements }
353
- let propertyBody = properties.map { $0.decorateStatements }
354
- let body = (functionBody + propertyBody).joined(separator: "\n")
403
+ let body = decorateBody(functions: functions, properties: properties, receiver: .module)
355
404
  return """
356
405
  @JavaScriptActor
357
406
  public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
@@ -360,23 +409,24 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
360
409
  """
361
410
  }
362
411
 
363
- /// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly,
364
- /// bypassing the dynamic-type converter (`asDouble()` for `Double`, etc.). Returns `nil` for
365
- /// types without a dedicated accessor arrays, records, optionals, shared objects, other numeric
366
- /// widths which decode through `getDynamicType().cast(...)`.
367
- private func fastDecodeAccessor(for type: String) -> String? {
368
- switch type {
369
- case "Bool":
370
- return "asBool"
371
- case "Int":
372
- return "asInt"
373
- case "Double":
374
- return "asDouble"
375
- case "String":
376
- return "asString"
377
- default:
378
- return nil
379
- }
412
+ /// The shared-object counterpart of `_decorateModule`. Core supplies the class `prototype`; this binds
413
+ /// 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
418
+ /// bound separately (see `JSConstructor.buildConstructor`). Only emitted when the type has at least one
419
+ /// `@JS func`/`var`.
420
+ internal func buildDecorateSharedObject(
421
+ functions: [JSFunction], properties: [JSProperty], typeName: String
422
+ ) -> DeclSyntax {
423
+ let body = decorateBody(functions: functions, properties: properties, receiver: .sharedObject(typeName: typeName))
424
+ return """
425
+ @JavaScriptActor
426
+ public static func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
427
+ \(raw: body)
428
+ }
429
+ """
380
430
  }
381
431
 
382
432
  /// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
@@ -244,55 +244,3 @@ private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
244
244
  }
245
245
  return false
246
246
  }
247
-
248
- // MARK: - Member builders
249
-
250
- private func collectProperties(
251
- varDecl: VariableDeclSyntax,
252
- attribute: AttributeSyntax
253
- ) -> [JSProperty] {
254
- let jsNameOverride = jsNameArgument(of: attribute)
255
- // A `let` is never settable; only `var` bindings can carry a setter.
256
- let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
257
-
258
- return varDecl.bindings.compactMap { binding in
259
- guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
260
- return nil
261
- }
262
- let swiftName = ident.identifier.text
263
- // Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
264
- // when there's none. `nil` falls back to inference at the use site.
265
- let valueType = binding.typeAnnotation?.type.trimmedDescription
266
- ?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
267
- return JSProperty(
268
- swiftName: swiftName,
269
- jsName: jsNameOverride ?? swiftName,
270
- valueType: valueType,
271
- isSettable: isVar && bindingIsSettable(binding)
272
- )
273
- }
274
- }
275
-
276
- /// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
277
- /// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
278
- /// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
279
- /// observers imply stored storage, which is also settable.
280
- private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
281
- guard let accessorBlock = binding.accessorBlock else {
282
- return true
283
- }
284
- switch accessorBlock.accessors {
285
- case .accessors(let accessors):
286
- return accessors.contains { accessor in
287
- switch accessor.accessorSpecifier.tokenKind {
288
- case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
289
- return true
290
- default:
291
- return false
292
- }
293
- }
294
- case .getter:
295
- return false
296
- }
297
- }
298
-
@@ -0,0 +1,60 @@
1
+ import SwiftSyntax
2
+
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.
7
+ internal struct JSConstructor {
8
+ let parameters: [FunctionParameterSyntax]
9
+
10
+ init(initDecl: InitializerDeclSyntax) {
11
+ self.parameters = Array(initDecl.signature.parameterClause.parameters)
12
+ }
13
+
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, …)`.
16
+ private func bodyStatements(typeName: String, indent: String) -> String {
17
+ var lines: [String] = []
18
+
19
+ lines.append(
20
+ """
21
+ guard arguments.count == \(parameters.count) else {
22
+ throw Exceptions.ArgumentsRangeMismatch((functionName: "\(typeName)", received: arguments.count, required: \(parameters.count), maximum: \(parameters.count)))
23
+ }
24
+ """)
25
+
26
+ var callArguments: [String] = []
27
+ 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
+ }
37
+
38
+ let label = parameter.firstName.text
39
+ callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
40
+ }
41
+
42
+ lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
43
+
44
+ return lines
45
+ .flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
46
+ .map { indent + $0 }
47
+ .joined(separator: "\n")
48
+ }
49
+
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.
52
+ func buildConstructor(typeName: String) -> DeclSyntax {
53
+ return """
54
+ @JavaScriptActor
55
+ public static func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime, appContext: AppContext) throws -> \(raw: typeName) {
56
+ \(raw: bodyStatements(typeName: typeName, indent: " "))
57
+ }
58
+ """
59
+ }
60
+ }