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.
- package/.github/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +389 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- 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
|
+
}
|