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,319 @@
1
+ import Foundation
2
+
3
+ /// The deep-scan surface: the full JS-exported shape of every `@ExpoModule`, `@SharedObject`, and
4
+ /// `@Record` type, with the per-member detail a TypeScript type generator needs. Read syntactically
5
+ /// (like the macros): each boundary type becomes a structured `TypeNode` tree so the consumer walks a
6
+ /// tagged tree rather than re-parsing Swift type syntax.
7
+
8
+ /// One parameter of a `@JS` function or `@JS init`.
9
+ struct ExportedParameter: Encodable, Equatable {
10
+ /// The argument label (the `first` name): `to` in `func move(to point: Point)`, or `_` if unlabeled.
11
+ let label: String
12
+
13
+ /// The internal parameter name (the `second` name, else the same as `label`): `point` above.
14
+ let name: String
15
+
16
+ let type: TypeNode
17
+
18
+ /// True when the caller may omit it: a default value or an optional type. Distinct from the type
19
+ /// being `.optional` (a defaulted non-optional is also omittable). Encoded as `optional`.
20
+ let isOptional: Bool
21
+
22
+ private enum CodingKeys: String, CodingKey {
23
+ case label, name, type
24
+ case isOptional = "optional"
25
+ }
26
+ }
27
+
28
+ /// One `@JS func` on a module or shared object.
29
+ struct ExportedFunction: Encodable, Equatable {
30
+ /// The Swift declaration name.
31
+ let name: String
32
+
33
+ /// The JS name it binds under: the `@JS("x")` override, else `name`.
34
+ let jsName: String
35
+
36
+ let parameters: [ExportedParameter]
37
+
38
+ /// The return type, or `nil` for `Void`. Named `returns` to pair with `parameters`.
39
+ let returns: TypeNode?
40
+
41
+ /// `async` (an async function is promise-returning in JS).
42
+ let isAsync: Bool
43
+
44
+ /// `throws`.
45
+ let isThrowing: Bool
46
+
47
+ /// `static`/`class` member.
48
+ let isStatic: Bool
49
+
50
+ /// The flags are encoded under their TS-keyword spellings.
51
+ private enum CodingKeys: String, CodingKey {
52
+ case name, jsName, parameters, returns
53
+ case isAsync = "async"
54
+ case isThrowing = "throws"
55
+ case isStatic = "static"
56
+ }
57
+ }
58
+
59
+ /// One `@JS var` on a module or shared object.
60
+ struct ExportedProperty: Encodable, Equatable {
61
+ /// The Swift declaration name.
62
+ let name: String
63
+
64
+ /// The JS name it binds under: the `@JS("x")` override, else `name`.
65
+ let jsName: String
66
+
67
+ /// The value type, or `nil` when undeterminable syntactically (no annotation and no literal
68
+ /// default), the case the macro binds getter-only.
69
+ let type: TypeNode?
70
+
71
+ /// True when JS can assign to it: a stored `var` or a computed `var` with a `set`. Encoded as its
72
+ /// inverse, `readonly`.
73
+ let isSettable: Bool
74
+
75
+ /// `static`/`class` member. Encoded as `static`.
76
+ let isStatic: Bool
77
+
78
+ private enum CodingKeys: String, CodingKey {
79
+ case name, jsName, type
80
+ case isReadonly = "readonly"
81
+ case isStatic = "static"
82
+ }
83
+
84
+ func encode(to encoder: Encoder) throws {
85
+ var container = encoder.container(keyedBy: CodingKeys.self)
86
+ try container.encode(name, forKey: .name)
87
+ try container.encode(jsName, forKey: .jsName)
88
+ try container.encodeIfPresent(type, forKey: .type)
89
+ try container.encode(!isSettable, forKey: .isReadonly)
90
+ try container.encode(isStatic, forKey: .isStatic)
91
+ }
92
+ }
93
+
94
+ /// One `@Record` property: a plain data slot exposing `optional`/`required` (vs. `ExportedProperty`,
95
+ /// a JS accessor exposing `readonly`/`static`). A record crosses the boundary by value, so the whole
96
+ /// type is read-only in JS; that's a record-level fact and isn't stamped per property.
97
+ struct ExportedRecordProperty: Encodable, Equatable {
98
+ let name: String
99
+
100
+ /// `@Record` requires a determinable type on every property, so this is never `nil`.
101
+ let type: TypeNode
102
+
103
+ /// Optional-typed. Encoded as `optional`.
104
+ let isOptional: Bool
105
+
106
+ /// Has a default value. Not encoded (derivable as `!isOptional && !isRequired`, and a Swift default
107
+ /// never reaches JS); kept only to derive `isRequired`.
108
+ let hasDefault: Bool
109
+
110
+ /// Whether JS must supply this property, matching the macro's `RecordProperty.isRequired`. Encoded
111
+ /// as `required`.
112
+ var isRequired: Bool {
113
+ return !hasDefault && !isOptional
114
+ }
115
+
116
+ private enum CodingKeys: String, CodingKey {
117
+ case name, type
118
+ case isOptional = "optional"
119
+ case isRequired = "required"
120
+ }
121
+
122
+ func encode(to encoder: Encoder) throws {
123
+ var container = encoder.container(keyedBy: CodingKeys.self)
124
+ try container.encode(name, forKey: .name)
125
+ try container.encode(type, forKey: .type)
126
+ try container.encode(isOptional, forKey: .isOptional)
127
+ try container.encode(isRequired, forKey: .isRequired)
128
+ }
129
+ }
130
+
131
+ /// One `@Event var` on a module or shared object: a typed event JS listens for with
132
+ /// `addListener(jsName, …)` rather than calls.
133
+ struct ExportedEvent: Encodable, Equatable {
134
+ /// The Swift property name, e.g. `onStatusChange`.
135
+ let name: String
136
+
137
+ /// The name JS listens under: the `@Event("x")` override, else `name` with a conventional `on`
138
+ /// prefix stripped and decapitalized (`onStatusChange` -> `statusChange`).
139
+ let jsName: String
140
+
141
+ /// The single payload parameter's type, or `nil` for a no-payload `() -> Void` event.
142
+ let payload: TypeNode?
143
+
144
+ /// `@Event(sync: true)`, dispatching inline on the JS thread instead of scheduling.
145
+ let isSync: Bool
146
+
147
+ private enum CodingKeys: String, CodingKey {
148
+ case name, jsName, payload
149
+ case isSync = "sync"
150
+ }
151
+
152
+ func encode(to encoder: Encoder) throws {
153
+ var container = encoder.container(keyedBy: CodingKeys.self)
154
+ try container.encode(name, forKey: .name)
155
+ try container.encode(jsName, forKey: .jsName)
156
+ try container.encodeIfPresent(payload, forKey: .payload)
157
+ try container.encode(isSync, forKey: .isSync)
158
+ }
159
+ }
160
+
161
+ /// A `@ExpoModule` type and its `@JS` surface.
162
+ struct ExportedModule: Encodable, Equatable {
163
+ /// The Swift class name.
164
+ let name: String
165
+
166
+ /// The JS module name: `@ExpoModule("Foo")` override, else the class name.
167
+ let jsName: String
168
+
169
+ let functions: [ExportedFunction]
170
+ let properties: [ExportedProperty]
171
+ let events: [ExportedEvent]
172
+
173
+ /// Absolute source path, matching `scan-modules`.
174
+ let file: String
175
+ }
176
+
177
+ /// A `@SharedObject` type: a JS class with an optional `@JS init` constructor plus its `@JS` members.
178
+ struct ExportedSharedObject: Encodable, Equatable {
179
+ /// The Swift class name.
180
+ let name: String
181
+
182
+ /// The JS class name: `@SharedObject("Foo")` override, else the class name.
183
+ let jsName: String
184
+
185
+ /// The `@JS init` parameters, or `nil` when there's none. A shared object has at most one.
186
+ let constructorParameters: [ExportedParameter]?
187
+
188
+ let functions: [ExportedFunction]
189
+ let properties: [ExportedProperty]
190
+ let events: [ExportedEvent]
191
+
192
+ let file: String
193
+ }
194
+
195
+ /// A `@Record` type and its properties (data only: no functions, accessors, or constructor).
196
+ struct ExportedRecord: Encodable, Equatable {
197
+ /// The Swift type name (struct or class).
198
+ let name: String
199
+
200
+ let properties: [ExportedRecordProperty]
201
+
202
+ let file: String
203
+ }
204
+
205
+ /// One case of a reported enum.
206
+ ///
207
+ /// What `rawValue` carries depends on the enum's raw type, and a consumer reads it that way:
208
+ /// - `String`: always present, and **decoded** (`case active = "act"` reports `act`, unquoted). A case
209
+ /// writing none takes its own name, so nothing is left to derive.
210
+ /// - an integer type: present only where written, as **source text** (`1`, `1 << 3`), since the value
211
+ /// may be an expression a syntactic scan can't evaluate. Swift also continues from the preceding
212
+ /// case's value (`case a = 1; case b` makes `b` 2), and that carry is the consumer's to apply.
213
+ /// - no raw type: always absent, since the enum has no raw values.
214
+ struct ExportedEnumCase: Encodable, Equatable {
215
+ /// The case name as declared.
216
+ let name: String
217
+
218
+ /// The raw value, decoded for a string and verbatim source text otherwise, or `nil` per the rule
219
+ /// above.
220
+ let rawValue: String?
221
+
222
+ private enum CodingKeys: String, CodingKey {
223
+ case name, rawValue
224
+ }
225
+
226
+ func encode(to encoder: Encoder) throws {
227
+ var container = encoder.container(keyedBy: CodingKeys.self)
228
+ try container.encode(name, forKey: .name)
229
+ try container.encodeIfPresent(rawValue, forKey: .rawValue)
230
+ }
231
+ }
232
+
233
+ /// An `Enumerable` enum: a type that crosses the boundary as its raw value rather than as an object.
234
+ /// Detected by conformance, not by a macro attribute, because core converts it through its
235
+ /// `RawRepresentable`/`Enumerable` conformance (`Coding/…+Enumerable`) with no macro involved.
236
+ struct ExportedEnum: Encodable, Equatable {
237
+ /// The Swift enum name. No `jsName`: an enum carries no macro to spell an override on, and it
238
+ /// reaches JS as its raw values, not under a bound name.
239
+ let name: String
240
+
241
+ /// The raw value type as written (`String`, `Int`, …), or `nil` for a bare `Enumerable` conformance
242
+ /// with no raw type. A `nil` raw type is reported as-is rather than dropped: the enum is still
243
+ /// declared convertible, and the consumer decides how to treat it.
244
+ let rawType: TypeNode?
245
+
246
+ let cases: [ExportedEnumCase]
247
+
248
+ let file: String
249
+
250
+ private enum CodingKeys: String, CodingKey {
251
+ case name, rawType, cases, file
252
+ }
253
+
254
+ func encode(to encoder: Encoder) throws {
255
+ var container = encoder.container(keyedBy: CodingKeys.self)
256
+ try container.encode(name, forKey: .name)
257
+ try container.encodeIfPresent(rawType, forKey: .rawType)
258
+ try container.encode(cases, forKey: .cases)
259
+ try container.encode(file, forKey: .file)
260
+ }
261
+ }
262
+
263
+ /// One alternative a `@Union` may hold: the Swift case name plus its payload type. Not a member in the
264
+ /// sense a module or shared object has members (functions, properties, events).
265
+ struct ExportedUnionMember: Encodable, Equatable {
266
+ /// The case name as declared. Swift-side only: discrimination is structural, so it never reaches JS,
267
+ /// but it ties an alternative back to the declaration.
268
+ let name: String
269
+
270
+ /// The associated value's type: what this alternative decodes from. Named `type` like every other
271
+ /// type-valued field here; `payload` already means an event's argument type on `ExportedEvent`.
272
+ let type: TypeNode
273
+
274
+ private enum CodingKeys: String, CodingKey {
275
+ case name, type
276
+ }
277
+ }
278
+
279
+ /// A `@Union` enum: a typed union of its cases' payload types (`A | B | C` in TypeScript).
280
+ ///
281
+ /// **`members` is ordered, and the order is part of the contract.** Decode tries each payload in
282
+ /// declaration order and takes the first that succeeds, so where two shapes overlap (`Int` and
283
+ /// `Double`, two compatible records) the earlier case wins. Reordering them describes a different
284
+ /// union than the one the module runs.
285
+ struct ExportedUnion: Encodable, Equatable {
286
+ /// The Swift enum name. No `jsName`: `@Union` takes no arguments, and a union reaches JS as its
287
+ /// payload types rather than under a bound name.
288
+ let name: String
289
+
290
+ let members: [ExportedUnionMember]
291
+
292
+ let file: String
293
+ }
294
+
295
+ /// The exported types grouped by kind, nested under `exports` in the result so the surface is one
296
+ /// self-contained object separate from `stats`.
297
+ struct ExportedSurface: Encodable, Equatable {
298
+ let modules: [ExportedModule]
299
+ let sharedObjects: [ExportedSharedObject]
300
+ let records: [ExportedRecord]
301
+ let enums: [ExportedEnum]
302
+ let unions: [ExportedUnion]
303
+ }
304
+
305
+ /// Version of the `scan-exports` output shape. Bumped on any breaking change to the envelope or to
306
+ /// anything under `exports`, so a consumer can verify it understands the output before trusting it.
307
+ /// Versioned independently of `scanModulesSchemaVersion`: the two commands serve different consumers
308
+ /// and change for different reasons. Version 2 added `events` to modules and shared objects; version 3
309
+ /// added `enums`; version 4 added `unions`; version 5 resolves refs, adding `refKind` and correcting
310
+ /// an enum ref's `typeof`.
311
+ let scanExportsSchemaVersion = 5
312
+
313
+ /// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
314
+ /// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
315
+ struct ScanExportsResult: Encodable, Equatable {
316
+ let schemaVersion: Int
317
+ let exports: ExportedSurface
318
+ let stats: ScanStats
319
+ }
@@ -0,0 +1,199 @@
1
+ import Foundation
2
+
3
+ /// Fills in each `.ref`'s `refKind` once the whole scan is done.
4
+ ///
5
+ /// A `TypeNode` is parsed from one `TypeSyntax` in isolation, so at parse time a name like `Status` is
6
+ /// just a name: the parser has no idea whether the scan will turn up an `@Record`, an `Enumerable`
7
+ /// enum, or nothing at all. Resolution is therefore a second pass over the collected surface, run once
8
+ /// every file has been walked.
9
+ ///
10
+ /// Two things change on a resolved ref:
11
+ /// - `refKind` names which kind declares it, so a consumer doesn't repeat this lookup.
12
+ /// - the `typeof` category is corrected for an enum, which crosses as its raw value rather than as an
13
+ /// object. That correction is the reason this can't be left to the consumer: the scanner is the only
14
+ /// side that knows the enum's `rawType`.
15
+ ///
16
+ /// A name the scan never declared keeps `refKind: nil` and its `object` category. Not an error: it is
17
+ /// a platform convertible (`CGPoint`, `URL`) or a type from another module, and the consumer's own
18
+ /// catalog decides.
19
+
20
+ /// The declared names of a scanned surface, each mapped to how it should be reported at a use site.
21
+ /// Built once per scan and used for every ref lookup.
22
+ struct RefIndex {
23
+ /// `name` -> the kind declaring it, plus the JS category a ref to it crosses as.
24
+ private var entries: [String: (kind: RefKind, jsType: JSType)] = [:]
25
+
26
+ init(surface: ExportedSurface) {
27
+ for record in surface.records {
28
+ entries[record.name] = (.record, .object)
29
+ }
30
+ for sharedObject in surface.sharedObjects {
31
+ entries[sharedObject.name] = (.sharedObject, .object)
32
+ }
33
+ for union in surface.unions {
34
+ // A union crosses as whichever alternative matched, so its category is only meaningful per
35
+ // member. `object` is the honest coarse answer; the consumer reads `members` for the real shape.
36
+ entries[union.name] = (.union, .object)
37
+ }
38
+ for enumeration in surface.enums {
39
+ entries[enumeration.name] = (.enum, jsType(ofRawType: enumeration.rawType))
40
+ }
41
+ }
42
+
43
+ /// How a ref to `name` should be reported, or `nil` when the scan declares no such type.
44
+ func lookup(_ name: String) -> (kind: RefKind, jsType: JSType)? {
45
+ if let entry = entries[name] {
46
+ return entry
47
+ }
48
+ // A qualified use site (`Media.Status`) names the same type a bare declaration did. Fall back to
49
+ // the trailing component, which is how the conformance and raw-type checks already match names.
50
+ guard let trailing = name.split(separator: ".").last.map(String.init), trailing != name else {
51
+ return nil
52
+ }
53
+ return entries[trailing]
54
+ }
55
+ }
56
+
57
+ /// The JS category a raw-value enum crosses as: its raw value's. A bare `Enumerable` conformance with
58
+ /// no raw type has nothing to go on, so it stays an object.
59
+ private func jsType(ofRawType rawType: TypeNode?) -> JSType {
60
+ guard let rawType, let jsType = rawType.jsType else {
61
+ return .object
62
+ }
63
+ return jsType
64
+ }
65
+
66
+ extension TypeNode {
67
+ /// This node with every `.ref` in it resolved against `index`, recursively. Returns an unchanged
68
+ /// node when nothing in it resolves.
69
+ func resolvingRefs(using index: RefIndex) -> TypeNode {
70
+ switch self {
71
+ case .primitive, .unknown:
72
+ return self
73
+ case .ref(let name, _, _):
74
+ guard let entry = index.lookup(name) else {
75
+ return self
76
+ }
77
+ // Only an enum crosses as something other than an object, so only it carries an override.
78
+ return .ref(
79
+ name: name, refKind: entry.kind, jsTypeOverride: entry.jsType == .object ? nil : entry.jsType)
80
+ case .optional(let wrapped):
81
+ return .optional(wrapped: wrapped.resolvingRefs(using: index))
82
+ case .array(let element):
83
+ return .array(element: element.resolvingRefs(using: index))
84
+ case .dictionary(let key, let value):
85
+ return .dictionary(
86
+ key: key.resolvingRefs(using: index), value: value.resolvingRefs(using: index))
87
+ case .promise(let value):
88
+ return .promise(value: value.resolvingRefs(using: index))
89
+ case .function(let parameters, let returns, let isAsync, let isThrowing):
90
+ return .function(
91
+ parameters: parameters.map { $0.resolvingRefs(using: index) },
92
+ returns: returns?.resolvingRefs(using: index),
93
+ isAsync: isAsync,
94
+ isThrowing: isThrowing
95
+ )
96
+ }
97
+ }
98
+ }
99
+
100
+ extension ExportedSurface {
101
+ /// This surface with every `.ref` in every reported type resolved against its own declarations. The
102
+ /// second pass `scanExports` runs once the walk is done.
103
+ ///
104
+ /// Only the boundary types are rewritten; names, flags, and ordering are untouched. The declared
105
+ /// types (a record's own name, a union's members) are not themselves refs, so they carry no
106
+ /// `refKind`: it is the *use sites* that gain one.
107
+ func resolvingRefs() -> ExportedSurface {
108
+ let index = RefIndex(surface: self)
109
+ return ExportedSurface(
110
+ modules: modules.map { module in
111
+ ExportedModule(
112
+ name: module.name,
113
+ jsName: module.jsName,
114
+ functions: module.functions.map { $0.resolvingRefs(using: index) },
115
+ properties: module.properties.map { $0.resolvingRefs(using: index) },
116
+ events: module.events.map { $0.resolvingRefs(using: index) },
117
+ file: module.file
118
+ )
119
+ },
120
+ sharedObjects: sharedObjects.map { sharedObject in
121
+ ExportedSharedObject(
122
+ name: sharedObject.name,
123
+ jsName: sharedObject.jsName,
124
+ constructorParameters: sharedObject.constructorParameters?.map { $0.resolvingRefs(using: index) },
125
+ functions: sharedObject.functions.map { $0.resolvingRefs(using: index) },
126
+ properties: sharedObject.properties.map { $0.resolvingRefs(using: index) },
127
+ events: sharedObject.events.map { $0.resolvingRefs(using: index) },
128
+ file: sharedObject.file
129
+ )
130
+ },
131
+ records: records.map { record in
132
+ ExportedRecord(
133
+ name: record.name,
134
+ properties: record.properties.map { property in
135
+ ExportedRecordProperty(
136
+ name: property.name,
137
+ type: property.type.resolvingRefs(using: index),
138
+ isOptional: property.isOptional,
139
+ hasDefault: property.hasDefault
140
+ )
141
+ },
142
+ file: record.file
143
+ )
144
+ },
145
+ // An enum's raw type is a primitive or an unscanned spelling, never a ref to another scanned
146
+ // type, so there is nothing in one to resolve.
147
+ enums: enums,
148
+ unions: unions.map { union in
149
+ ExportedUnion(
150
+ name: union.name,
151
+ members: union.members.map { member in
152
+ ExportedUnionMember(name: member.name, type: member.type.resolvingRefs(using: index))
153
+ },
154
+ file: union.file
155
+ )
156
+ }
157
+ )
158
+ }
159
+ }
160
+
161
+ extension ExportedParameter {
162
+ fileprivate func resolvingRefs(using index: RefIndex) -> ExportedParameter {
163
+ return ExportedParameter(
164
+ label: label, name: name, type: type.resolvingRefs(using: index), isOptional: isOptional)
165
+ }
166
+ }
167
+
168
+ extension ExportedFunction {
169
+ fileprivate func resolvingRefs(using index: RefIndex) -> ExportedFunction {
170
+ return ExportedFunction(
171
+ name: name,
172
+ jsName: jsName,
173
+ parameters: parameters.map { $0.resolvingRefs(using: index) },
174
+ returns: returns?.resolvingRefs(using: index),
175
+ isAsync: isAsync,
176
+ isThrowing: isThrowing,
177
+ isStatic: isStatic
178
+ )
179
+ }
180
+ }
181
+
182
+ extension ExportedProperty {
183
+ fileprivate func resolvingRefs(using index: RefIndex) -> ExportedProperty {
184
+ return ExportedProperty(
185
+ name: name,
186
+ jsName: jsName,
187
+ type: type?.resolvingRefs(using: index),
188
+ isSettable: isSettable,
189
+ isStatic: isStatic
190
+ )
191
+ }
192
+ }
193
+
194
+ extension ExportedEvent {
195
+ fileprivate func resolvingRefs(using index: RefIndex) -> ExportedEvent {
196
+ return ExportedEvent(
197
+ name: name, jsName: jsName, payload: payload?.resolvingRefs(using: index), isSync: isSync)
198
+ }
199
+ }
@@ -0,0 +1,66 @@
1
+ import Foundation
2
+ import SwiftParser
3
+ import SwiftSyntax
4
+
5
+ extension Scanner {
6
+ /// Runs `scan-exports` over `paths`, prints the JSON report to stdout, and returns the exit code
7
+ /// (`0` on success, `1` if encoding fails). The deep counterpart to `runModules`.
8
+ public static func runExports(paths: [String]) -> Int32 {
9
+ let result = scanExports(paths: paths)
10
+
11
+ do {
12
+ let encoder = JSONEncoder()
13
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
14
+ let data = try encoder.encode(result)
15
+ FileHandle.standardOutput.write(data)
16
+ FileHandle.standardOutput.write(Data("\n".utf8))
17
+ return 0
18
+ } catch {
19
+ FileHandle.standardError.write(Data("error: failed to encode results: \(error)\n".utf8))
20
+ return 1
21
+ }
22
+ }
23
+ }
24
+
25
+ /// Scans `paths` for `@ExpoModule`, `@SharedObject`, `@Record`, and `@Union` types plus `Enumerable`
26
+ /// enums, and returns their exported surface plus the run's stats. Separate from the public entry so
27
+ /// tests can drive it without argv/stdout.
28
+ ///
29
+ /// The pre-filter omits `@Event`: an event only declares a member of one of these three types, so a
30
+ /// file containing one already matches on its enclosing type. It does include `Enumerable`, which is a
31
+ /// conformance rather than an attribute: an enum is commonly declared in a file of its own, which no
32
+ /// macro attribute would match.
33
+ func scanExports(paths: [String]) -> ScanExportsResult {
34
+ var modules: [ExportedModule] = []
35
+ var sharedObjects: [ExportedSharedObject] = []
36
+ var records: [ExportedRecord] = []
37
+ var enums: [ExportedEnum] = []
38
+ var unions: [ExportedUnion] = []
39
+
40
+ let stats = scanFiles(
41
+ paths: paths,
42
+ macros: [.expoModule, .sharedObject, .record, .union],
43
+ conformances: [enumerableConformanceName]
44
+ ) { source, file in
45
+ let tree = Parser.parse(source: source)
46
+ let visitor = SurfaceVisitor(file: file)
47
+ visitor.walk(tree)
48
+ modules.append(contentsOf: visitor.modules)
49
+ sharedObjects.append(contentsOf: visitor.sharedObjects)
50
+ records.append(contentsOf: visitor.records)
51
+ enums.append(contentsOf: visitor.enums)
52
+ unions.append(contentsOf: visitor.unions)
53
+ }
54
+
55
+ // Refs resolve only once every file has been walked: a type parsed in one file may name a type
56
+ // declared in another, so the full set of declarations has to exist before any lookup is valid.
57
+ let surface = ExportedSurface(
58
+ modules: modules, sharedObjects: sharedObjects, records: records, enums: enums, unions: unions
59
+ ).resolvingRefs()
60
+
61
+ return ScanExportsResult(
62
+ schemaVersion: scanExportsSchemaVersion,
63
+ exports: surface,
64
+ stats: stats
65
+ )
66
+ }