@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.
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
- The single generated function that decorates the module's JS object. Core supplies the object;
152
- this binds every `@JS func` into it via one inlined `setProperty` closure per function. Mirrors
153
- core's `ObjectDefinition.decorate(object:)`, including its `borrowing` object parameter (it
154
- mutates through the reference without reassigning or taking ownership). Named `_decorateModule`
155
- with the leading-underscore convention for synthesized members the **runtime calls by name**; the
156
- `ExpoModule` suffix names the `@ExpoModule` macro it came from (a shared object's counterpart is
157
- `_decorateSharedObject`).
158
- */
159
- internal func buildDecorateJavaScriptObject(functions: [JSFunction]) -> DeclSyntax {
160
- let body = functions.map { $0.decorateStatements }.joined(separator: "\n")
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 synthesized
46
- // `_decorateModule` rather than described with a `Function(...)` / `AsyncFunction(...)` DSL entry,
47
- // so they're collected here instead of appended to `entries`. Properties still go through
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
- entries.append(contentsOf: buildPropertyEntries(varDecl: varDecl, attribute: attribute))
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` into the module's JS object,
104
- // with the decode-call-encode body inlined into each closure. Only emitted when there are
105
- // functions to bind.
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 buildPropertyEntries(
231
+ private func collectProperties(
232
232
  varDecl: VariableDeclSyntax,
233
233
  attribute: AttributeSyntax
234
- ) -> [String] {
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
- let jsName = jsNameOverride ?? swiftName
243
- return "Property(\"\(jsName)\") { self.\(swiftName) }"
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/expo-modules-macros-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "license": "MIT",
6
6
  "author": "650 Industries, Inc.",