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.
- package/.github/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- 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
|
+
}
|