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.
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 +365 -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,477 @@
1
+ import Foundation
2
+ import SwiftDiagnostics
3
+ import SwiftSyntax
4
+ import SwiftSyntaxBuilder
5
+ import SwiftSyntaxMacros
6
+
7
+ /// Member + extension macro applied to a view-props type. Like `@Record`, **every stored property**
8
+ /// that is not `static`, `private`, `fileprivate`, `lazy` or computed is part of the surface — no
9
+ /// `@Field` wrapper — but unlike a record, the properties split into two kinds by their type:
10
+ ///
11
+ /// - a **function-typed** property is an **event** (`var onTap: (TapEvent) -> Void`), dispatched by
12
+ /// name through the view's event emitter. No JS function object is ever decoded into it.
13
+ /// - every other property is a **value prop**, decoded from the raw props Fabric delivers.
14
+ ///
15
+ /// The macro synthesizes the identity surface the batched reaction model reads:
16
+ ///
17
+ /// - `PropName`: a `String`-backed `CaseIterable` enum, one case per value prop, whose raw value is
18
+ /// the wire key. It doubles as the string → prop translation table, so the runtime maps a raw
19
+ /// changed key with `PropName(rawValue:)` and names a prop in a log with `prop.rawValue`.
20
+ /// - `PropSet`: an `OptionSet` over `UInt64`, one bit per value prop in declaration order. This is the
21
+ /// membership currency: the changed set on a diff is a bitmask, so asking what changed costs no
22
+ /// allocation and no hashing.
23
+ /// - `_eventNames`: the event props' names, verbatim, for core to register at view creation.
24
+ /// - `typealias Diff = PropsDiff<Self>`, the nested spelling of core's one generic diff.
25
+ ///
26
+ /// Author-facing shape — no conformance to spell out:
27
+ ///
28
+ /// @ViewProps
29
+ /// struct CardProps {
30
+ /// var color: UIColor = .red // value prop, bit 0
31
+ /// var radius: CGFloat = 0 // value prop, bit 1
32
+ /// var onTap: (TapEvent) -> Void // event, no bit
33
+ /// }
34
+ ///
35
+ /// The diff type itself is deliberately **not** generated. Core's generic
36
+ /// `PropsDiff<Props: AnyViewProps>` owns both storage (`old`/`new`/`changedProps`) and behavior
37
+ /// (`changed(_:)`, `oldValue(_:)`, `isInitial`, `changedNames`), reaching the per-props types through
38
+ /// the conformance's associated types — so the query API can grow without a macro release.
39
+ ///
40
+ /// Event names are emitted **verbatim**, with no `on`-prefix stripping. This differs from `@Event` on
41
+ /// modules and shared objects, which strips it (`onStatusChange` → `"statusChange"`): a view event prop
42
+ /// is a React prop name that Fabric carries as-is, so the native and JS spellings must match exactly.
43
+ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
44
+ public static func expansion(
45
+ of node: AttributeSyntax,
46
+ providingMembersOf declaration: some DeclGroupSyntax,
47
+ conformingTo protocols: [TypeSyntax],
48
+ in context: some MacroExpansionContext
49
+ ) throws -> [DeclSyntax] {
50
+ let props = try validatedViewProps(of: declaration)
51
+
52
+ var members: [DeclSyntax] = []
53
+ members.append(propNameEnum(valueProps: props.valueProps))
54
+ members.append(propSetStruct(valueProps: props.valueProps))
55
+ members.append(eventNamesConstant(eventProps: props.eventProps))
56
+ members.append(diffTypealias())
57
+ return members
58
+ }
59
+
60
+ /// Auto-conforms the type to `AnyViewProps`, supplying the two requirements that can only be written
61
+ /// per-props: `allProps` (every bit set, the changed set on the first application) and
62
+ /// `propSet(for:)` (one name's bit, for folding raw changed keys into the mask).
63
+ ///
64
+ /// A conformance the author already spelled out in the inheritance clause is not repeated.
65
+ public static func expansion(
66
+ of node: AttributeSyntax,
67
+ attachedTo declaration: some DeclGroupSyntax,
68
+ providingExtensionsOf type: some TypeSyntaxProtocol,
69
+ conformingTo protocols: [TypeSyntax],
70
+ in context: some MacroExpansionContext
71
+ ) throws -> [ExtensionDeclSyntax] {
72
+ guard declaration.is(StructDeclSyntax.self) else {
73
+ return []
74
+ }
75
+ // Both roles read the same model, but a validation failure is the member macro's to report:
76
+ // expansion runs each role independently, so throwing here too would surface every diagnostic
77
+ // twice. When the model doesn't validate, the member macro has already emitted the error and
78
+ // there is nothing to extend.
79
+ guard let props = try? validatedViewProps(of: declaration) else {
80
+ return []
81
+ }
82
+ let alreadyConforms = inheritsProtocol(named: viewPropsProtocolName, in: declaration)
83
+
84
+ // `allProps` is read on every first application of props, once per view instance, so it's a
85
+ // stored constant with the mask already folded rather than a computed property rebuilding an
86
+ // array literal per call. The macro knows the bit count, so the literal is exact: n props
87
+ // occupy bits 0..<n, which is the low n bits set.
88
+ let allPropsMask = props.valueProps.isEmpty
89
+ ? "0"
90
+ : "0b" + String(repeating: "1", count: props.valueProps.count)
91
+
92
+ // With no value prop there's nothing to switch over, and an empty `switch` over an uninhabited
93
+ // enum doesn't compile — return the empty set instead.
94
+ let propSetBody: String
95
+ if props.valueProps.isEmpty {
96
+ propSetBody = " return []"
97
+ } else {
98
+ let cases = props.valueProps
99
+ .map { " case .\($0.name):\n return .\($0.name)" }
100
+ .joined(separator: "\n")
101
+ propSetBody = " switch name {\n\(cases)\n }"
102
+ }
103
+
104
+ let conformanceClause = alreadyConforms ? "" : ": \(viewPropsProtocolName)"
105
+ let ext: DeclSyntax = """
106
+ extension \(type.trimmed)\(raw: conformanceClause) {
107
+ public static let allProps = PropSet(rawValue: \(raw: allPropsMask))
108
+
109
+ /// `@inlinable` so core's raw-key fold can inline the lookup across the module boundary:
110
+ /// it runs once per changed key per props batch.
111
+ @inlinable
112
+ public static func propSet(for name: PropName) -> PropSet {
113
+ \(raw: propSetBody)
114
+ }
115
+ }
116
+ """
117
+ guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
118
+ return []
119
+ }
120
+ return [extDecl]
121
+ }
122
+ }
123
+
124
+ /// The core protocol carrying the `PropName`/`PropSet` associated types plus `allProps` and
125
+ /// `propSet(for:)`, through which the generic `PropsDiff` reaches a specific props type.
126
+ private let viewPropsProtocolName = "AnyViewProps"
127
+
128
+ /// The mask is a single `UInt64`, so a props type can carry at most this many value props. Event
129
+ /// props take no bit and don't count.
130
+ private let maxValueProps = 64
131
+
132
+ // MARK: - Diagnostics
133
+
134
+ /// The error for a leftover `@Field`, attached to the attribute itself and carrying a fix-it that
135
+ /// deletes it. `@ViewProps` treats every stored property as a prop, so the attribute has no meaning
136
+ /// here; left in place it would wrap the value in `Field<T>` and the props type would decode against
137
+ /// the wrong type.
138
+ private func fieldAttributeDiagnostic(
139
+ for attribute: AttributeSyntax,
140
+ on varDecl: VariableDeclSyntax
141
+ ) -> Diagnostic {
142
+ // Rebuild the attribute list without this entry, so the fix-it removes the attribute and the
143
+ // trivia it carried rather than leaving a blank line behind.
144
+ var attributes = varDecl.attributes
145
+ if let index = attributes.firstIndex(where: { element in
146
+ guard case .attribute(let candidate) = element else {
147
+ return false
148
+ }
149
+ return candidate == attribute
150
+ }) {
151
+ attributes.remove(at: index)
152
+ }
153
+ let fixIt = FixIt(
154
+ message: ViewPropsFixItMessage("Remove the '@Field' attribute", id: "viewprops-remove-field"),
155
+ changes: [
156
+ .replace(
157
+ oldNode: Syntax(varDecl),
158
+ newNode: Syntax(varDecl.with(\.attributes, attributes))
159
+ )
160
+ ]
161
+ )
162
+ let message = ViewPropsDiagnosticMessage(
163
+ "@Field is no longer used — @ViewProps treats every stored property as a prop. Remove the @Field attribute",
164
+ id: "viewprops-field-attribute"
165
+ )
166
+ return Diagnostic(node: attribute, message: message, fixIts: [fixIt])
167
+ }
168
+
169
+ /// The error for an optional event prop, attached to the declared type and carrying a fix-it that
170
+ /// unwraps it. Events are registered once at view creation from a static name list, so there is no
171
+ /// shape that could express "sometimes emitted".
172
+ private func optionalEventDiagnostic(for type: TypeSyntax, name: String) -> Diagnostic {
173
+ var fixIts: [FixIt] = []
174
+ if let unwrapped = unwrappedOptionalType(type) {
175
+ fixIts.append(
176
+ FixIt(
177
+ message: ViewPropsFixItMessage("Make '\(name)' non-optional", id: "viewprops-unwrap-event"),
178
+ changes: [
179
+ .replace(
180
+ oldNode: Syntax(type),
181
+ newNode: Syntax(unwrapped.with(\.trailingTrivia, type.trailingTrivia))
182
+ )
183
+ ]
184
+ )
185
+ )
186
+ }
187
+ let message = ViewPropsDiagnosticMessage(
188
+ "Event props cannot be optional — '\(name)' is registered once at view creation, so its presence can't vary. Drop the '?'",
189
+ id: "viewprops-optional-event"
190
+ )
191
+ return Diagnostic(node: type, message: message, fixIts: fixIts)
192
+ }
193
+
194
+ /// The type an optional wraps, with the enclosing parentheses of a parenthesized function type kept
195
+ /// (`(() -> Void)?` unwraps to `() -> Void`, not to a stray `(() -> Void)`). Returns `nil` for a
196
+ /// spelling the fix-it can't rewrite mechanically.
197
+ private func unwrappedOptionalType(_ type: TypeSyntax) -> TypeSyntax? {
198
+ guard let wrapped = optionalWrappedType(type) else {
199
+ return nil
200
+ }
201
+ return innerFunctionType(of: wrapped) ?? wrapped
202
+ }
203
+
204
+ /// The function type inside a single-element parenthesized type, so unwrapping `(() -> Void)?`
205
+ /// yields `() -> Void` rather than keeping the now-redundant parentheses.
206
+ private func innerFunctionType(of type: TypeSyntax) -> TypeSyntax? {
207
+ guard let tuple = type.as(TupleTypeSyntax.self),
208
+ tuple.elements.count == 1,
209
+ let only = tuple.elements.first,
210
+ only.type.is(FunctionTypeSyntax.self) else {
211
+ return nil
212
+ }
213
+ return only.type
214
+ }
215
+
216
+ private struct ViewPropsDiagnosticMessage: DiagnosticMessage {
217
+ let message: String
218
+ let diagnosticID: MessageID
219
+ let severity: DiagnosticSeverity = .error
220
+
221
+ init(_ message: String, id: String) {
222
+ self.message = message
223
+ self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
224
+ }
225
+ }
226
+
227
+ private struct ViewPropsFixItMessage: FixItMessage {
228
+ let message: String
229
+ let fixItID: MessageID
230
+
231
+ init(_ message: String, id: String) {
232
+ self.message = message
233
+ self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
234
+ }
235
+ }
236
+
237
+ // MARK: - Property model
238
+
239
+ /// One stored property of the props type, classified as a value prop or an event.
240
+ private struct ViewProp {
241
+ /// The property name as written, backticks included for an escaped name. Every identifier position
242
+ /// in the generated code uses this, since `case default` and `static let default` don't parse.
243
+ let name: String
244
+ /// The property's declared type, verbatim. Retained for the decode surface, which lands with the
245
+ /// core props contract.
246
+ let type: String
247
+
248
+ /// The name with any escaping backticks removed: the JS-visible key, so the enum's raw value and
249
+ /// `_eventNames` both read `"default"`, never `` "`default`" ``.
250
+ var wireName: String {
251
+ return name.trimmingCharacters(in: CharacterSet(charactersIn: "`"))
252
+ }
253
+ }
254
+
255
+ private struct ViewPropsModel {
256
+ /// Value props, in declaration order. The order is the bit order, so it is part of the ABI
257
+ /// between a compiled view and the runtime that fills the mask.
258
+ let valueProps: [ViewProp]
259
+ /// Event props, in declaration order.
260
+ let eventProps: [ViewProp]
261
+ }
262
+
263
+ /// Reads and validates the props type's stored properties, splitting them into value props and events.
264
+ ///
265
+ /// Rejects what can't be expressed: a non-`struct` declaration (the class form is the SwiftUI path and
266
+ /// needs the observable protocol, which is deferred), a leftover `@Field` attribute, an optional
267
+ /// function type, a property with no determinable type, and more than 64 value props.
268
+ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws -> ViewPropsModel {
269
+ guard let structDecl = declaration.as(StructDeclSyntax.self) else {
270
+ throw MacroExpansionErrorMessage(
271
+ "@ViewProps can only be applied to a struct — the class form (for SwiftUI views) is not supported yet"
272
+ )
273
+ }
274
+ // A generic props type can't work: the synthesized extension would have to repeat the generic
275
+ // parameter list and its constraints, and core reaches the props type through a view's static
276
+ // `Props` typealias, which names one concrete type.
277
+ if structDecl.genericParameterClause != nil {
278
+ throw MacroExpansionErrorMessage(
279
+ "@ViewProps does not support generic types — a view's props type must be concrete"
280
+ )
281
+ }
282
+
283
+ var valueProps: [ViewProp] = []
284
+ var eventProps: [ViewProp] = []
285
+
286
+ for member in declaration.memberBlock.members {
287
+ guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
288
+ continue
289
+ }
290
+ if isExcludedByModifier(varDecl.modifiers) {
291
+ continue
292
+ }
293
+ // `@Field` is the v1 property wrapper and has no meaning here — every stored property is already
294
+ // part of the surface. Left in place it would wrap the value in `Field<T>`, so the props type
295
+ // would decode against the wrong type. Flag it rather than emit code built on it.
296
+ if let fieldAttribute = varDecl.attributes.firstAttribute(named: "Field") {
297
+ throw DiagnosticsError(diagnostics: [fieldAttributeDiagnostic(for: fieldAttribute, on: varDecl)])
298
+ }
299
+
300
+ for binding in varDecl.bindings {
301
+ // A computed property is never a prop, but an accessor block alone doesn't mean computed:
302
+ // `willSet`/`didSet` observers imply stored storage. `bindingIsSettable` draws exactly that
303
+ // line, so a stored property with observers stays part of the surface.
304
+ if binding.accessorBlock != nil && !bindingIsSettable(binding) {
305
+ continue
306
+ }
307
+ // A tuple-destructuring binding (`var (a, b) = (1, 2)`) has no single name to key a prop on,
308
+ // and silently dropping it would leave the props type missing fields the author declared.
309
+ guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
310
+ if binding.pattern.is(TuplePatternSyntax.self) {
311
+ throw MacroExpansionErrorMessage(
312
+ "@ViewProps does not support tuple-destructuring properties — declare each prop separately"
313
+ )
314
+ }
315
+ continue
316
+ }
317
+ // An escaped name (`` var `default`: Int ``) needs both spellings: `identifier.text` keeps the
318
+ // backticks, which every identifier position requires (`case \`default\``), while the wire key
319
+ // and `_eventNames` need the bare name.
320
+ let name = ident.identifier.text
321
+
322
+ // Prefer the explicit annotation. When it's omitted, recover the type from a literal default
323
+ // (`var title = ""`). Anything a syntactic macro can't resolve still needs an annotation,
324
+ // since the decode surface names the type.
325
+ let declaredType = binding.typeAnnotation?.type
326
+ let resolvedType = declaredType?.trimmedDescription
327
+ ?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
328
+ guard let resolvedType else {
329
+ throw MacroExpansionErrorMessage(
330
+ "@ViewProps props must declare an explicit type — '\(name)' has none"
331
+ )
332
+ }
333
+
334
+ // A function type means an event. An *optional* function type would make the event's presence
335
+ // dynamic, but events are registered once at view creation from a static name list, so there
336
+ // is no shape that could express "sometimes emitted".
337
+ if let declaredType, isOptionalType(declaredType), underlyingFunctionType(of: declaredType) != nil {
338
+ throw DiagnosticsError(diagnostics: [
339
+ optionalEventDiagnostic(for: declaredType, name: name)
340
+ ])
341
+ }
342
+
343
+ let isEvent = declaredType.map { underlyingFunctionType(of: $0) != nil } ?? false
344
+ // `PropSet` stores its mask in `rawValue`, so a value prop of that name would emit a static
345
+ // member shadowing it and the option set would not compile ("circular reference"). The error
346
+ // would point at generated code, so catch it here and name the property.
347
+ if !isEvent && name.trimmingCharacters(in: CharacterSet(charactersIn: "`")) == "rawValue" {
348
+ throw MacroExpansionErrorMessage(
349
+ "'rawValue' can't be used as a prop name — it collides with the synthesized PropSet's storage. Rename the property"
350
+ )
351
+ }
352
+ let prop = ViewProp(name: name, type: resolvedType)
353
+ if isEvent {
354
+ eventProps.append(prop)
355
+ } else {
356
+ valueProps.append(prop)
357
+ }
358
+ }
359
+ }
360
+
361
+ guard valueProps.count <= maxValueProps else {
362
+ throw MacroExpansionErrorMessage(
363
+ "@ViewProps supports at most \(maxValueProps) value props (the changed-props mask is a UInt64), but this type declares \(valueProps.count); '\(valueProps[maxValueProps].name)' is the first over the limit. Event props don't count toward it"
364
+ )
365
+ }
366
+ return ViewPropsModel(valueProps: valueProps, eventProps: eventProps)
367
+ }
368
+
369
+ /// The function type a props property declares, unwrapping any number of enclosing parentheses and an
370
+ /// optional wrapper, or `nil` when the type isn't a function. `@Sendable`/`@escaping` and other
371
+ /// attributed forms unwrap to the function type underneath.
372
+ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax? {
373
+ if let functionType = type.as(FunctionTypeSyntax.self) {
374
+ return functionType
375
+ }
376
+ if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first {
377
+ return underlyingFunctionType(of: only.type)
378
+ }
379
+ if let attributed = type.as(AttributedTypeSyntax.self) {
380
+ return underlyingFunctionType(of: attributed.baseType)
381
+ }
382
+ if let optional = type.as(OptionalTypeSyntax.self) {
383
+ return underlyingFunctionType(of: optional.wrappedType)
384
+ }
385
+ if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
386
+ return underlyingFunctionType(of: implicitlyUnwrapped.wrappedType)
387
+ }
388
+ // The long spelling of an optional: `Optional<() -> Void>` has to reach the same diagnostic as
389
+ // `(() -> Void)?` and `(() -> Void)!`, or the same declaration would be an event in one spelling
390
+ // and a value prop in another.
391
+ if let identifier = type.as(IdentifierTypeSyntax.self),
392
+ identifier.name.text == "Optional",
393
+ let argument = identifier.genericArgumentClause?.arguments.first,
394
+ case .type(let wrapped) = argument.argument {
395
+ return underlyingFunctionType(of: wrapped)
396
+ }
397
+ return nil
398
+ }
399
+
400
+ // MARK: - Synthesized members
401
+
402
+ /// The `PropName` enum: one case per value prop, `String`-backed so the raw value is the wire key.
403
+ /// Emitted even when empty (an uninhabited enum is legal and keeps the conformance's associated type
404
+ /// satisfied), so a props type with only events still conforms.
405
+ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
406
+ // An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
407
+ // the wire key visible in the generated source and independent of that implicit rule.
408
+ let cases = valueProps
409
+ .map { prop in
410
+ prop.name == prop.wireName
411
+ ? " case \(prop.name)"
412
+ : " case \(prop.name) = \"\(prop.wireName)\""
413
+ }
414
+ .joined(separator: "\n")
415
+ if valueProps.isEmpty {
416
+ return """
417
+ public enum PropName: String, CaseIterable {
418
+ }
419
+ """
420
+ }
421
+ return """
422
+ public enum PropName: String, CaseIterable {
423
+ \(raw: cases)
424
+ }
425
+ """
426
+ }
427
+
428
+ /// The `PropSet` option set: one static member per value prop, at its declaration-order bit. This is
429
+ /// what a diff's `changedProps` holds, and what `changed(_:)` tests against, so the whole changed set
430
+ /// is one machine word.
431
+ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
432
+ let members = valueProps.enumerated()
433
+ .map { index, prop in
434
+ " public static let \(prop.name) = PropSet(rawValue: 1 << \(index))"
435
+ }
436
+ .joined(separator: "\n")
437
+
438
+ if valueProps.isEmpty {
439
+ return """
440
+ public struct PropSet: OptionSet, Sendable {
441
+ public let rawValue: UInt64
442
+
443
+ public init(rawValue: UInt64) {
444
+ self.rawValue = rawValue
445
+ }
446
+ }
447
+ """
448
+ }
449
+ return """
450
+ public struct PropSet: OptionSet, Sendable {
451
+ public let rawValue: UInt64
452
+
453
+ public init(rawValue: UInt64) {
454
+ self.rawValue = rawValue
455
+ }
456
+
457
+ \(raw: members)
458
+ }
459
+ """
460
+ }
461
+
462
+ /// The event props' names, verbatim, for core to register at view creation. Verbatim because a view
463
+ /// event prop is a React prop name Fabric carries as-is — unlike `@Event` on a module, which strips
464
+ /// the `on` prefix.
465
+ private func eventNamesConstant(eventProps: [ViewProp]) -> DeclSyntax {
466
+ let names = eventProps.map { "\"\($0.wireName)\"" }.joined(separator: ", ")
467
+ return """
468
+ public static let _eventNames: [String] = [\(raw: names)]
469
+ """
470
+ }
471
+
472
+ /// The nested spelling of core's one generic diff, so a view writes `MyProps.Diff`.
473
+ private func diffTypealias() -> DeclSyntax {
474
+ return """
475
+ public typealias Diff = PropsDiff<Self>
476
+ """
477
+ }
@@ -0,0 +1,19 @@
1
+ /// An attached macro that generates an optimized synchronous function descriptor.
2
+ /// Creates a peer function that returns `OptimizedFunctionDescriptor`, for use with
3
+ /// the `Function("name", descriptor)` overload in ModuleDefinition result builders.
4
+ ///
5
+ /// Usage:
6
+ ///
7
+ /// @OptimizedFunction
8
+ /// private func addNumbers(a: Double, b: Double) -> Double {
9
+ /// return a + b
10
+ /// }
11
+ ///
12
+ /// // In definition():
13
+ /// Function("addNumbers", addNumbers())
14
+ ///
15
+ /// The generated peer function uses the optimized JSI bridge path with
16
+ /// @convention(block) closures for maximum performance.
17
+ @attached(peer, names: arbitrary)
18
+ public macro OptimizedFunction() =
19
+ #externalMacro(module: "ExpoModulesMacros", type: "OptimizedFunctionAttachedMacro")
@@ -0,0 +1,114 @@
1
+ import Foundation
2
+
3
+ /// Command-line front end for the scanner: parses the subcommand and paths, then delegates to the
4
+ /// matching `Scanner` entry (which runs the scan and writes its JSON output). Lives in the library
5
+ /// so the macro plugin executable can dispatch into it: the compiler always launches that executable
6
+ /// without arguments and speaks the plugin protocol over stdin, so any argument means a scanner
7
+ /// invocation. Each path may be a `.swift` file or a directory (scanned recursively for `.swift`
8
+ /// files).
9
+ ///
10
+ /// Subcommands:
11
+ /// scan-modules <path>... fast: top-level `@ExpoModule` types, for autolinking
12
+ /// scan-exports <path>... deep: full JS-exported surface, for TS type generation
13
+ public enum ScannerCLI {
14
+ /// Runs the CLI for the given arguments (argv without the executable path) and returns the
15
+ /// process exit code: `0` on success, `1` if encoding the report fails, `2` on a usage error.
16
+ public static func run(arguments: [String]) -> Int32 {
17
+ // `-h`/`--help` anywhere is treated as a help request: print usage to stdout and exit 0.
18
+ if arguments.contains(where: { $0 == "-h" || $0 == "--help" }) {
19
+ printUsage(to: .standardOutput)
20
+ return 0
21
+ }
22
+
23
+ guard let subcommand = arguments.first else {
24
+ printUsage()
25
+ return 2
26
+ }
27
+
28
+ var paths: [String] = []
29
+ var platform: String?
30
+ var defines: [String] = []
31
+
32
+ var rest = arguments.dropFirst().makeIterator()
33
+ while let argument = rest.next() {
34
+ switch argument {
35
+ case "--platform":
36
+ guard let value = rest.next() else {
37
+ return usageError("--platform requires a value")
38
+ }
39
+ platform = value
40
+ case "--define":
41
+ guard let value = rest.next() else {
42
+ return usageError("--define requires a value")
43
+ }
44
+ defines.append(value)
45
+ default:
46
+ paths.append(argument)
47
+ }
48
+ }
49
+
50
+ switch subcommand {
51
+ case "scan-modules":
52
+ // scan-modules is platform-agnostic: each reported module carries the platforms that include
53
+ // it, and the consumer filters. An option selecting one platform would silently drop data.
54
+ guard platform == nil else {
55
+ return usageError("scan-modules does not take --platform; each module reports its 'platforms' and the consumer filters")
56
+ }
57
+ guard !paths.isEmpty else {
58
+ return usageError("scan-modules requires at least one path")
59
+ }
60
+ return Scanner.runModules(paths: paths, defines: defines)
61
+
62
+ case "scan-exports":
63
+ // The exports surface visitor doesn't evaluate `#if` blocks yet, so accepting the options
64
+ // here would silently do nothing.
65
+ guard platform == nil, defines.isEmpty else {
66
+ return usageError("scan-exports does not support --platform or --define")
67
+ }
68
+ guard !paths.isEmpty else {
69
+ return usageError("scan-exports requires at least one path")
70
+ }
71
+ return Scanner.runExports(paths: paths)
72
+
73
+ default:
74
+ return usageError("unknown subcommand '\(subcommand)'")
75
+ }
76
+ }
77
+ }
78
+
79
+ /// The invoked executable's basename, so the usage text matches however the tool was launched
80
+ /// (the `ExpoModulesMacros` shipped in the package, or a locally built `ExpoModulesMacros-tool`).
81
+ private var toolName: String {
82
+ return (CommandLine.arguments.first as NSString?)?.lastPathComponent ?? "ExpoModulesScanner"
83
+ }
84
+
85
+ private var usageText: String {
86
+ """
87
+ usage: \(toolName) <subcommand> [options] <path> [<path> ...]
88
+
89
+ subcommands:
90
+ scan-modules fast scan for top-level @ExpoModule types (autolinking)
91
+ scan-exports deep scan of the full JS-exported surface (type generation)
92
+
93
+ options (scan-modules only):
94
+ --define <flag> treat a conditional compilation flag (e.g. DEBUG) as set when resolving
95
+ each module's 'platforms' list; repeatable
96
+
97
+ options:
98
+ -h, --help print this help and exit
99
+
100
+ """
101
+ }
102
+
103
+ /// Prints the usage text to the given handle. Goes to stdout when help was explicitly requested
104
+ /// (a successful action), stderr when it accompanies a usage error.
105
+ private func printUsage(to handle: FileHandle = .standardError) {
106
+ handle.write(Data(usageText.utf8))
107
+ }
108
+
109
+ /// Reports a usage error on stderr, followed by the usage text, and returns the usage exit code.
110
+ private func usageError(_ message: String) -> Int32 {
111
+ FileHandle.standardError.write(Data("error: \(message)\n".utf8))
112
+ printUsage()
113
+ return 2
114
+ }