@expo/expo-modules-macros-plugin 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -2,14 +2,17 @@ import SwiftSyntax
2
2
 
3
3
  /// A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
4
4
  /// `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call, the enclosing
5
- /// macro synthesizes a decorator (`_decorateModule` / `_decorateSharedObject`) that binds each such
5
+ /// macro synthesizes a decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`) that binds each such
6
6
  /// function into the JS object via the closure-taking `JavaScriptObject.setProperty(_:)`, with the
7
- /// decode-call-encode body inlined into the closure. This omits the `[Any]`/`toTuple` dynamic-call path:
7
+ /// decode-call-encode body inlined into the closure. This omits the dynamic-call path entirely:
8
8
  /// every argument is decoded individually by its static type.
9
9
  ///
10
10
  /// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
11
- /// unwrapped from the JS `this` for a shared-object binding. An `async` `@JS func` produces an `async`
12
- /// closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
11
+ /// unwrapped from the JS `this` for a shared-object binding. An `async` `@JS func` binds through the
12
+ /// two-phase async `setProperty(_:)` overload: the closure is the synchronous decode phase (receiver
13
+ /// unwrap, arity guard, argument decode, all while `this` and the arguments are still valid)
14
+ /// and returns the async body (call and result encode), so nothing JSI-owned crosses the asynchronous
15
+ /// boundary and JS gets a promise.
13
16
  internal struct JSFunction {
14
17
  let swiftName: String
15
18
  let jsName: String
@@ -60,7 +63,7 @@ internal struct JSFunction {
60
63
  let maximum = parameters.count
61
64
  var lines: [String] = []
62
65
 
63
- if let unwrap = receiver.unwrapStatement(isAsync: isAsync) {
66
+ if let unwrap = receiver.unwrapStatement {
64
67
  lines.append(unwrap)
65
68
  }
66
69
 
@@ -87,8 +90,31 @@ internal struct JSFunction {
87
90
  }
88
91
 
89
92
  if required == maximum {
90
- // No omittable trailing run: a single flat call with every argument decoded.
91
- lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
93
+ // No omittable trailing run: a single flat call with every argument decoded. An async
94
+ // function decodes any remaining slots here (still the synchronous phase) and returns its
95
+ // body; a sync one calls and encodes directly.
96
+ if isAsync {
97
+ for index in required..<maximum {
98
+ lines.append(decodeStatement(at: index))
99
+ }
100
+ lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: maximum))
101
+ } else {
102
+ lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
103
+ }
104
+ } else if isAsync {
105
+ // One body per accepted arity, branching on `arguments.count`. A branch decodes its trailing
106
+ // slots synchronously and returns the async body for that call shape, so each `return` ends
107
+ // the decode phase for its arity.
108
+ lines.append("switch arguments.count {")
109
+ for arity in required...maximum {
110
+ let label = arity == maximum ? "default:" : "case \(arity):"
111
+ lines.append(label)
112
+ for index in required..<arity {
113
+ lines.append(" " + decodeStatement(at: index))
114
+ }
115
+ lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: arity).map { " " + $0 })
116
+ }
117
+ lines.append("}")
92
118
  } else {
93
119
  // One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
94
120
  // trailing slot before the call, so this is a `switch` statement (not an expression): a
@@ -116,6 +142,24 @@ internal struct JSFunction {
116
142
  .joined(separator: "\n")
117
143
  }
118
144
 
145
+ /// The `return { … }` statement closing an async binding's decode phase: the returned closure is
146
+ /// the function's async body (`AsyncFunctionBody`), awaiting the call and encoding the result. It
147
+ /// captures only what the call needs — the decoded `arg<i>` locals, the receiver, and `runtime`
148
+ /// for the encode — never `this` or the arguments buffer, which are only valid for the duration
149
+ /// of the host call.
150
+ private func asyncBodyLines(receiver: Receiver, arity: Int) -> [String] {
151
+ var lines: [String] = ["return {"]
152
+ if returnType != nil {
153
+ lines.append(" let result = \(callExpression(receiver: receiver, arity: arity))")
154
+ lines.append(contentsOf: encodeResultLines().map { " " + $0 })
155
+ } else {
156
+ lines.append(" \(callExpression(receiver: receiver, arity: arity))")
157
+ lines.append(" return .undefined")
158
+ }
159
+ lines.append("}")
160
+ return lines
161
+ }
162
+
119
163
  /// `let arg<index> = …` decoding the slot at `index` by its static type through
120
164
  /// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
121
165
  /// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
@@ -177,40 +221,51 @@ internal struct JSFunction {
177
221
  /// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
178
222
  /// catchable error is the right behavior, and matches how non-primitive integers already encode. A
179
223
  /// no-return function returns `.undefined` instead.
224
+ ///
225
+ /// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
226
+ /// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
227
+ /// object, array, or typed array) off the JS thread races the engine's garbage collector and
228
+ /// corrupts the heap. `runtime.execute` hops the encode onto the JS thread, running inline when it is
229
+ /// already there (the common case, where the body never truly suspended), so synchronous functions —
230
+ /// which always run on the JS thread — keep encoding directly without the wrapper.
180
231
  private func encodeResultLines() -> [String] {
181
232
  guard let returnType else {
182
233
  return ["return .undefined"]
183
234
  }
184
- return ["return try \(expressionType(returnType)).encode(result, in: runtime)"]
235
+ let encode = "try \(expressionType(returnType)).encode(result, in: runtime)"
236
+ if isAsync {
237
+ return [
238
+ "return try await runtime.execute {",
239
+ " return \(encode)",
240
+ "}",
241
+ ]
242
+ }
243
+ return ["return \(encode)"]
185
244
  }
186
245
 
187
246
  /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
188
247
  /// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
189
248
  /// (which creates the host function under the hood) — no separate named binding. For an `async`
190
- /// function the body `await`s the call, which selects the async `setProperty` overload (so JS
191
- /// receives a promise).
249
+ /// function the closure is the synchronous decode phase and returns the async body, which selects
250
+ /// the async `setProperty` overload (so JS receives a promise).
192
251
  ///
193
- /// Capture mirrors core's `SyncFunctionDefinition.build`: a module captures its `self` **strong** —
252
+ /// A module captures its `self` **strong** —
194
253
  /// the host-function closure is what keeps the native callable alive for as long as JS can invoke
195
254
  /// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
196
255
  /// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
197
- func decorateStatements(receiver: Receiver) -> String {
198
- // Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
199
- // `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
200
- // `JavaScriptValue` and forming its `weak`-runtime reference on every call. A module ignores
201
- // `this`; a shared object unwraps it (still borrowed). The first parameter is typed `borrowing
202
- // JavaScriptUnownedValue` to select that (otherwise `@_disfavoredOverload`) overload which
203
- // requires the *parenthesized, fully typed* parameter list, since Swift rejects a type annotation
204
- // on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
205
- // shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
206
- // escapes into the task anyway.
256
+ func decorateStatements(object: String, receiver: Receiver) -> String {
257
+ // Every `@JS` binding hands `this` in as a borrowed `JavaScriptUnownedValue` instead of
258
+ // allocating an owning `JavaScriptValue` and forming its `weak`-runtime reference on every call,
259
+ // and consumes the arguments buffer sync and async closures share one parameter shape and
260
+ // differ only in what they return. A module ignores `this`; a shared object unwraps it (still
261
+ // borrowed) an async binding does so in its synchronous decode phase, before the borrow ends.
262
+ // The parameter list is parenthesized and fully typed, since Swift rejects a type annotation on
263
+ // a shorthand `{ [capture] name, name in }` parameter; for a sync binding the explicit
264
+ // `JavaScriptUnownedValue` also selects the (otherwise `@_disfavoredOverload`) unowned-`this`
265
+ // overload.
207
266
  let captures = receiver.captureClause
208
- let parameters =
209
- isAsync
210
- ? "this, arguments"
211
- : "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
267
+ let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
212
268
 
213
- let object = receiver.decoratedObject
214
269
  return """
215
270
  \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
216
271
  \(bodyStatements(receiver: receiver, indent: " "))
@@ -221,9 +276,9 @@ internal struct JSFunction {
221
276
 
222
277
  /// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
223
278
  /// `Property(...)` DSL entry, the enclosing macro synthesizes a get/set accessor into the JS object
224
- /// inside its decorator (`_decorateModule` / `_decorateSharedObject`): it builds a descriptor object
279
+ /// inside its decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`): it builds a descriptor object
225
280
  /// (`enumerable` + `get`, and `set` when the property is settable) and installs it with
226
- /// `object.defineProperty(name, descriptor:)`, mirroring core's `PropertyDefinition.buildDescriptor`.
281
+ /// `object.defineProperty(name, descriptor:)`.
227
282
  /// The `get`/`set` host functions are installed the same way `@JS func`s are — the closure-taking
228
283
  /// `setProperty(_:)` overload, with the read/write body inlined into the closure.
229
284
  ///
@@ -248,14 +303,13 @@ internal struct JSProperty {
248
303
  /// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
249
304
  /// function bindings: a module captures `self` strong, a shared object captures nothing of the
250
305
  /// instance. Getter and setter are gated independently.
251
- func decorateStatements(receiver: Receiver) -> String {
306
+ func decorateStatements(object: String, receiver: Receiver) -> String {
252
307
  let descriptorName = "\(swiftName)Descriptor"
253
308
  let callee = receiver.callee
254
- let object = receiver.decoratedObject
255
309
  // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
256
310
  // reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
257
311
  // they take the borrowed unowned `this`.
258
- let unwrap = receiver.unwrapStatement(isAsync: false).map { "\($0)\n" } ?? ""
312
+ let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
259
313
  var lines: [String] = []
260
314
 
261
315
  lines.append("let \(descriptorName) = runtime.createObject()")
@@ -319,24 +373,30 @@ internal struct JSProperty {
319
373
  }
320
374
  }
321
375
 
322
- /// The body shared by both decorators: every `@JS func` bound via an inlined `setProperty` closure
323
- /// and every `@JS var` via a `defineProperty` accessor, joined for the function body. The `receiver`
324
- /// selects how each binding reaches its Swift value (module `self` vs. shared-object `_self`).
325
- private func decorateBody(functions: [JSFunction], properties: [JSProperty], receiver: Receiver) -> String {
326
- let functionBody = functions.map { $0.decorateStatements(receiver: receiver) }
327
- let propertyBody = properties.map { $0.decorateStatements(receiver: receiver) }
376
+ /// The body shared by every decorator phase: every `@JS func` bound via an inlined `setProperty`
377
+ /// closure and every `@JS var` via a `defineProperty` accessor, joined for the function body. `object`
378
+ /// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
379
+ /// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
380
+ /// metatype for a static member).
381
+ private func decorateBody(
382
+ functions: [JSFunction], properties: [JSProperty], object: String, receiver: Receiver
383
+ ) -> String {
384
+ let functionBody = functions.map { $0.decorateStatements(object: object, receiver: receiver) }
385
+ let propertyBody = properties.map { $0.decorateStatements(object: object, receiver: receiver) }
328
386
  return (functionBody + propertyBody).joined(separator: "\n")
329
387
  }
330
388
 
331
- /// The single generated function that decorates the module's JS object. Core supplies the object;
332
- /// this binds every `@JS func` (via an inlined `setProperty` closure) and every `@JS var` (via a
333
- /// `defineProperty` accessor) into it. Mirrors core's `ObjectDefinition.decorate(object:)`, including
334
- /// its `borrowing` object parameter (it mutates through the reference without reassigning or taking
335
- /// ownership). Named `_decorateModule` with the leading-underscore convention for synthesized members
336
- /// the **runtime calls by name**; the `ExpoModule` suffix names the `@ExpoModule` macro it came from (a
337
- /// shared object's counterpart is `_decorateSharedObject`). The bindings call into the module `self`.
389
+ /// The module decorator: a `_decorateModule(object:)` that binds every `@JS func` (via an inlined
390
+ /// `setProperty` closure) and every `@JS var` (via a `defineProperty` accessor) into the module's own
391
+ /// JS object. Core supplies the object; the bindings call into the module `self`. It's an *instance*
392
+ /// method (a module is a singleton, satisfying the `AnyModule` requirement of the same name), with a
393
+ /// `borrowing` object parameter (it mutates through the reference without reassigning or taking
394
+ /// ownership). Only emitted when there's at least
395
+ /// one member to bind. Uses the shared body generation with the module `object:` phase and `self`
396
+ /// receiver; the shared-object counterpart is `buildDecorateSharedObjectPhase`.
338
397
  internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
339
- let body = decorateBody(functions: functions, properties: properties, receiver: .module)
398
+ let body = decorateBody(
399
+ functions: functions, properties: properties, object: Phase.object.rawValue, receiver: .module)
340
400
  return """
341
401
  @JavaScriptActor
342
402
  public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
@@ -345,22 +405,28 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
345
405
  """
346
406
  }
347
407
 
348
- /// The shared-object counterpart of `_decorateModule`. Core supplies the class `prototype`; this binds
349
- /// every `@JS func` and `@JS var` of the given shared-object type onto it. Because a shared object has a
350
- /// distinct native instance behind each JS object, the bindings recover the typed receiver from the JS
351
- /// `this` per call (`try SharedObject.native(from: this.asObject(in: runtime), as: <Type>.self)`)
352
- /// rather than capturing a singleton `self`. Overrides the base `SharedObject` class method so core can
353
- /// dispatch to it through the concrete type's metatype. The first parameter is `prototype` (not `object`
354
- /// as on `_decorateModule`) because it's the shared class prototype, not an instance. The constructor is
355
- /// bound separately (see `JSConstructor.buildConstructor`). Only emitted when the type has at least one
356
- /// `@JS func`/`var`.
357
- internal func buildDecorateSharedObject(
358
- functions: [JSFunction], properties: [JSProperty], typeName: String
408
+ /// A shared-object decorator phase, a label overload of `_decorateSharedObject` overriding the matching
409
+ /// `open class func` on the `SharedObject` base so core can dispatch through the concrete type's
410
+ /// metatype. The `prototype:` overload binds the type's instance `@JS func`/`var` members onto the class
411
+ /// prototype (the original hook, unchanged); the `constructor:` overload binds the `static`/`class` ones
412
+ /// onto the constructor function itself (a new, additive overload, so introducing it isn't a breaking
413
+ /// core change). Both are *static* (a shared object has no singleton `self`). An instance binding
414
+ /// recovers its typed receiver from the JS `this` per call (`SharedObject.native(from:as:)`); a static
415
+ /// binding calls the Swift member on the type and ignores `this` (which, on the static side, is the
416
+ /// constructor). The constructor *object* the static members decorate is distinct from the `@JS init`,
417
+ /// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
418
+ /// is emitted only when the type has a member for it.
419
+ internal func buildDecorateSharedObjectPhase(
420
+ phase: Phase, functions: [JSFunction], properties: [JSProperty], typeName: String
359
421
  ) -> DeclSyntax {
360
- let body = decorateBody(functions: functions, properties: properties, receiver: .sharedObject(typeName: typeName))
422
+ let receiver: Receiver = phase == .constructor
423
+ ? .staticMember(typeName: typeName)
424
+ : .sharedObject(typeName: typeName)
425
+ let body = decorateBody(
426
+ functions: functions, properties: properties, object: phase.rawValue, receiver: receiver)
361
427
  return """
362
428
  @JavaScriptActor
363
- public override class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
429
+ public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
364
430
  \(raw: body)
365
431
  }
366
432
  """
@@ -98,14 +98,6 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
98
98
  return nil
99
99
  }
100
100
 
101
- /// True when the modifiers make the member type-level (`static` or `class`), so its assertion peer
102
- /// must be emitted in the same metatype context rather than as an instance member.
103
- private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
104
- return modifiers.contains {
105
- $0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
106
- }
107
- }
108
-
109
101
  /// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
110
102
  /// no conformance assertion. (A missing return clause never reaches here: `returnClause` is `nil`.)
111
103
  private func isVoidType(_ type: TypeSyntax) -> Bool {
@@ -43,6 +43,15 @@ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
43
43
  return false
44
44
  }
45
45
 
46
+ /// True when the modifiers make the member type-level (`static` or `class`). A shared object routes
47
+ /// such members to its constructor (JS-static) and instance members to its prototype; the `@JS`
48
+ /// conformance assertion also uses this to emit its peer in the matching metatype context.
49
+ internal func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
50
+ return modifiers.contains {
51
+ $0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
52
+ }
53
+ }
54
+
46
55
  /// True if a trailing occurrence of this parameter may be omitted by the JS caller: it either has a
47
56
  /// default value (Swift applies it) or is an optional type (an absent slot becomes `nil`). The arity
48
57
  /// range and the per-arity call branches are derived from this.
@@ -1,62 +1,79 @@
1
1
  import SwiftSyntax
2
2
 
3
- /// Where a directly-bound closure gets the Swift value it calls into. A module is a singleton, so its
4
- /// bindings call `self` and ignore the JS `this`; a shared object has a distinct native instance per JS
5
- /// object, so its bindings recover the typed receiver from `this`.
3
+ /// Where a directly-bound closure gets the Swift value it calls into. This is one of the two
4
+ /// orthogonal axes of a binding; the other is the `Phase` (which JS object the member is installed
5
+ /// on). The two are independent: choosing the prototype phase does not by itself decide whether the
6
+ /// receiver is `self`, `_self`, or the metatype.
7
+ ///
8
+ /// - A module is a singleton, so its bindings call `self` and ignore the JS `this`.
9
+ /// - A shared-object *instance* member has a distinct native instance per JS object, so it recovers
10
+ /// the typed receiver from `this`.
11
+ /// - A `static`/`class` member has no instance at all; it calls the Swift member on the metatype
12
+ /// (`Cache.open(…)`) and ignores `this` (which, on the static side, is the constructor).
6
13
  internal enum Receiver {
7
14
  /// The module singleton; the closure captures `self` strong.
8
15
  case module
9
16
  /// A shared object of the given concrete type; the closure captures nothing and recovers the receiver
10
17
  /// from `this` per call.
11
18
  case sharedObject(typeName: String)
19
+ /// A `static`/`class` member of the given concrete type; the closure captures nothing and calls the
20
+ /// Swift member on the type itself, ignoring `this`.
21
+ case staticMember(typeName: String)
12
22
 
13
23
  /// The expression the body calls members on: `self` for a module, `_self` (bound by `unwrapStatement`)
14
- /// for a shared object. The leading underscore avoids colliding with a user member like `var owner`.
24
+ /// for a shared-object instance, the type name for a static member. The leading underscore on `_self`
25
+ /// avoids colliding with a user member like `var owner`.
15
26
  var callee: String {
16
27
  switch self {
17
28
  case .module:
18
29
  return "self"
19
30
  case .sharedObject:
20
31
  return "_self"
32
+ case .staticMember(let typeName):
33
+ return typeName
21
34
  }
22
35
  }
23
36
 
24
- /// The JS object the decorator binds members onto, matching its first parameter: `object` for a
25
- /// module (its own JS object), `prototype` for a shared object (the shared class prototype).
26
- var decoratedObject: String {
27
- switch self {
28
- case .module:
29
- return "object"
30
- case .sharedObject:
31
- return "prototype"
32
- }
33
- }
34
-
35
- /// The leading body line binding the receiver, or `nil` for a module (it reads `self` directly). For a
36
- /// shared object, `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign
37
- /// object or a type mismatch.
37
+ /// The leading body line binding the receiver, or `nil` when nothing needs to be unwrapped (a module
38
+ /// reads `self` directly; a static member calls the type directly). For a shared-object instance,
39
+ /// `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign object or a type
40
+ /// mismatch.
38
41
  ///
39
- /// The `this` object comes from the borrowed `JavaScriptUnownedValue` in a sync binding (`asObject(in:)`)
40
- /// and from the owning `JavaScriptValue` in an async one (`asObject()`). An async binding must take an
41
- /// owning `this` because a borrowed unowned value can't survive the closure's suspension points.
42
- func unwrapStatement(isAsync: Bool) -> String? {
42
+ /// The `this` object always comes from the borrowed `JavaScriptUnownedValue` (`asObject(in:)`): an
43
+ /// async binding unwraps in its synchronous decode phase, before the borrowed value's lifetime ends,
44
+ /// and only the recovered native instance crosses into the async body.
45
+ var unwrapStatement: String? {
43
46
  switch self {
44
- case .module:
47
+ case .module, .staticMember:
45
48
  return nil
46
49
  case .sharedObject(let typeName):
47
- let thisObject = isAsync ? "this.asObject()" : "this.asObject(in: runtime)"
48
- return "let _self = try SharedObject.native(from: \(thisObject), as: \(typeName).self)"
50
+ return "let _self = try SharedObject.native(from: this.asObject(in: runtime), as: \(typeName).self)"
49
51
  }
50
52
  }
51
53
 
52
54
  /// The capture-clause fragment (with a trailing space, or empty when nothing is captured). A module
53
- /// captures `self` strong; a shared object captures nothing of the instance.
55
+ /// captures `self` strong; a shared-object instance and a static member capture nothing.
54
56
  var captureClause: String {
55
57
  switch self {
56
58
  case .module:
57
59
  return "[self] "
58
- case .sharedObject:
60
+ case .sharedObject, .staticMember:
59
61
  return ""
60
62
  }
61
63
  }
62
64
  }
65
+
66
+ /// Which JS object a set of bindings is installed on: the second orthogonal axis alongside `Receiver`.
67
+ /// It selects the decorator entry point's argument label and the local name the body binds members onto,
68
+ /// mirroring JS class semantics (a class has a constructor function whose `.prototype` carries instance
69
+ /// members).
70
+ /// The raw value is the argument label of the decorator entry point, which is also the local name the
71
+ /// body binds members onto.
72
+ internal enum Phase: String {
73
+ /// A concrete JS object: a singleton's own object (the module). Instance method; receiver `self`.
74
+ case object
75
+ /// The class constructor's `prototype`, carrying per-instance members. Static; receiver `_self`.
76
+ case prototype
77
+ /// The class constructor function itself, carrying `static`/`class` members. Static; receiver the type.
78
+ case constructor
79
+ }
@@ -42,13 +42,20 @@ public struct SharedObjectMacro: MemberMacro {
42
42
  let jsName = jsNameArgument(of: node) ?? typeName
43
43
 
44
44
  // `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
45
- // the synthesized `_decorateSharedObject` / `_constructSharedObject` rather than described with a
46
- // `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so they're collected here
47
- // instead of appended to the `Class` block. The block keeps only non-`@JS` definitions (none are
48
- // collected today), so it's empty when every member is `@JS`.
45
+ // the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`
46
+ // rather than described with a `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so
47
+ // they're collected here instead of appended to the `Class` block. The block keeps only non-`@JS`
48
+ // definitions (none are collected today), so it's empty when every member is `@JS`.
49
+ //
50
+ // Members split by the `static`/`class` modifier onto two different JS objects: instance members
51
+ // decorate the prototype (receiver recovered from JS `this`), static members decorate the
52
+ // constructor (called on the metatype). A JS instance and static member may share a name without
53
+ // colliding: they live on different objects.
49
54
  let entries: [String] = []
50
- var functions: [JSFunction] = []
51
- var properties: [JSProperty] = []
55
+ var instanceFunctions: [JSFunction] = []
56
+ var instanceProperties: [JSProperty] = []
57
+ var staticFunctions: [JSFunction] = []
58
+ var staticProperties: [JSProperty] = []
52
59
  var constructor: JSConstructor?
53
60
 
54
61
  for member in classDecl.memberBlock.members {
@@ -66,13 +73,23 @@ public struct SharedObjectMacro: MemberMacro {
66
73
 
67
74
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
68
75
  let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
69
- functions.append(JSFunction(funcDecl: funcDecl, attribute: attribute))
76
+ let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
77
+ if isTypeLevel(funcDecl.modifiers) {
78
+ staticFunctions.append(function)
79
+ } else {
80
+ instanceFunctions.append(function)
81
+ }
70
82
  continue
71
83
  }
72
84
 
73
85
  if let varDecl = decl.as(VariableDeclSyntax.self),
74
86
  let attribute = varDecl.attributes.firstAttribute(named: "JS") {
75
- properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
87
+ let collected = collectProperties(varDecl: varDecl, attribute: attribute)
88
+ if isTypeLevel(varDecl.modifiers) {
89
+ staticProperties.append(contentsOf: collected)
90
+ } else {
91
+ instanceProperties.append(contentsOf: collected)
92
+ }
76
93
  }
77
94
  }
78
95
 
@@ -89,13 +106,22 @@ public struct SharedObjectMacro: MemberMacro {
89
106
  """
90
107
  ]
91
108
 
92
- // Direct JSI binding: one `_decorateSharedObject` that binds each `@JS func`/`var` onto the JS
93
- // object (unwrapping the per-call receiver from `this`), and a `_constructSharedObject` that
94
- // builds an instance from the `@JS init` arguments. Each is emitted only when it has something
95
- // to do.
96
- if !functions.isEmpty || !properties.isEmpty {
109
+ // Direct JSI binding, split by which JS object each member decorates:
110
+ // `_decorateSharedObject(prototype:)` binds instance `@JS func`/`var`s (unwrapping the per-call receiver from
111
+ // `this`); `_decorateSharedObject(constructor:)` binds `static`/`class` ones (called on the metatype); and
112
+ // `_constructSharedObject` builds an instance from the `@JS init` arguments. Each is emitted only
113
+ // when it has something to bind.
114
+ if !instanceFunctions.isEmpty || !instanceProperties.isEmpty {
115
+ emitted.append(
116
+ buildDecorateSharedObjectPhase(
117
+ phase: .prototype, functions: instanceFunctions, properties: instanceProperties,
118
+ typeName: typeName))
119
+ }
120
+ if !staticFunctions.isEmpty || !staticProperties.isEmpty {
97
121
  emitted.append(
98
- buildDecorateSharedObject(functions: functions, properties: properties, typeName: typeName))
122
+ buildDecorateSharedObjectPhase(
123
+ phase: .constructor, functions: staticFunctions, properties: staticProperties,
124
+ typeName: typeName))
99
125
  }
100
126
  if let constructor {
101
127
  emitted.append(constructor.buildConstructor(typeName: typeName))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/expo-modules-macros-plugin",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "license": "MIT",
6
6
  "author": "650 Industries, Inc.",