expo-modules-macros 0.0.1 → 0.14.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.
Files changed (46) hide show
  1. package/.github/resources/expo-modules-macros.svg +23 -0
  2. package/.github/workflows/publish.yml +122 -0
  3. package/.github/workflows/swift.yml +68 -0
  4. package/AGENTS.md +59 -0
  5. package/CLAUDE.md +1 -0
  6. package/LICENSE +21 -0
  7. package/README.md +119 -0
  8. package/apple/ExpoModulesMacros +0 -0
  9. package/apple/Package.resolved +15 -0
  10. package/apple/Package.swift +61 -0
  11. package/apple/PodTests/StubTest.swift +5 -0
  12. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
  13. package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
  14. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
  15. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
  16. package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
  17. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
  18. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
  19. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
  20. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
  21. package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
  22. package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
  23. package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
  24. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
  25. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
  26. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
  27. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
  28. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
  29. package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
  30. package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
  31. package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
  32. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
  33. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
  34. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
  35. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
  36. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
  37. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
  38. package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
  39. package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
  40. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
  41. package/apple/build.js +138 -0
  42. package/build/index.d.ts +51 -0
  43. package/build/index.js +153 -0
  44. package/build/types.d.ts +264 -0
  45. package/build/types.js +15 -0
  46. package/package.json +22 -5
@@ -0,0 +1,444 @@
1
+ import SwiftSyntax
2
+
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(object:)` / `_decorateSharedObject(prototype:)`) 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 dynamic-call path entirely:
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` 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.
16
+ internal struct JSFunction {
17
+ let swiftName: String
18
+ let jsName: String
19
+ let parameters: [FunctionParameterSyntax]
20
+ /// The declared return type as written, or `nil` when the function returns `Void`/nothing.
21
+ let returnType: String?
22
+ let isThrowing: Bool
23
+ let isAsync: Bool
24
+
25
+ init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
26
+ self.swiftName = funcDecl.name.text
27
+ self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
28
+ self.parameters = Array(funcDecl.signature.parameterClause.parameters)
29
+
30
+ let declaredReturnType = funcDecl.signature.returnClause?.type
31
+ self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
32
+
33
+ let effectSpecifiers = funcDecl.signature.effectSpecifiers
34
+ self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
35
+ self.isAsync = effectSpecifiers?.asyncSpecifier != nil
36
+ }
37
+
38
+ /// The number of leading parameters that must always be supplied: the total minus the maximal
39
+ /// trailing run of *omittable* parameters (each having a default value or an optional type). A
40
+ /// non-omittable parameter part-way through stops the run, since arguments are positional — a
41
+ /// required parameter after an omittable one forces the earlier one to be supplied too.
42
+ private var requiredArgumentCount: Int {
43
+ var required = parameters.count
44
+ for parameter in parameters.reversed() {
45
+ guard isOmittable(parameter) else {
46
+ break
47
+ }
48
+ required -= 1
49
+ }
50
+ return required
51
+ }
52
+
53
+ /// The decode-call-encode statements that form the host-function body, indented with the given
54
+ /// prefix. Receiver unwrap (shared objects only), then an arity guard (an exact check when every
55
+ /// parameter is required, otherwise a range check) throwing `Exceptions.ArgumentsRangeMismatch`;
56
+ /// then the decode of the always-present required prefix via `JavaScriptDecodable.decode` on a
57
+ /// zero-copy `arguments.unownedValue(at:)`; then the call and result encode via
58
+ /// `JavaScriptEncodable.encode`. When a trailing run of parameters is omittable the call branches
59
+ /// on `arguments.count`, decoding only the slots that branch actually has — `unownedValue(at:)` is
60
+ /// unchecked, so a slot the caller didn't pass is never indexed.
61
+ private func bodyStatements(receiver: Receiver, indent: String) -> String {
62
+ let required = requiredArgumentCount
63
+ let maximum = parameters.count
64
+ var lines: [String] = []
65
+
66
+ if let unwrap = receiver.unwrapStatement {
67
+ lines.append(unwrap)
68
+ }
69
+
70
+ if required == maximum {
71
+ lines.append(
72
+ """
73
+ guard arguments.count == \(maximum) else {
74
+ throw Exceptions.ArgumentsRangeMismatch((functionName: "\(jsName)", received: arguments.count, required: \(required), maximum: \(maximum)))
75
+ }
76
+ """)
77
+ } else {
78
+ lines.append(
79
+ """
80
+ guard arguments.count >= \(required) && arguments.count <= \(maximum) else {
81
+ throw Exceptions.ArgumentsRangeMismatch((functionName: "\(jsName)", received: arguments.count, required: \(required), maximum: \(maximum)))
82
+ }
83
+ """)
84
+ }
85
+
86
+ // Decode the required prefix once — these slots are present in every accepted arity, so the
87
+ // decode is shared rather than repeated per branch.
88
+ for index in 0..<required {
89
+ lines.append(decodeStatement(at: index))
90
+ }
91
+
92
+ if required == maximum {
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("}")
118
+ } else {
119
+ // One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
120
+ // trailing slot before the call, so this is a `switch` statement (not an expression): a
121
+ // value-returning function declares `result` up front and each branch assigns it.
122
+ if let returnType {
123
+ lines.append("let result: \(expressionType(returnType))")
124
+ }
125
+ lines.append("switch arguments.count {")
126
+ for arity in required...maximum {
127
+ let label = arity == maximum ? "default:" : "case \(arity):"
128
+ lines.append(label)
129
+ for index in required..<arity {
130
+ lines.append(" " + decodeStatement(at: index))
131
+ }
132
+ let assignment = returnType != nil ? "result = " : ""
133
+ lines.append(" \(assignment)\(callExpression(receiver: receiver, arity: arity))")
134
+ }
135
+ lines.append("}")
136
+ lines.append(contentsOf: encodeResultLines())
137
+ }
138
+
139
+ return lines
140
+ .flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
141
+ .map { indent + $0 }
142
+ .joined(separator: "\n")
143
+ }
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
+
163
+ /// `let arg<index> = …` decoding the slot at `index` by its static type through
164
+ /// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
165
+ /// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
166
+ /// primitive's `decode` is `@inlinable` and lowers to the same direct accessor a hand-rolled fast
167
+ /// path would use.
168
+ private func decodeStatement(at index: Int) -> String {
169
+ let type = parameters[index].type.trimmedDescription
170
+ return "let arg\(index) = try \(decodeCall(type, from: "arguments.unownedValue(at: \(index))"))"
171
+ }
172
+
173
+ /// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
174
+ /// `arg<i>`; a trailing optional-without-default slot that this arity omits is passed `nil`; a
175
+ /// trailing defaulted slot that this arity omits is dropped entirely so Swift applies its default.
176
+ private func callExpression(receiver: Receiver, arity: Int) -> String {
177
+ var callArguments: [String] = []
178
+ for (index, parameter) in parameters.enumerated() {
179
+ let label = parameter.firstName.text
180
+ let value: String?
181
+ if index < arity {
182
+ value = "arg\(index)"
183
+ } else if hasDefaultValue(parameter) {
184
+ // Omitted defaulted slot: drop it from the call so Swift fills in the default.
185
+ value = nil
186
+ } else {
187
+ // Omitted optional-without-default slot: pass `nil`.
188
+ value = "nil"
189
+ }
190
+ guard let value else {
191
+ continue
192
+ }
193
+ callArguments.append(label == "_" ? value : "\(label): \(value)")
194
+ }
195
+ // `try` only for a throwing function. An `async` call that doesn't throw must not get one, or the
196
+ // expansion warns that no throwing call occurs within the `try` expression.
197
+ let tryKeyword = isThrowing ? "try " : ""
198
+ let awaitKeyword = isAsync ? "await " : ""
199
+ return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments.joined(separator: ", ")))"
200
+ }
201
+
202
+ /// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
203
+ /// `let result = <callee>.f(...)` then the return encode (or the no-return `<callee>.f(...)` +
204
+ /// `.undefined`).
205
+ private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
206
+ var lines: [String] = []
207
+ for index in decodingFrom..<arity {
208
+ lines.append(decodeStatement(at: index))
209
+ }
210
+ if returnType != nil {
211
+ lines.append("let result = \(callExpression(receiver: receiver, arity: arity))")
212
+ lines.append(contentsOf: encodeResultLines())
213
+ } else {
214
+ lines.append(callExpression(receiver: receiver, arity: arity))
215
+ lines.append("return .undefined")
216
+ }
217
+ return lines
218
+ }
219
+
220
+ /// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode`, the same
221
+ /// for every type. Unlike the decode side there's no primitive fast path: `encode` produces the same
222
+ /// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
223
+ /// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
224
+ /// catchable error is the right behavior, and matches how non-primitive integers already encode. A
225
+ /// no-return function returns `.undefined` instead.
226
+ ///
227
+ /// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
228
+ /// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
229
+ /// object, array, or typed array) off the JS thread races the engine's garbage collector and
230
+ /// corrupts the heap. `runtime.execute` hops the encode onto the JS thread, running inline when it is
231
+ /// already there (the common case, where the body never truly suspended), so synchronous functions —
232
+ /// which always run on the JS thread — keep encoding directly without the wrapper.
233
+ private func encodeResultLines() -> [String] {
234
+ guard let returnType else {
235
+ return ["return .undefined"]
236
+ }
237
+ let encode = "try \(expressionType(returnType)).encode(result, in: runtime)"
238
+ if isAsync {
239
+ return [
240
+ "return try await runtime.execute {",
241
+ " return \(encode)",
242
+ "}",
243
+ ]
244
+ }
245
+ return ["return \(encode)"]
246
+ }
247
+
248
+ /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
249
+ /// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
250
+ /// (which creates the host function under the hood) — no separate named binding. For an `async`
251
+ /// function the closure is the synchronous decode phase and returns the async body, which selects
252
+ /// the async `setProperty` overload (so JS receives a promise).
253
+ ///
254
+ /// A module captures its `self` **strong** —
255
+ /// the host-function closure is what keeps the native callable alive for as long as JS can invoke
256
+ /// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
257
+ /// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
258
+ func decorateStatements(object: String, receiver: Receiver) -> String {
259
+ // Every `@JS` binding hands `this` in as a borrowed `JavaScriptUnownedValue` instead of
260
+ // allocating an owning `JavaScriptValue` and forming its `weak`-runtime reference on every call,
261
+ // and consumes the arguments buffer — sync and async closures share one parameter shape and
262
+ // differ only in what they return. A module ignores `this`; a shared object unwraps it (still
263
+ // borrowed) — an async binding does so in its synchronous decode phase, before the borrow ends.
264
+ // The parameter list is parenthesized and fully typed, since Swift rejects a type annotation on
265
+ // a shorthand `{ [capture] name, name in }` parameter; for a sync binding the explicit
266
+ // `JavaScriptUnownedValue` also selects the (otherwise `@_disfavoredOverload`) unowned-`this`
267
+ // overload.
268
+ let captures = receiver.captureClause
269
+ let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
270
+
271
+ return """
272
+ \(object).setProperty("\(jsName)") { \(captures)\(parameters) in
273
+ \(bodyStatements(receiver: receiver, indent: " "))
274
+ }
275
+ """
276
+ }
277
+ }
278
+
279
+ /// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
280
+ /// `Property(...)` DSL entry, the enclosing macro synthesizes a get/set accessor into the JS object
281
+ /// inside its decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`): it builds a descriptor object
282
+ /// (`enumerable` + `get`, and `set` when the property is settable) and installs it with
283
+ /// `object.defineProperty(name, descriptor:)`.
284
+ /// The `get`/`set` host functions are installed the same way `@JS func`s are — the closure-taking
285
+ /// `setProperty(_:)` overload, with the read/write body inlined into the closure.
286
+ ///
287
+ /// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
288
+ /// unwrapped from the JS `this` for a shared object. The getter reads `<callee>.<name>` and the setter
289
+ /// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
290
+ /// `JavaScriptEncodable.encode`, the same uniform path as functions.
291
+ internal struct JSProperty {
292
+ let swiftName: String
293
+ let jsName: String
294
+ /// The property's value type as written, or `nil` when it couldn't be inferred (no annotation and
295
+ /// no literal default). When `nil` the getter still works (the encode infers from `self.<name>`)
296
+ /// but the setter uses an untyped closure parameter.
297
+ let valueType: String?
298
+ /// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
299
+ /// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
300
+ let isSettable: Bool
301
+
302
+ /// The statements that install this property's accessor on the JS object, indented for the
303
+ /// decorator body. Builds a descriptor object (`enumerable` + `get`, and `set` when settable) via
304
+ /// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
305
+ /// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
306
+ /// function bindings: a module captures `self` strong, a shared object captures nothing of the
307
+ /// instance. Getter and setter are gated independently.
308
+ func decorateStatements(object: String, receiver: Receiver) -> String {
309
+ let descriptorName = "\(swiftName)Descriptor"
310
+ let callee = receiver.callee
311
+ // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
312
+ // reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
313
+ // they take the borrowed unowned `this`.
314
+ let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
315
+ var lines: [String] = []
316
+
317
+ lines.append("let \(descriptorName) = runtime.createObject()")
318
+ lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
319
+
320
+ // Getter: read `<callee>.<name>` and encode the result back to JS through `encode`. When the value
321
+ // type couldn't be inferred (no annotation and no literal default, rare for a stored var) there's
322
+ // no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
323
+ let getEncode: String
324
+ if let valueType {
325
+ getEncode = "return try \(expressionType(valueType)).encode(\(callee).\(swiftName), in: runtime)"
326
+ } else {
327
+ getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
328
+ }
329
+ lines.append(
330
+ accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)"))
331
+
332
+ // Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
333
+ // typed setter needs a known value type; when the type couldn't be inferred the property is bound
334
+ // getter-only (a settable var with neither an annotation nor a literal default is rare and can't
335
+ // be decoded).
336
+ if isSettable, let valueType {
337
+ let setDecode = "\(callee).\(swiftName) = try \(decodeCall(valueType, from: "arguments.unownedValue(at: 0)"))"
338
+ lines.append(
339
+ accessorClosure(
340
+ descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
341
+ }
342
+
343
+ lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
344
+
345
+ return lines
346
+ .flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
347
+ .map { " " + $0 }
348
+ .joined(separator: "\n")
349
+ }
350
+
351
+ /// One `descriptor.setProperty("get"/"set") { … }` accessor entry. The capture list follows the
352
+ /// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
353
+ private func accessorClosure(
354
+ _ descriptorName: String, _ key: String, receiver: Receiver, body: String
355
+ ) -> String {
356
+ let captures = receiver.captureClause
357
+ // Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
358
+ // `\(body)` interpolation would only indent the first line.
359
+ let indentedBody = body
360
+ .split(separator: "\n", omittingEmptySubsequences: false)
361
+ .map { " \($0)" }
362
+ .joined(separator: "\n")
363
+ // Property `get`/`set` accessors are always synchronous, so they bind through the unowned-`this`
364
+ // `setProperty` overload like sync functions. The parameter list is parenthesized and fully typed
365
+ // because Swift rejects a type annotation on a shorthand closure parameter; the explicit
366
+ // `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
367
+ // a shared object unwraps it in the body.
368
+ let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
369
+ return """
370
+ \(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
371
+ \(indentedBody)
372
+ }
373
+ """
374
+ }
375
+ }
376
+
377
+ /// The body shared by every decorator phase: every `@JS func` bound via an inlined `setProperty`
378
+ /// closure and every `@JS var` via a `defineProperty` accessor, joined for the function body. `object`
379
+ /// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
380
+ /// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
381
+ /// metatype for a static member).
382
+ private func decorateBody(
383
+ functions: [JSFunction], properties: [JSProperty], object: String, receiver: Receiver
384
+ ) -> String {
385
+ let functionBody = functions.map { $0.decorateStatements(object: object, receiver: receiver) }
386
+ let propertyBody = properties.map { $0.decorateStatements(object: object, receiver: receiver) }
387
+ return (functionBody + propertyBody).joined(separator: "\n")
388
+ }
389
+
390
+ /// The module decorator: a `_decorateModule(object:)` that binds every `@JS func` (via an inlined
391
+ /// `setProperty` closure) and every `@JS var` (via a `defineProperty` accessor) into the module's own
392
+ /// JS object. Core supplies the object; the bindings call into the module `self`. It's an *instance*
393
+ /// method (a module is a singleton, satisfying the `AnyModule` requirement of the same name), with a
394
+ /// `borrowing` object parameter (it mutates through the reference without reassigning or taking
395
+ /// ownership). Only emitted when there's at least
396
+ /// one member to bind. Uses the shared body generation with the module `object:` phase and `self`
397
+ /// receiver; the shared-object counterpart is `buildDecorateSharedObjectPhase`.
398
+ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
399
+ let body = decorateBody(
400
+ functions: functions, properties: properties, object: Phase.object.rawValue, receiver: .module)
401
+ return """
402
+ @JavaScriptActor
403
+ public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
404
+ \(raw: body)
405
+ }
406
+ """
407
+ }
408
+
409
+ /// A shared-object decorator phase, a label overload of `_decorateSharedObject` overriding the matching
410
+ /// `open class func` on the `SharedObject` base so core can dispatch through the concrete type's
411
+ /// metatype. The `prototype:` overload binds the type's instance `@JS func`/`var` members onto the class
412
+ /// prototype (the original hook, unchanged); the `constructor:` overload binds the `static`/`class` ones
413
+ /// onto the constructor function itself (a new, additive overload, so introducing it isn't a breaking
414
+ /// core change). Both are *static* (a shared object has no singleton `self`). An instance binding
415
+ /// recovers its typed receiver from the JS `this` per call (`SharedObject.native(from:as:)`); a static
416
+ /// binding calls the Swift member on the type and ignores `this` (which, on the static side, is the
417
+ /// constructor). The constructor *object* the static members decorate is distinct from the `@JS init`,
418
+ /// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
419
+ /// is emitted only when the type has a member for it.
420
+ internal func buildDecorateSharedObjectPhase(
421
+ phase: Phase, functions: [JSFunction], properties: [JSProperty], typeName: String
422
+ ) -> DeclSyntax {
423
+ let receiver: Receiver = phase == .constructor
424
+ ? .staticMember(typeName: typeName)
425
+ : .sharedObject(typeName: typeName)
426
+ let body = decorateBody(
427
+ functions: functions, properties: properties, object: phase.rawValue, receiver: receiver)
428
+ return """
429
+ @JavaScriptActor
430
+ public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
431
+ \(raw: body)
432
+ }
433
+ """
434
+ }
435
+
436
+ /// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
437
+ /// nothing JS-visible, so the binding returns `.undefined`.
438
+ private func isVoidType(_ type: TypeSyntax?) -> Bool {
439
+ guard let type else {
440
+ return true
441
+ }
442
+ let text = type.trimmedDescription
443
+ return text == "Void" || text == "()"
444
+ }