expo-modules-macros 0.0.1 → 0.15.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 +389 -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,433 @@
1
+ import SwiftSyntax
2
+
3
+ /**
4
+ Reads the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork".
5
+ Returns nil if the attribute has no arguments or the first argument is not a string literal.
6
+ */
7
+ internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
8
+ guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
9
+ let first = args.first,
10
+ let str = first.expression.as(StringLiteralExprSyntax.self),
11
+ let segment = str.segments.first?.as(StringSegmentSyntax.self) else {
12
+ return nil
13
+ }
14
+ return segment.content.text
15
+ }
16
+
17
+ /**
18
+ Reads a labeled boolean-literal argument of an attribute, e.g. `@Event(sync: true)` -> true.
19
+ Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
20
+ */
21
+ internal func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool? {
22
+ guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
23
+ return nil
24
+ }
25
+ for arg in args where arg.label?.text == label {
26
+ guard let literal = arg.expression.as(BooleanLiteralExprSyntax.self) else {
27
+ return nil
28
+ }
29
+ return literal.literal.tokenKind == .keyword(.true)
30
+ }
31
+ return nil
32
+ }
33
+
34
+ /// True if the attribute lists the given `JSOptions` member, e.g. `@JS(.concurrent)` or
35
+ /// `@JS("name", [.concurrent])` -> true for "concurrent". Options are written as member-access
36
+ /// expressions (`.concurrent`), optionally inside an array literal when more than one is combined, so
37
+ /// both spellings are scanned. The leading string literal, when present, is the JS name and is skipped.
38
+ internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
39
+ guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
40
+ return false
41
+ }
42
+ for arg in args {
43
+ if namesOption(arg.expression, option) {
44
+ return true
45
+ }
46
+ if let array = arg.expression.as(ArrayExprSyntax.self),
47
+ array.elements.contains(where: { namesOption($0.expression, option) }) {
48
+ return true
49
+ }
50
+ }
51
+ return false
52
+ }
53
+
54
+ /// True when the expression is the member access `.<option>` (or `JSOptions.<option>`).
55
+ private func namesOption(_ expression: ExprSyntax, _ option: String) -> Bool {
56
+ guard let member = expression.as(MemberAccessExprSyntax.self) else {
57
+ return false
58
+ }
59
+ return member.declName.baseName.text == option
60
+ }
61
+
62
+ /// True if the `@JS`-marked declaration opted into running off the JavaScript thread with
63
+ /// `@JS(.concurrent)`. Such a member is left unstamped and gets `@concurrent` instead, so its body
64
+ /// runs on the concurrent pool rather than inheriting the JS thread.
65
+ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
66
+ guard let attribute = memberAttributes(of: decl).firstAttribute(named: "JS") else {
67
+ return false
68
+ }
69
+ return hasJSOption(attribute, named: "concurrent")
70
+ }
71
+
72
+ /// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
73
+ /// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
74
+ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
75
+ if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
76
+ return true
77
+ }
78
+ if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == "Optional" {
79
+ return true
80
+ }
81
+ return false
82
+ }
83
+
84
+ /// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
85
+ /// optional.
86
+ internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
87
+ if let optional = type.as(OptionalTypeSyntax.self) {
88
+ return optional.wrappedType
89
+ }
90
+ if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
91
+ return implicitlyUnwrapped.wrappedType
92
+ }
93
+ if let identifier = type.as(IdentifierTypeSyntax.self),
94
+ identifier.name.text == "Optional",
95
+ let argument = identifier.genericArgumentClause?.arguments.first,
96
+ case .type(let wrapped) = argument.argument {
97
+ return wrapped
98
+ }
99
+ return nil
100
+ }
101
+
102
+ /// True when the modifiers make the member type-level (`static` or `class`). A shared object routes
103
+ /// such members to its constructor (JS-static) and instance members to its prototype; the `@JS`
104
+ /// conformance assertion also uses this to emit its peer in the matching metatype context.
105
+ internal func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
106
+ return modifiers.contains {
107
+ $0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
108
+ }
109
+ }
110
+
111
+ /// True if a trailing occurrence of this parameter may be omitted by the JS caller: it either has a
112
+ /// default value (Swift applies it) or is an optional type (an absent slot becomes `nil`). The arity
113
+ /// range and the per-arity call branches are derived from this.
114
+ internal func isOmittable(_ parameter: FunctionParameterSyntax) -> Bool {
115
+ return hasDefaultValue(parameter) || isOptionalType(parameter.type)
116
+ }
117
+
118
+ /// True if the parameter declares a default value (`b: Int = 5`). An omitted defaulted slot is left
119
+ /// out of the call so Swift fills in the default, distinguishing it from an omitted optional slot
120
+ /// (passed `nil`).
121
+ internal func hasDefaultValue(_ parameter: FunctionParameterSyntax) -> Bool {
122
+ return parameter.defaultValue != nil
123
+ }
124
+
125
+ /**
126
+ True if the declaration is a `@Event(sync: true)` property. A sync event dispatches inline on the
127
+ JS thread instead of scheduling, so `@ExpoModule`/`@SharedObject` stamp it with `@JavaScriptActor`,
128
+ making "must be called on the JS thread" a compile-time guarantee at the call site. Async events
129
+ (the default) are deliberately not stamped: their `emit` schedules onto the JS thread itself, so
130
+ they stay callable from any thread.
131
+ */
132
+ internal func isSyncEventMember(_ decl: DeclSyntaxProtocol) -> Bool {
133
+ guard let varDecl = decl.as(VariableDeclSyntax.self),
134
+ let attribute = varDecl.attributes.firstAttribute(named: "Event") else {
135
+ return false
136
+ }
137
+ return boolArgument(of: attribute, label: "sync") == true
138
+ }
139
+
140
+ /**
141
+ Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
142
+ and returns the type names referenced (e.g. `["Foo", "Bar"]`). Each element must be a
143
+ `<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
144
+ */
145
+ internal func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
146
+ guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
147
+ return []
148
+ }
149
+ for arg in args where arg.label?.text == label {
150
+ guard let array = arg.expression.as(ArrayExprSyntax.self) else {
151
+ return []
152
+ }
153
+ return array.elements.compactMap { element -> String? in
154
+ guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
155
+ memberAccess.declName.baseName.text == "self",
156
+ let base = memberAccess.base else {
157
+ return nil
158
+ }
159
+ // A bare `CardView.self` has a `DeclReferenceExprSyntax` base; a qualified
160
+ // `Outer.CardView.self` has a `MemberAccessExprSyntax` one. Both name a type the generated
161
+ // code can spell, so take the base verbatim rather than only its last component: dropping the
162
+ // qualified form would silently omit the entry, and the type would never be registered.
163
+ if base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self) {
164
+ return base.trimmedDescription
165
+ }
166
+ return nil
167
+ }
168
+ }
169
+ return []
170
+ }
171
+
172
+ /**
173
+ Returns true if the class's inheritance clause names any of the given identifiers.
174
+ Matches by base identifier only, so `Module`, `ExpoModulesCore.Module`, and
175
+ `Module<Foo>` all match an entry of "Module".
176
+ */
177
+ internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>) -> Bool {
178
+ guard let inherited = classDecl.inheritanceClause?.inheritedTypes else {
179
+ return false
180
+ }
181
+ for entry in inherited {
182
+ if let name = baseIdentifier(of: entry.type), names.contains(name) {
183
+ return true
184
+ }
185
+ }
186
+ return false
187
+ }
188
+
189
+ /**
190
+ Returns the rightmost identifier of a type, e.g. `Foo` for `Foo`, `Foo` for
191
+ `Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
192
+ */
193
+ internal func baseIdentifier(of type: TypeSyntax) -> String? {
194
+ if let identifier = type.as(IdentifierTypeSyntax.self) {
195
+ return identifier.name.text
196
+ }
197
+ if let member = type.as(MemberTypeSyntax.self) {
198
+ return member.name.text
199
+ }
200
+ return nil
201
+ }
202
+
203
+ /**
204
+ True if the declaration carries a `@JS` attribute. Works for functions, properties, and inits;
205
+ returns false for any other decl kind.
206
+ */
207
+ internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
208
+ if let funcDecl = decl.as(FunctionDeclSyntax.self) {
209
+ return funcDecl.attributes.firstAttribute(named: "JS") != nil
210
+ }
211
+ if let varDecl = decl.as(VariableDeclSyntax.self) {
212
+ return varDecl.attributes.firstAttribute(named: "JS") != nil
213
+ }
214
+ if let initDecl = decl.as(InitializerDeclSyntax.self) {
215
+ return initDecl.attributes.firstAttribute(named: "JS") != nil
216
+ }
217
+ return false
218
+ }
219
+
220
+ /// Decides whether the macro should stamp `@JavaScriptActor` on a `@JS`-marked member.
221
+ /// The macro defers to the user when they've already chosen an isolation:
222
+ /// - the member opted out with `@JS(.concurrent)`
223
+ /// - the `nonisolated` modifier is present on the member
224
+ /// - any attribute whose name matches a known global actor (`@MainActor`, `@JavaScriptActor`)
225
+ /// or follows the `*Actor` naming convention is present on the member or its enclosing type
226
+ /// `async` members are stamped too: an `async` function starts its execution on the JS thread and
227
+ /// stays there until the first suspension point, where it may hop to another executor.
228
+ internal func shouldStampJavaScriptActor(
229
+ on member: DeclSyntaxProtocol,
230
+ enclosedBy enclosing: some DeclGroupSyntax
231
+ ) -> Bool {
232
+ if isConcurrentJSMember(member) {
233
+ return false
234
+ }
235
+
236
+ let modifiers = memberModifiers(of: member)
237
+ if modifiers.contains(where: { $0.name.text == "nonisolated" }) {
238
+ return false
239
+ }
240
+
241
+ let memberAttributes = memberAttributes(of: member)
242
+ if memberAttributes.contains(where: hasGlobalActorShape) {
243
+ return false
244
+ }
245
+
246
+ if enclosing.attributes.contains(where: hasGlobalActorShape) {
247
+ return false
248
+ }
249
+
250
+ return true
251
+ }
252
+
253
+ private func memberModifiers(of decl: DeclSyntaxProtocol) -> DeclModifierListSyntax {
254
+ if let funcDecl = decl.as(FunctionDeclSyntax.self) {
255
+ return funcDecl.modifiers
256
+ }
257
+ if let varDecl = decl.as(VariableDeclSyntax.self) {
258
+ return varDecl.modifiers
259
+ }
260
+ if let initDecl = decl.as(InitializerDeclSyntax.self) {
261
+ return initDecl.modifiers
262
+ }
263
+ return DeclModifierListSyntax()
264
+ }
265
+
266
+ internal func memberAttributes(of decl: DeclSyntaxProtocol) -> AttributeListSyntax {
267
+ if let funcDecl = decl.as(FunctionDeclSyntax.self) {
268
+ return funcDecl.attributes
269
+ }
270
+ if let varDecl = decl.as(VariableDeclSyntax.self) {
271
+ return varDecl.attributes
272
+ }
273
+ if let initDecl = decl.as(InitializerDeclSyntax.self) {
274
+ return initDecl.attributes
275
+ }
276
+ return AttributeListSyntax()
277
+ }
278
+
279
+ private func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool {
280
+ guard let attribute = element.as(AttributeSyntax.self) else {
281
+ return false
282
+ }
283
+ let name = attribute.attributeName.trimmedDescription
284
+ return name.hasSuffix("Actor")
285
+ }
286
+
287
+ /// The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
288
+ /// when the expression isn't one of those literals. Used to recover a property's type when it has no
289
+ /// annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
290
+ /// Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
291
+ /// macro can't know (function calls, collection literals, member access) return `nil`.
292
+ internal func inferredLiteralType(of expression: ExprSyntax) -> String? {
293
+ if expression.is(StringLiteralExprSyntax.self) {
294
+ return "String"
295
+ }
296
+ if expression.is(FloatLiteralExprSyntax.self) {
297
+ return "Double"
298
+ }
299
+ if expression.is(IntegerLiteralExprSyntax.self) {
300
+ return "Int"
301
+ }
302
+ if expression.is(BooleanLiteralExprSyntax.self) {
303
+ return "Bool"
304
+ }
305
+ return nil
306
+ }
307
+
308
+ extension AttributeListSyntax {
309
+ internal func firstAttribute(named name: String) -> AttributeSyntax? {
310
+ for element in self {
311
+ if let attr = element.as(AttributeSyntax.self),
312
+ attr.attributeName.trimmedDescription == name {
313
+ return attr
314
+ }
315
+ }
316
+ return nil
317
+ }
318
+ }
319
+
320
+ /// A type spelled so it's valid in expression position (before `.decode`/`.encode`, before
321
+ /// `.getDynamicType()`, or after `as!`). Implicitly-unwrapped optionals (`T!`) are only allowed in
322
+ /// type-annotation position, so a trailing `!` is rewritten to `?` (`T!` and `T?` are both
323
+ /// `Optional<T>`, which the conversion layer treats identically). Other type spellings pass through
324
+ /// unchanged.
325
+ internal func expressionType(_ type: String) -> String {
326
+ guard type.hasSuffix("!") else {
327
+ return type
328
+ }
329
+ return type.dropLast() + "?"
330
+ }
331
+
332
+ /// The decode expression for a boundary type read from `valueExpression` (a `JavaScriptUnownedValue`),
333
+ /// as it appears after `try`. A free-form type (`Any`, `[Any]`, `[String: Any]`) can't conform to
334
+ /// `JavaScriptDecodable`, so it decodes through the dedicated `JavaScriptValue.decodeAny…` entry point
335
+ /// keyed to its shape; every other type decodes through its own static `decode`, spelled in expression
336
+ /// position (`T!` rewritten to `T?`).
337
+ internal func decodeCall(_ type: String, from valueExpression: String) -> String {
338
+ if let method = freeFormDecodeMethod(for: type) {
339
+ return "JavaScriptValue.\(method)(\(valueExpression), in: runtime)"
340
+ }
341
+ return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
342
+ }
343
+
344
+ // MARK: - @JS property collection
345
+
346
+ /// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
347
+ /// Shared between `@ExpoModule` and `@SharedObject` — the resulting properties are receiver-agnostic;
348
+ /// the decorator that emits them picks the receiver (module `self` vs. shared-object `_self`).
349
+ internal func collectProperties(
350
+ varDecl: VariableDeclSyntax,
351
+ attribute: AttributeSyntax
352
+ ) -> [JSProperty] {
353
+ let jsNameOverride = jsNameArgument(of: attribute)
354
+ // A `let` is never settable; only `var` bindings can carry a setter.
355
+ let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
356
+
357
+ return varDecl.bindings.compactMap { binding in
358
+ guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
359
+ return nil
360
+ }
361
+ let swiftName = ident.identifier.text
362
+ // Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
363
+ // when there's none. `nil` falls back to inference at the use site.
364
+ let valueType = binding.typeAnnotation?.type.trimmedDescription
365
+ ?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
366
+ return JSProperty(
367
+ swiftName: swiftName,
368
+ jsName: jsNameOverride ?? swiftName,
369
+ valueType: valueType,
370
+ isSettable: isVar && bindingIsSettable(binding)
371
+ )
372
+ }
373
+ }
374
+
375
+ /// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
376
+ /// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
377
+ /// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
378
+ /// observers imply stored storage, which is also settable.
379
+ internal func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
380
+ guard let accessorBlock = binding.accessorBlock else {
381
+ return true
382
+ }
383
+ switch accessorBlock.accessors {
384
+ case .accessors(let accessors):
385
+ return accessors.contains { accessor in
386
+ switch accessor.accessorSpecifier.tokenKind {
387
+ case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
388
+ return true
389
+ default:
390
+ return false
391
+ }
392
+ }
393
+ case .getter:
394
+ return false
395
+ }
396
+ }
397
+
398
+ /// True if the variable declaration carries a modifier that excludes it from being a stored property
399
+ /// of the surface: `static`, `class` (type-level storage), `private`, `fileprivate`, or `lazy`.
400
+ /// Shared by `@Record` and `@ViewProps`, which apply the same "every stored property counts" rule.
401
+ internal func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
402
+ for modifier in modifiers {
403
+ switch modifier.name.tokenKind {
404
+ case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
405
+ return true
406
+ default:
407
+ continue
408
+ }
409
+ }
410
+ return false
411
+ }
412
+
413
+ /// True if the type's inheritance clause already lists a protocol with the given name. Matches
414
+ /// either the bare identifier (`Record`) or a qualified member access ending in the name
415
+ /// (`ExpoModulesCore.Record`). Used by the extension macros to skip a conformance the author already
416
+ /// spelled out. Works for `struct`, `class`, and `enum` declarations; any other declaration kind has no
417
+ /// inheritance clause to read and reports `false`.
418
+ internal func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
419
+ let inheritanceClause: InheritanceClauseSyntax?
420
+ if let structDecl = declaration.as(StructDeclSyntax.self) {
421
+ inheritanceClause = structDecl.inheritanceClause
422
+ } else if let classDecl = declaration.as(ClassDeclSyntax.self) {
423
+ inheritanceClause = classDecl.inheritanceClause
424
+ } else if let enumDecl = declaration.as(EnumDeclSyntax.self) {
425
+ inheritanceClause = enumDecl.inheritanceClause
426
+ } else {
427
+ return false
428
+ }
429
+ guard let inherited = inheritanceClause?.inheritedTypes else {
430
+ return false
431
+ }
432
+ return inherited.contains { baseIdentifier(of: $0.type) == name }
433
+ }
@@ -0,0 +1,55 @@
1
+ import SwiftSyntaxMacros
2
+
3
+ /// Generates Objective-C type encoding string for the given signature
4
+ internal func generateTypeEncoding(returnType: String, paramTypes: [String]) throws -> String {
5
+ var encoding = ""
6
+
7
+ // Encode return type
8
+ guard let returnEncoding = typeToEncoding(returnType) else {
9
+ throw MacroExpansionErrorMessage("Unsupported return type: \(returnType)")
10
+ }
11
+ encoding += returnEncoding
12
+
13
+ // Add block marker
14
+ encoding += "@?"
15
+
16
+ // Encode parameters
17
+ for paramType in paramTypes {
18
+ guard let paramEncoding = typeToEncoding(paramType) else {
19
+ throw MacroExpansionErrorMessage("Unsupported parameter type: \(paramType)")
20
+ }
21
+ encoding += paramEncoding
22
+ }
23
+
24
+ return encoding
25
+ }
26
+
27
+ /// Maps Swift type to Objective-C type encoding character
28
+ internal func typeToEncoding(_ type: String) -> String? {
29
+ switch type {
30
+ case "Double":
31
+ return "d"
32
+ case "Int", "Int64":
33
+ return "q"
34
+ case "String":
35
+ return "@"
36
+ case "Bool":
37
+ return "B"
38
+ case "Void", "()":
39
+ return "v"
40
+ default:
41
+ return nil
42
+ }
43
+ }
44
+
45
+ struct MacroExpansionErrorMessage: Error, CustomStringConvertible {
46
+ let message: String
47
+
48
+ init(_ message: String) {
49
+ self.message = message
50
+ }
51
+
52
+ var description: String {
53
+ return message
54
+ }
55
+ }
@@ -0,0 +1,34 @@
1
+ import ExpoModulesScanner
2
+ import Foundation
3
+ import SwiftCompilerPlugin
4
+ import SwiftSyntaxMacros
5
+
6
+ struct ExpoModulesMacrosPlugin: CompilerPlugin {
7
+ let providingMacros: [Macro.Type] = [
8
+ OptimizedFunctionAttachedMacro.self,
9
+ JSMacro.self,
10
+ EventMacro.self,
11
+ ExpoModuleMacro.self,
12
+ SharedObjectMacro.self,
13
+ RecordMacro.self,
14
+ UnionMacro.self,
15
+ ViewPropsMacro.self,
16
+ ExpoViewMacro.self,
17
+ ]
18
+ }
19
+
20
+ /// The executable doubles as the scanner CLI. The compiler always launches a plugin executable
21
+ /// without arguments and speaks the plugin protocol over stdin, so any argument means a scanner
22
+ /// invocation (`ExpoModulesMacros scan-modules <path>...`); with none, this starts the plugin
23
+ /// server exactly as `@main` on the `CompilerPlugin` type would.
24
+ @main
25
+ enum EntryPoint {
26
+ static func main() throws {
27
+ let arguments = Array(CommandLine.arguments.dropFirst())
28
+ if arguments.isEmpty {
29
+ try ExpoModulesMacrosPlugin.main()
30
+ } else {
31
+ exit(ScannerCLI.run(arguments: arguments))
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,79 @@
1
+ import SwiftSyntax
2
+
3
+ /// Where a directly-bound closure gets the Swift value it calls into. This is one of the two
4
+ /// orthogonal axes of a binding; the other is the `Phase` (which JS object the member is installed
5
+ /// on). The two are independent: choosing the prototype phase does not by itself decide whether the
6
+ /// receiver is `self`, `_self`, or the metatype.
7
+ ///
8
+ /// - A module is a singleton, so its bindings call `self` and ignore the JS `this`.
9
+ /// - A shared-object *instance* member has a distinct native instance per JS object, so it recovers
10
+ /// the typed receiver from `this`.
11
+ /// - A `static`/`class` member has no instance at all; it calls the Swift member on the metatype
12
+ /// (`Cache.open(…)`) and ignores `this` (which, on the static side, is the constructor).
13
+ internal enum Receiver {
14
+ /// The module singleton; the closure captures `self` strong.
15
+ case module
16
+ /// A shared object of the given concrete type; the closure captures nothing and recovers the receiver
17
+ /// from `this` per call.
18
+ case sharedObject(typeName: String)
19
+ /// A `static`/`class` member of the given concrete type; the closure captures nothing and calls the
20
+ /// Swift member on the type itself, ignoring `this`.
21
+ case staticMember(typeName: String)
22
+
23
+ /// The expression the body calls members on: `self` for a module, `_self` (bound by `unwrapStatement`)
24
+ /// for a shared-object instance, the type name for a static member. The leading underscore on `_self`
25
+ /// avoids colliding with a user member like `var owner`.
26
+ var callee: String {
27
+ switch self {
28
+ case .module:
29
+ return "self"
30
+ case .sharedObject:
31
+ return "_self"
32
+ case .staticMember(let typeName):
33
+ return typeName
34
+ }
35
+ }
36
+
37
+ /// The leading body line binding the receiver, or `nil` when nothing needs to be unwrapped (a module
38
+ /// reads `self` directly; a static member calls the type directly). For a shared-object instance,
39
+ /// `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign object or a type
40
+ /// mismatch.
41
+ ///
42
+ /// The `this` object always comes from the borrowed `JavaScriptUnownedValue` (`asObject(in:)`): an
43
+ /// async binding unwraps in its synchronous decode phase, before the borrowed value's lifetime ends,
44
+ /// and only the recovered native instance crosses into the async body.
45
+ var unwrapStatement: String? {
46
+ switch self {
47
+ case .module, .staticMember:
48
+ return nil
49
+ case .sharedObject(let typeName):
50
+ return "let _self = try SharedObject.native(from: this.asObject(in: runtime), as: \(typeName).self)"
51
+ }
52
+ }
53
+
54
+ /// The capture-clause fragment (with a trailing space, or empty when nothing is captured). A module
55
+ /// captures `self` strong; a shared-object instance and a static member capture nothing.
56
+ var captureClause: String {
57
+ switch self {
58
+ case .module:
59
+ return "[self] "
60
+ case .sharedObject, .staticMember:
61
+ return ""
62
+ }
63
+ }
64
+ }
65
+
66
+ /// Which JS object a set of bindings is installed on: the second orthogonal axis alongside `Receiver`.
67
+ /// It selects the decorator entry point's argument label and the local name the body binds members onto,
68
+ /// mirroring JS class semantics (a class has a constructor function whose `.prototype` carries instance
69
+ /// members).
70
+ /// The raw value is the argument label of the decorator entry point, which is also the local name the
71
+ /// body binds members onto.
72
+ internal enum Phase: String {
73
+ /// A concrete JS object: a singleton's own object (the module). Instance method; receiver `self`.
74
+ case object
75
+ /// The class constructor's `prototype`, carrying per-instance members. Static; receiver `_self`.
76
+ case prototype
77
+ /// The class constructor function itself, carrying `static`/`class` members. Static; receiver the type.
78
+ case constructor
79
+ }