@expo/expo-modules-macros-plugin 0.1.0 → 0.2.1

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.
@@ -97,7 +97,9 @@ jobs:
97
97
  run: |
98
98
  version="${{ steps.bump.outputs.version }}"
99
99
  git commit -am "Release $version"
100
- git tag "$version"
100
+ # Use an annotated tag so `git push --follow-tags` actually pushes it
101
+ # (--follow-tags ignores lightweight tags).
102
+ git tag -a "$version" -m "Release $version"
101
103
  git push --follow-tags origin HEAD
102
104
 
103
105
  - name: Create GitHub release
Binary file
@@ -0,0 +1,196 @@
1
+ import SwiftSyntax
2
+
3
+ /**
4
+ A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
5
+ `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call,
6
+ `@ExpoModule` synthesizes a `_decorateModule` that binds each such function into the module's JS object
7
+ via the closure-taking `JavaScriptObject.setProperty(_:)`, with the decode-call-encode body
8
+ inlined into the closure. This omits the `[Any]`/`toTuple` dynamic-call path: every argument is
9
+ decoded individually by its static type.
10
+
11
+ The receiver is the module's real `self` (a module is a singleton instance), so the body calls
12
+ `self.<name>(...)` directly and ignores the JS `this`. An `async` `@JS func` produces an `async`
13
+ closure body and is installed through the async `setProperty(_:)` overload (so JS gets a promise).
14
+ */
15
+ internal struct JSFunction {
16
+ let swiftName: String
17
+ let jsName: String
18
+ let parameters: [FunctionParameterSyntax]
19
+ /// The declared return type as written, or `nil` when the function returns `Void`/nothing.
20
+ let returnType: String?
21
+ let isThrowing: Bool
22
+ let isAsync: Bool
23
+
24
+ init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
25
+ self.swiftName = funcDecl.name.text
26
+ self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
27
+ self.parameters = Array(funcDecl.signature.parameterClause.parameters)
28
+
29
+ let declaredReturnType = funcDecl.signature.returnClause?.type
30
+ self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
31
+
32
+ let effectSpecifiers = funcDecl.signature.effectSpecifiers
33
+ self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
34
+ self.isAsync = effectSpecifiers?.asyncSpecifier != nil
35
+ }
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
+ /// The decode-call-encode statements that form the host-function body, indented with the given
46
+ /// prefix. Arity guard, then per-argument decode (primitives via a direct typed accessor like
47
+ /// `asDouble()` on a zero-copy `arguments.unownedValue(at:)`, others via `getDynamicType().cast(...)`),
48
+ /// the `self.<name>(...)` call, and the
49
+ /// result encode (primitives via `toJavaScriptValue(in:)`, others via `castToJS(...)`).
50
+ private func bodyStatements(indent: String) -> String {
51
+ var lines: [String] = []
52
+
53
+ lines.append(
54
+ """
55
+ guard arguments.count == \(parameters.count) else {
56
+ throw Exception(name: "InvalidArgumentCount", description: "Function '\(jsName)' expects \(parameters.count) argument(s), but got \\(arguments.count)")
57
+ }
58
+ """)
59
+
60
+ var callArguments: [String] = []
61
+ for (index, parameter) in parameters.enumerated() {
62
+ let type = parameter.type.trimmedDescription
63
+
64
+ // Primitives decode through a direct typed accessor (`asDouble()`, etc.) on a borrowed
65
+ // `JavaScriptUnownedValue` — no owning `JavaScriptValue` allocation, no `jsi::Value` copy, no
66
+ // `getDynamicType()` allocation, no `Any` boxing, no force-cast — while still validating and
67
+ // throwing `TypeError` on a mismatch. Other types fall back to the dynamic converter, which
68
+ // needs an owning value, so they index the buffer directly.
69
+ if let accessor = fastDecodeAccessor(for: type) {
70
+ lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
71
+ } else {
72
+ lines.append(
73
+ "let arg\(index) = try \(type).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(type)")
74
+ }
75
+
76
+ let label = parameter.firstName.text
77
+ callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
78
+ }
79
+
80
+ let tryKeyword = (isThrowing || isAsync) ? "try " : ""
81
+ let awaitKeyword = isAsync ? "await " : ""
82
+ let callExpression =
83
+ "\(tryKeyword)\(awaitKeyword)self.\(swiftName)(\(callArguments.joined(separator: ", ")))"
84
+
85
+ if let returnType {
86
+ lines.append("let result = \(callExpression)")
87
+ // Primitives encode through `toJavaScriptValue(in:)` (the typed `JavaScriptRepresentable`
88
+ // conversion) — no `Any`, no dynamic-type allocation. Others go through the dynamic converter.
89
+ if fastDecodeAccessor(for: returnType) != nil {
90
+ lines.append("return result.toJavaScriptValue(in: runtime)")
91
+ } else {
92
+ lines.append("return try \(returnType).getDynamicType().castToJS(result, appContext: appContext, in: runtime)")
93
+ }
94
+ } else {
95
+ lines.append(callExpression)
96
+ lines.append("return .undefined")
97
+ }
98
+
99
+ return lines
100
+ .flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
101
+ .map { indent + $0 }
102
+ .joined(separator: "\n")
103
+ }
104
+
105
+ /// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
106
+ /// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
107
+ /// (which creates the host function under the hood) — no separate named binding. For an `async`
108
+ /// function the body `await`s the call, which selects the async `setProperty` overload (so JS
109
+ /// receives a promise).
110
+ ///
111
+ /// Capture mirrors core's `SyncFunctionDefinition.build`: `self` (the module) is captured
112
+ /// **strong** — the host-function closure is what keeps the native callable alive for as long as
113
+ /// JS can invoke it; its lifetime is bounded by the JS VM's garbage collection of the object.
114
+ /// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
115
+ /// the app context. When no argument or return value goes through the dynamic-type converter the
116
+ /// body never references `appContext`, so the capture and guard are omitted to avoid the
117
+ /// unused-capture warning.
118
+ var decorateStatements: String {
119
+ if usesAppContext {
120
+ return """
121
+ object.setProperty("\(jsName)") { [weak appContext, self] this, arguments in
122
+ guard let appContext else {
123
+ throw Exceptions.AppContextLost()
124
+ }
125
+ \(bodyStatements(indent: " "))
126
+ }
127
+ """
128
+ }
129
+ return """
130
+ object.setProperty("\(jsName)") { [self] this, arguments in
131
+ \(bodyStatements(indent: " "))
132
+ }
133
+ """
134
+ }
135
+
136
+ /// True when the host-function body references `appContext` — i.e. some parameter or the return
137
+ /// type lacks a fast accessor and decodes/encodes through `getDynamicType()`, which threads
138
+ /// `appContext` in.
139
+ private var usesAppContext: Bool {
140
+ if parameters.contains(where: { fastDecodeAccessor(for: $0.type.trimmedDescription) == nil }) {
141
+ return true
142
+ }
143
+ if let returnType, fastDecodeAccessor(for: returnType) == nil {
144
+ return true
145
+ }
146
+ return false
147
+ }
148
+ }
149
+
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")
161
+ return """
162
+ @JavaScriptActor
163
+ public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
164
+ \(raw: body)
165
+ }
166
+ """
167
+ }
168
+
169
+ /// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly,
170
+ /// bypassing the dynamic-type converter (`asDouble()` for `Double`, etc.). Returns `nil` for
171
+ /// types without a dedicated accessor — arrays, records, optionals, shared objects, other numeric
172
+ /// widths — which decode through `getDynamicType().cast(...)`.
173
+ private func fastDecodeAccessor(for type: String) -> String? {
174
+ switch type {
175
+ case "Bool":
176
+ return "asBool"
177
+ case "Int":
178
+ return "asInt"
179
+ case "Double":
180
+ return "asDouble"
181
+ case "String":
182
+ return "asString"
183
+ default:
184
+ return nil
185
+ }
186
+ }
187
+
188
+ /// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
189
+ /// nothing JS-visible, so the binding returns `.undefined`.
190
+ private func isVoidType(_ type: TypeSyntax?) -> Bool {
191
+ guard let type else {
192
+ return true
193
+ }
194
+ let text = type.trimmedDescription
195
+ return text == "Void" || text == "()"
196
+ }
@@ -42,6 +42,12 @@ 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.
49
+ var functions: [JSFunction] = []
50
+
45
51
  for typeName in classListArgument(of: node, label: "classes") {
46
52
  entries.append("\(typeName)._synthesizedClassDefinition()")
47
53
  }
@@ -51,7 +57,7 @@ public struct ExpoModuleMacro: MemberMacro {
51
57
 
52
58
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
53
59
  let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
54
- entries.append(buildFunctionEntry(funcDecl: funcDecl, attribute: attribute))
60
+ functions.append(JSFunction(funcDecl: funcDecl, attribute: attribute))
55
61
  continue
56
62
  }
57
63
 
@@ -94,6 +100,13 @@ public struct ExpoModuleMacro: MemberMacro {
94
100
  """
95
101
  emitted.append(method)
96
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))
108
+ }
109
+
97
110
  return emitted
98
111
  }
99
112
  }
@@ -215,17 +228,6 @@ private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
215
228
 
216
229
  // MARK: - Member builders
217
230
 
218
- private func buildFunctionEntry(
219
- funcDecl: FunctionDeclSyntax,
220
- attribute: AttributeSyntax
221
- ) -> String {
222
- let swiftName = funcDecl.name.text
223
- let jsName = jsNameArgument(of: attribute) ?? swiftName
224
- let isAsync = funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil
225
- let dslEntry = isAsync ? "AsyncFunction" : "Function"
226
- return "\(dslEntry)(\"\(jsName)\", \(swiftName))"
227
- }
228
-
229
231
  private func buildPropertyEntries(
230
232
  varDecl: VariableDeclSyntax,
231
233
  attribute: AttributeSyntax
@@ -1,29 +1,93 @@
1
1
  import SwiftSyntax
2
2
  import SwiftSyntaxMacros
3
3
 
4
- /**
5
- Marker macro applied to module / shared-object members that should be exposed to JavaScript.
6
- Expands to nothing on its own; `@ExpoModule` and `@SharedObject` discover declarations
7
- carrying this attribute and generate the corresponding `Function` / `AsyncFunction` /
8
- `Property` / `Constructor` registrations.
9
-
10
- Usage:
11
-
12
- @JS
13
- func greet(name: String) -> String { ... }
14
-
15
- @JS("doWork")
16
- func performWork() async throws { ... }
17
-
18
- @JS
19
- var status: String { "ok" }
20
- */
4
+ /// Marker macro applied to module / shared-object members that should be exposed to JavaScript.
5
+ /// `@ExpoModule` and `@SharedObject` discover declarations carrying this attribute and generate the
6
+ /// corresponding `Function` / `AsyncFunction` / `Property` / `Constructor` registrations; that part
7
+ /// of the expansion lives in those macros.
8
+ ///
9
+ /// On its own, `@JS` emits one thing: a never-called peer that asserts every type crossing the JS
10
+ /// boundary is JS-convertible. Because it's a **peer** of the marked member, a non-conforming type
11
+ /// produces a compile error located on the user's own `@JS` declaration rather than on the enclosing
12
+ /// `@ExpoModule`. The assertion mechanism itself is shared (see `typeConformanceAssertion`); `@JS`
13
+ /// only supplies the boundary types it reads off the declaration.
14
+ ///
15
+ /// Usage:
16
+ ///
17
+ /// @JS
18
+ /// func greet(name: String) -> String { ... }
19
+ ///
20
+ /// @JS("doWork")
21
+ /// func performWork() async throws { ... }
22
+ ///
23
+ /// @JS
24
+ /// var status: String { "ok" }
21
25
  public struct JSMacro: PeerMacro {
22
26
  public static func expansion(
23
27
  of node: AttributeSyntax,
24
28
  providingPeersOf declaration: some DeclSyntaxProtocol,
25
29
  in context: some MacroExpansionContext
26
30
  ) throws -> [DeclSyntax] {
27
- return []
31
+ guard let member = boundaryMember(of: declaration),
32
+ let assertion = typeConformanceAssertion(
33
+ for: ConformanceAssertion(name: member.name, types: member.types),
34
+ isStatic: member.isStatic
35
+ ) else {
36
+ return []
37
+ }
38
+ return [assertion]
39
+ }
40
+ }
41
+
42
+ /// What an assertion peer needs about the `@JS` member it sits beside: a name (to keep the peer
43
+ /// unique among siblings), the types crossing the JS boundary, and whether the member is type-level.
44
+ private struct BoundaryMember {
45
+ let name: String
46
+ /// Boundary types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
47
+ /// conditional conformances transitively constrain the elements.
48
+ let types: [String]
49
+ /// True for `static`/`class` members, so the peer is emitted in the same metatype context.
50
+ let isStatic: Bool
51
+ }
52
+
53
+ /// Reads the boundary member off a `@JS` declaration. A function contributes its parameter types
54
+ /// plus the return type (when non-Void); a property contributes its declared type. Returns `nil` for
55
+ /// declaration kinds `@JS` doesn't read types from, or a property whose type isn't spelled out (a
56
+ /// syntactic macro can't recover it), so no assertion is emitted there.
57
+ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
58
+ if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
59
+ var types = funcDecl.signature.parameterClause.parameters.map { $0.type.trimmedDescription }
60
+ if let returnType = funcDecl.signature.returnClause?.type, !isVoidType(returnType) {
61
+ types.append(returnType.trimmedDescription)
62
+ }
63
+ return BoundaryMember(name: funcDecl.name.text, types: types, isStatic: isTypeLevel(funcDecl.modifiers))
28
64
  }
65
+
66
+ if let varDecl = declaration.as(VariableDeclSyntax.self),
67
+ let binding = varDecl.bindings.first,
68
+ let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
69
+ let type = binding.typeAnnotation?.type {
70
+ return BoundaryMember(
71
+ name: identifier.identifier.text,
72
+ types: [type.trimmedDescription],
73
+ isStatic: isTypeLevel(varDecl.modifiers)
74
+ )
75
+ }
76
+
77
+ return nil
78
+ }
79
+
80
+ /// True when the modifiers make the member type-level (`static` or `class`), so its assertion peer
81
+ /// must be emitted in the same metatype context rather than as an instance member.
82
+ private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
83
+ return modifiers.contains {
84
+ $0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
85
+ }
86
+ }
87
+
88
+ /// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
89
+ /// no conformance assertion. (A missing return clause never reaches here: `returnClause` is `nil`.)
90
+ private func isVoidType(_ type: TypeSyntax) -> Bool {
91
+ let text = type.trimmedDescription
92
+ return text == "Void" || text == "()"
29
93
  }
@@ -59,6 +59,18 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
59
59
  let existingInitLabels = initializerParameterLabels(of: declaration)
60
60
 
61
61
  var members: [DeclSyntax] = []
62
+
63
+ // A single never-called member that makes the compiler verify each property type is
64
+ // JS-convertible (the conversions below go through its dynamic-type API). Each property keeps its
65
+ // own named assertion inside, so the compiler's conformance diagnostic names the offending
66
+ // property (see `typeConformanceAssertions`). Emitted first so that, for a non-conforming type,
67
+ // this clear "requires that '…' conform to '…'" error is reported ahead of the noisier
68
+ // "no member 'getDynamicType'" errors from the conversion code below.
69
+ let assertions = properties.map { ConformanceAssertion(name: $0.name, types: [$0.type]) }
70
+ if let assertionMember = typeConformanceAssertions(for: assertions) {
71
+ members.append(assertionMember)
72
+ }
73
+
62
74
  if !existingInitLabels.contains([]) {
63
75
  if let defaultInit = defaultInit(properties: properties, isClass: isClass) {
64
76
  members.append(defaultInit)
@@ -0,0 +1,105 @@
1
+ import SwiftSyntax
2
+
3
+ /// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
4
+ /// eventual rename (this is a placeholder name) is a single edit, and shared by every macro that
5
+ /// asserts the conformance (`@JS`, `@Record`, …).
6
+ internal let jsConvertibleProtocolName = "AnyArgument"
7
+
8
+ /// Types we never assert because they're statically known to conform and never reach the dynamic
9
+ /// converter: the JS primitives. Asserting them would only add noise to the expansion. Kept here
10
+ /// (rather than reusing the decode-path's `fastDecodeAccessor`) because "known-to-conform" is a
11
+ /// concept that belongs with the assertion logic, not with how a value is decoded.
12
+ private let knownConformingPrimitives: Set<String> = ["Bool", "Int", "Double", "String"]
13
+
14
+ /// One member's worth of conformance assertion: a name (the member it stands for) and the declared
15
+ /// types crossing the JS boundary for it. The name surfaces verbatim in the compiler's conformance
16
+ /// diagnostic ("local function '<name>' requires that '<Type>' conform to …"), so it identifies the
17
+ /// offending member in the error message on top of the location pointing at the user's declaration.
18
+ internal struct ConformanceAssertion {
19
+ let name: String
20
+ /// Declared types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
21
+ /// conditional conformances transitively constrain the elements.
22
+ let types: [String]
23
+ }
24
+
25
+ /// A single conformance-assertion peer for a `@JS` member: a never-called `private func` whose body
26
+ /// statically asserts the member's boundary types conform. The assertion is compile-time only — the
27
+ /// function is never invoked, but Swift still type-checks its body, so a non-conforming type becomes
28
+ /// a compile error. Emitted as a **peer** of the user's declaration, so that error lands on the
29
+ /// user's own member rather than on the enclosing macro.
30
+ ///
31
+ /// `isStatic` makes the peer `static`, mirroring a `static`/`class` member so it's emitted in the
32
+ /// right metatype context (a peer of a type-level member can't be an instance method). `class func`
33
+ /// members collapse to `static` here too: the peer is private and never called or overridden, so
34
+ /// `static` is always sufficient.
35
+ ///
36
+ /// Returns `nil` when nothing is left to assert (every type was a known-conforming primitive, or the
37
+ /// list was empty), so the caller emits nothing in that case.
38
+ internal func typeConformanceAssertion(for assertion: ConformanceAssertion, isStatic: Bool) -> DeclSyntax? {
39
+ guard let body = conformanceAssertionBody(assertion) else {
40
+ return nil
41
+ }
42
+ let staticKeyword = isStatic ? "static " : ""
43
+ return """
44
+ private \(raw: staticKeyword)func _assertTypesConformance_\(raw: assertion.name)() {
45
+ \(raw: body)
46
+ }
47
+ """
48
+ }
49
+
50
+ /// One conformance-assertion peer covering several members' assertions at once — used by `@Record`,
51
+ /// which folds every property into a single `_assertTypesConformance()` rather than emitting a peer
52
+ /// per property. Each assertion keeps its own named nested helper, so the per-member naming in the
53
+ /// diagnostic is preserved even though they share one peer.
54
+ ///
55
+ /// Returns `nil` when no assertion has anything left to verify (all primitives / empty), so the
56
+ /// caller emits nothing.
57
+ internal func typeConformanceAssertions(for assertions: [ConformanceAssertion]) -> DeclSyntax? {
58
+ let bodies = assertions.compactMap(conformanceAssertionBody)
59
+ guard !bodies.isEmpty else {
60
+ return nil
61
+ }
62
+ return """
63
+ private func _assertTypesConformance() {
64
+ \(raw: bodies.joined(separator: "\n"))
65
+ }
66
+ """
67
+ }
68
+
69
+ /// The assertion's body fragment: a nested generic helper named after the member, plus one call per
70
+ /// distinct non-primitive type. Nesting the helper keeps the constraint entirely local — no shared
71
+ /// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
72
+ /// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
73
+ /// a known-conforming primitive or the list was empty.
74
+ private func conformanceAssertionBody(_ assertion: ConformanceAssertion) -> String? {
75
+ // Unwrap top-level optionals to the core type, then dedup so each type is asserted once even when
76
+ // it appears more than once; skip known primitives.
77
+ var seen: Set<String> = []
78
+ var distinct: [String] = []
79
+ for type in assertion.types.map(unwrappedOptional)
80
+ where !knownConformingPrimitives.contains(type) && seen.insert(type).inserted {
81
+ distinct.append(type)
82
+ }
83
+ guard !distinct.isEmpty else {
84
+ return nil
85
+ }
86
+
87
+ var lines = ["func \(assertion.name)<T: \(jsConvertibleProtocolName)>(_: T.Type) {}"]
88
+ lines.append(contentsOf: distinct.map { "\(assertion.name)(\($0).self)" })
89
+ return lines.joined(separator: "\n")
90
+ }
91
+
92
+ /// Strips every trailing optional marker (`?`/`!`) so the assertion targets the core wrapped type.
93
+ /// `Optional<W>: AnyArgument` holds exactly when `W: AnyArgument` (and `T!` is just `T?`), so each
94
+ /// layer is conformance-equivalent to its wrapped type. Asserting the core gives a cleaner diagnostic
95
+ /// (the direct `requires that '<type>' conform`, not the conditional-conformance phrasing through
96
+ /// `Optional`) and sidesteps that `T!.self` is invalid in metatype position. Only *trailing* markers
97
+ /// are stripped, so `[Int?]` keeps its inner `?`; a longhand `Optional<W>` isn't peeled but is still
98
+ /// asserted whole, which remains correct.
99
+ private func unwrappedOptional(_ type: String) -> String {
100
+ var result = Substring(type)
101
+ while result.hasSuffix("?") || result.hasSuffix("!") {
102
+ result = result.dropLast()
103
+ }
104
+ return String(result)
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/expo-modules-macros-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "license": "MIT",
6
6
  "author": "650 Industries, Inc.",