@expo/expo-modules-macros-plugin 0.2.1 → 0.2.2
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/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Sources/ExpoModulesMacros/{DecorateFunctionBuilder.swift → DecorateModuleBuilder.swift} +120 -19
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +47 -14
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +21 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -23
- package/package.json +1 -1
|
Binary file
|
|
@@ -34,14 +34,6 @@ internal struct JSFunction {
|
|
|
34
34
|
self.isAsync = effectSpecifiers?.asyncSpecifier != nil
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
The `#name` host-function body: a `@JavaScriptActor private func` matching the
|
|
39
|
-
`createFunction` closure shape `(this, arguments) throws -> JavaScriptValue`, threading
|
|
40
|
-
`appContext`/`runtime` in as parameters. It checks arity, decodes each argument by its static
|
|
41
|
-
type — primitives through a direct typed accessor (`asDouble()`, …) on a borrowed
|
|
42
|
-
`JavaScriptUnownedValue`, other types through the
|
|
43
|
-
`T.getDynamicType()` converter — calls `self.<name>(...)`, and converts the result back to JS.
|
|
44
|
-
*/
|
|
45
37
|
/// The decode-call-encode statements that form the host-function body, indented with the given
|
|
46
38
|
/// prefix. Arity guard, then per-argument decode (primitives via a direct typed accessor like
|
|
47
39
|
/// `asDouble()` on a zero-copy `arguments.unownedValue(at:)`, others via `getDynamicType().cast(...)`),
|
|
@@ -147,17 +139,126 @@ internal struct JSFunction {
|
|
|
147
139
|
}
|
|
148
140
|
}
|
|
149
141
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
142
|
+
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
143
|
+
/// `Property(...)` DSL entry, `@ExpoModule` synthesizes a get/set accessor into the module's JS
|
|
144
|
+
/// object inside `_decorateModule`: it builds a descriptor object (`enumerable` + `get`, and `set`
|
|
145
|
+
/// when the property is settable) and installs it with `object.defineProperty(name, descriptor:)`,
|
|
146
|
+
/// mirroring core's `PropertyDefinition.buildDescriptor`. The `get`/`set` host functions are
|
|
147
|
+
/// installed the same way `@JS func`s are — the closure-taking `setProperty(_:)` overload, with the
|
|
148
|
+
/// read/write body inlined into the closure.
|
|
149
|
+
///
|
|
150
|
+
/// The receiver is the module's real `self`, so the getter reads `self.<name>` and the setter writes
|
|
151
|
+
/// `self.<name> = …` directly, ignoring the JS `this`. Decode/encode of the value reuse the same
|
|
152
|
+
/// static-type fast path as functions (primitives through a direct typed accessor / `toJavaScriptValue`,
|
|
153
|
+
/// other types through the `getDynamicType()` converter).
|
|
154
|
+
internal struct JSProperty {
|
|
155
|
+
let swiftName: String
|
|
156
|
+
let jsName: String
|
|
157
|
+
/// The property's value type as written, or `nil` when it couldn't be inferred (no annotation and
|
|
158
|
+
/// no literal default). When `nil` the getter still works (the encode infers from `self.<name>`)
|
|
159
|
+
/// but the setter uses an untyped closure parameter.
|
|
160
|
+
let valueType: String?
|
|
161
|
+
/// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
|
|
162
|
+
/// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
|
|
163
|
+
let isSettable: Bool
|
|
164
|
+
|
|
165
|
+
/// The statements that install this property's accessor on the JS object, indented for the
|
|
166
|
+
/// `_decorateModule` body. Builds a descriptor object (`enumerable` + `get`, and `set` when
|
|
167
|
+
/// settable) via the closure-taking `setProperty(_:)` overload — with the read/write body inlined
|
|
168
|
+
/// into each closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture
|
|
169
|
+
/// matches the function bindings: `self` strong, `appContext` weak + guarded — and, like functions,
|
|
170
|
+
/// the `appContext` capture + guard are omitted from an accessor whose body never references it (a
|
|
171
|
+
/// primitive value, decoded/encoded without the dynamic converter), to avoid the unused-capture
|
|
172
|
+
/// warning. Getter and setter are gated independently.
|
|
173
|
+
var decorateStatements: String {
|
|
174
|
+
let descriptorName = "\(swiftName)Descriptor"
|
|
175
|
+
// A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
|
|
176
|
+
// references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
|
|
177
|
+
// getter, which also doesn't use it.
|
|
178
|
+
let usesAppContext = valueType.map { fastDecodeAccessor(for: $0) == nil } ?? false
|
|
179
|
+
var lines: [String] = []
|
|
180
|
+
|
|
181
|
+
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
182
|
+
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
183
|
+
|
|
184
|
+
// Getter: read `self.<name>` and encode the result back to JS.
|
|
185
|
+
let getEncode: String
|
|
186
|
+
if let valueType, fastDecodeAccessor(for: valueType) != nil {
|
|
187
|
+
getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
|
|
188
|
+
} else if let valueType {
|
|
189
|
+
getEncode =
|
|
190
|
+
"return try \(valueType).getDynamicType().castToJS(self.\(swiftName), appContext: appContext, in: runtime)"
|
|
191
|
+
} else {
|
|
192
|
+
// No known type: fall back to converting whatever `self.<name>` is. This only happens when the
|
|
193
|
+
// declaration has neither an annotation nor a literal default, which is rare for a stored var.
|
|
194
|
+
getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
|
|
195
|
+
}
|
|
196
|
+
lines.append(accessorClosure(descriptorName, "get", usesAppContext: usesAppContext, body: getEncode))
|
|
197
|
+
|
|
198
|
+
// Setter: decode argument 0 by the static type and write `self.<name>`. A typed setter needs a
|
|
199
|
+
// known value type; when the type couldn't be inferred the property is bound getter-only (a
|
|
200
|
+
// settable var with neither an annotation nor a literal default is rare and can't be decoded).
|
|
201
|
+
if isSettable, let valueType {
|
|
202
|
+
let setDecode: String
|
|
203
|
+
if let accessor = fastDecodeAccessor(for: valueType) {
|
|
204
|
+
setDecode = "self.\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
|
|
205
|
+
} else {
|
|
206
|
+
setDecode =
|
|
207
|
+
"self.\(swiftName) = try \(valueType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(valueType)"
|
|
208
|
+
}
|
|
209
|
+
lines.append(
|
|
210
|
+
accessorClosure(descriptorName, "set", usesAppContext: usesAppContext, body: "\(setDecode)\nreturn .undefined"))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
lines.append("object.defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
214
|
+
|
|
215
|
+
return lines
|
|
216
|
+
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
217
|
+
.map { " " + $0 }
|
|
218
|
+
.joined(separator: "\n")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry. Captures `self` strong and, when
|
|
222
|
+
/// `usesAppContext`, `appContext` weak + guarded (matching the function bindings); otherwise the
|
|
223
|
+
/// capture and guard are omitted so a primitive accessor doesn't warn on an unused capture.
|
|
224
|
+
private func accessorClosure(
|
|
225
|
+
_ descriptorName: String, _ key: String, usesAppContext: Bool, body: String
|
|
226
|
+
) -> String {
|
|
227
|
+
// Indent each line of a (possibly multi-line) body to sit one level inside the closure, aligned
|
|
228
|
+
// with the `guard`; a bare `\(body)` interpolation would only indent the first line.
|
|
229
|
+
let indentedBody = body
|
|
230
|
+
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
231
|
+
.map { " \($0)" }
|
|
232
|
+
.joined(separator: "\n")
|
|
233
|
+
if usesAppContext {
|
|
234
|
+
return """
|
|
235
|
+
\(descriptorName).setProperty("\(key)") { [weak appContext, self] this, arguments in
|
|
236
|
+
guard let appContext else {
|
|
237
|
+
throw Exceptions.AppContextLost()
|
|
238
|
+
}
|
|
239
|
+
\(indentedBody)
|
|
240
|
+
}
|
|
241
|
+
"""
|
|
242
|
+
}
|
|
243
|
+
return """
|
|
244
|
+
\(descriptorName).setProperty("\(key)") { [self] this, arguments in
|
|
245
|
+
\(indentedBody)
|
|
246
|
+
}
|
|
247
|
+
"""
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// The single generated function that decorates the module's JS object. Core supplies the object;
|
|
252
|
+
/// this binds every `@JS func` (via an inlined `setProperty` closure) and every `@JS var` (via a
|
|
253
|
+
/// `defineProperty` accessor) into it. Mirrors core's `ObjectDefinition.decorate(object:)`, including
|
|
254
|
+
/// its `borrowing` object parameter (it mutates through the reference without reassigning or taking
|
|
255
|
+
/// ownership). Named `_decorateModule` with the leading-underscore convention for synthesized members
|
|
256
|
+
/// the **runtime calls by name**; the `ExpoModule` suffix names the `@ExpoModule` macro it came from (a
|
|
257
|
+
/// shared object's counterpart is `_decorateSharedObject`).
|
|
258
|
+
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
259
|
+
let functionBody = functions.map { $0.decorateStatements }
|
|
260
|
+
let propertyBody = properties.map { $0.decorateStatements }
|
|
261
|
+
let body = (functionBody + propertyBody).joined(separator: "\n")
|
|
161
262
|
return """
|
|
162
263
|
@JavaScriptActor
|
|
163
264
|
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
@@ -42,11 +42,11 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
42
42
|
let moduleName = jsNameArgument(of: node) ?? classDecl.name.text
|
|
43
43
|
var entries: [String] = ["Name(\"\(moduleName)\")"]
|
|
44
44
|
|
|
45
|
-
// `@JS func`s (sync and async) are bound directly into the JS object by the
|
|
46
|
-
// `_decorateModule` rather than described with a `Function(...)` / `
|
|
47
|
-
// so they're collected here instead of appended to `entries`.
|
|
48
|
-
// the DSL for now.
|
|
45
|
+
// `@JS func`s (sync and async) and `@JS var`s are bound directly into the JS object by the
|
|
46
|
+
// synthesized `_decorateModule` rather than described with a `Function(...)` / `Property(...)`
|
|
47
|
+
// DSL entry, so they're collected here instead of appended to `entries`.
|
|
49
48
|
var functions: [JSFunction] = []
|
|
49
|
+
var properties: [JSProperty] = []
|
|
50
50
|
|
|
51
51
|
for typeName in classListArgument(of: node, label: "classes") {
|
|
52
52
|
entries.append("\(typeName)._synthesizedClassDefinition()")
|
|
@@ -63,7 +63,7 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
63
63
|
|
|
64
64
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
65
65
|
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
66
|
-
|
|
66
|
+
properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -100,11 +100,11 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
100
100
|
"""
|
|
101
101
|
emitted.append(method)
|
|
102
102
|
|
|
103
|
-
// Direct JSI binding: one `_decorateModule` that binds each `@JS func`
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
if !functions.isEmpty {
|
|
107
|
-
emitted.append(buildDecorateJavaScriptObject(functions: functions))
|
|
103
|
+
// Direct JSI binding: one `_decorateModule` that binds each `@JS func` (inlined `setProperty`
|
|
104
|
+
// closure) and each `@JS var` (a `defineProperty` get/set accessor) into the module's JS object.
|
|
105
|
+
// Only emitted when there's at least one member to bind.
|
|
106
|
+
if !functions.isEmpty || !properties.isEmpty {
|
|
107
|
+
emitted.append(buildDecorateJavaScriptObject(functions: functions, properties: properties))
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
return emitted
|
|
@@ -228,19 +228,52 @@ private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
|
228
228
|
|
|
229
229
|
// MARK: - Member builders
|
|
230
230
|
|
|
231
|
-
private func
|
|
231
|
+
private func collectProperties(
|
|
232
232
|
varDecl: VariableDeclSyntax,
|
|
233
233
|
attribute: AttributeSyntax
|
|
234
|
-
) -> [
|
|
234
|
+
) -> [JSProperty] {
|
|
235
235
|
let jsNameOverride = jsNameArgument(of: attribute)
|
|
236
|
+
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
237
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
236
238
|
|
|
237
239
|
return varDecl.bindings.compactMap { binding in
|
|
238
240
|
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
239
241
|
return nil
|
|
240
242
|
}
|
|
241
243
|
let swiftName = ident.identifier.text
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
245
|
+
// when there's none. `nil` falls back to inference at the use site.
|
|
246
|
+
let valueType = binding.typeAnnotation?.type.trimmedDescription
|
|
247
|
+
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
248
|
+
return JSProperty(
|
|
249
|
+
swiftName: swiftName,
|
|
250
|
+
jsName: jsNameOverride ?? swiftName,
|
|
251
|
+
valueType: valueType,
|
|
252
|
+
isSettable: isVar && bindingIsSettable(binding)
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
|
|
258
|
+
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
259
|
+
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
260
|
+
/// observers imply stored storage, which is also settable.
|
|
261
|
+
private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
262
|
+
guard let accessorBlock = binding.accessorBlock else {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
switch accessorBlock.accessors {
|
|
266
|
+
case .accessors(let accessors):
|
|
267
|
+
return accessors.contains { accessor in
|
|
268
|
+
switch accessor.accessorSpecifier.tokenKind {
|
|
269
|
+
case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
|
|
270
|
+
return true
|
|
271
|
+
default:
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
case .getter:
|
|
276
|
+
return false
|
|
244
277
|
}
|
|
245
278
|
}
|
|
246
279
|
|
|
@@ -155,6 +155,27 @@ private func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool
|
|
|
155
155
|
return name.hasSuffix("Actor")
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/// The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
|
|
159
|
+
/// when the expression isn't one of those literals. Used to recover a property's type when it has no
|
|
160
|
+
/// annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
|
|
161
|
+
/// Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
|
|
162
|
+
/// macro can't know (function calls, collection literals, member access) return `nil`.
|
|
163
|
+
internal func inferredLiteralType(of expression: ExprSyntax) -> String? {
|
|
164
|
+
if expression.is(StringLiteralExprSyntax.self) {
|
|
165
|
+
return "String"
|
|
166
|
+
}
|
|
167
|
+
if expression.is(FloatLiteralExprSyntax.self) {
|
|
168
|
+
return "Double"
|
|
169
|
+
}
|
|
170
|
+
if expression.is(IntegerLiteralExprSyntax.self) {
|
|
171
|
+
return "Int"
|
|
172
|
+
}
|
|
173
|
+
if expression.is(BooleanLiteralExprSyntax.self) {
|
|
174
|
+
return "Bool"
|
|
175
|
+
}
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
|
|
158
179
|
extension AttributeListSyntax {
|
|
159
180
|
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
160
181
|
for element in self {
|
|
@@ -453,29 +453,6 @@ private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
|
453
453
|
return false
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
/**
|
|
457
|
-
The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
|
|
458
|
-
when the expression isn't one of those literals. Used to recover a property's type when it has no
|
|
459
|
-
annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
|
|
460
|
-
Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
|
|
461
|
-
macro can't know (function calls, collection literals, member access) return `nil`.
|
|
462
|
-
*/
|
|
463
|
-
private func inferredLiteralType(of expression: ExprSyntax) -> String? {
|
|
464
|
-
if expression.is(StringLiteralExprSyntax.self) {
|
|
465
|
-
return "String"
|
|
466
|
-
}
|
|
467
|
-
if expression.is(FloatLiteralExprSyntax.self) {
|
|
468
|
-
return "Double"
|
|
469
|
-
}
|
|
470
|
-
if expression.is(IntegerLiteralExprSyntax.self) {
|
|
471
|
-
return "Int"
|
|
472
|
-
}
|
|
473
|
-
if expression.is(BooleanLiteralExprSyntax.self) {
|
|
474
|
-
return "Bool"
|
|
475
|
-
}
|
|
476
|
-
return nil
|
|
477
|
-
}
|
|
478
|
-
|
|
479
456
|
/**
|
|
480
457
|
True if the type syntax is optional: `T?`, `T!`, or the spelled-out `Optional<T>`.
|
|
481
458
|
*/
|