expo-modules-macros 0.19.1 → 0.20.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.
@@ -9,16 +9,25 @@ import SwiftSyntax
9
9
  /// Types nested in another type's body, or in a file-scope `extension`, are reported under their
10
10
  /// qualified Swift name (`Options.Encoding`), so a ref to one can be resolved from the scope it is
11
11
  /// written in. A type declared in a function body or a closure is local and is not reported.
12
+ ///
13
+ /// Each declaration and member reports the platforms that build it (see `PlatformResolver`), and one
14
+ /// that no platform builds is not reported. `#if` is read only where the runtime sees what it holds:
15
+ /// around types, `@Event` members, and enum cases. `@ExpoModule`, `@SharedObject`, `@Record`, and
16
+ /// `@Union` read only the members written directly in the type body, so `@JS` members, record
17
+ /// properties, and union cases in a `#if` clause are never bound and are not reported. An `@Event`
18
+ /// expands on its own declaration, and the compiler reads enum cases itself.
12
19
  final class SurfaceVisitor: SyntaxVisitor {
13
20
  private let file: String
21
+ private let platformResolver: PlatformResolver
14
22
  private(set) var modules: [ExportedModule] = []
15
23
  private(set) var sharedObjects: [ExportedSharedObject] = []
16
24
  private(set) var records: [ExportedRecord] = []
17
25
  private(set) var enums: [ExportedEnum] = []
18
26
  private(set) var unions: [ExportedUnion] = []
19
27
 
20
- init(file: String) {
28
+ init(file: String, tree: SourceFileSyntax) {
21
29
  self.file = file
30
+ self.platformResolver = PlatformResolver(tree: tree)
22
31
  super.init(viewMode: .sourceAccurate)
23
32
  }
24
33
 
@@ -26,8 +35,7 @@ final class SurfaceVisitor: SyntaxVisitor {
26
35
  guard let scope = enclosingTypes(of: node) else {
27
36
  return .skipChildren
28
37
  }
29
- classify(
30
- name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
38
+ classify(node, name: node.name.identifierName, scope: scope)
31
39
  // The member walk reads the body itself; the children are visited only to find nested types.
32
40
  return .visitChildren
33
41
  }
@@ -36,8 +44,7 @@ final class SurfaceVisitor: SyntaxVisitor {
36
44
  guard let scope = enclosingTypes(of: node) else {
37
45
  return .skipChildren
38
46
  }
39
- classify(
40
- name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
47
+ classify(node, name: node.name.identifierName, scope: scope)
41
48
  return .visitChildren
42
49
  }
43
50
 
@@ -78,12 +85,20 @@ final class SurfaceVisitor: SyntaxVisitor {
78
85
  return .skipChildren
79
86
  }
80
87
  let name = qualifiedName(node.name.identifierName, in: scope)
88
+ let platforms = platformResolver.platforms(of: node)
89
+ // An enum is often a namespace for other types, so its body is searched too, even when this
90
+ // enum is not reported.
91
+ guard !platforms.isEmpty else {
92
+ return .visitChildren
93
+ }
81
94
 
82
95
  if node.attributes.firstAttribute(named: DetectedMacro.union.rawValue) != nil {
83
96
  unions.append(
84
97
  ExportedUnion(
85
98
  name: name,
86
99
  members: collectUnionMembers(node.memberBlock.members),
100
+ accessLevel: spelledAccessLevel(of: node.modifiers),
101
+ platforms: platforms,
87
102
  file: file
88
103
  ))
89
104
  } else if inherits(from: enumerableConformanceName, in: node.inheritanceClause) {
@@ -93,6 +108,8 @@ final class SurfaceVisitor: SyntaxVisitor {
93
108
  name: name,
94
109
  rawType: rawType,
95
110
  cases: collectEnumCases(node.memberBlock.members, rawType: rawType),
111
+ accessLevel: spelledAccessLevel(of: node.modifiers),
112
+ platforms: platforms,
96
113
  file: file
97
114
  ))
98
115
  }
@@ -107,10 +124,15 @@ final class SurfaceVisitor: SyntaxVisitor {
107
124
  /// `name` is the declared name and `scope` the names of the types enclosing it. The reported `name`
108
125
  /// is qualified with the scope, while a default `jsName` is the declared name alone, as the macros
109
126
  /// spell it.
110
- private func classify(
111
- name: String, scope: [String], attributes: AttributeListSyntax, members: MemberBlockItemListSyntax
112
- ) {
127
+ private func classify(_ node: some DeclGroupSyntax, name: String, scope: [String]) {
113
128
  let qualified = qualifiedName(name, in: scope)
129
+ let attributes = node.attributes
130
+ let members = node.memberBlock.members
131
+ let accessLevel = spelledAccessLevel(of: node.modifiers)
132
+ let platforms = platformResolver.platforms(of: node)
133
+ guard !platforms.isEmpty else {
134
+ return
135
+ }
114
136
 
115
137
  if let attribute = attributes.firstAttribute(named: DetectedMacro.expoModule.rawValue) {
116
138
  // Only a file-scope module is reported. Autolinking registers no other (`DetectionVisitor`
@@ -131,82 +153,111 @@ final class SurfaceVisitor: SyntaxVisitor {
131
153
  classes: classListArgument(of: attribute, label: "classes").map {
132
154
  ExportedClass(name: $0, jsName: nil)
133
155
  },
156
+ accessLevel: accessLevel,
157
+ platforms: platforms,
134
158
  file: file
135
159
  ))
136
160
  return
137
161
  }
138
162
 
139
163
  if let attribute = attributes.firstAttribute(named: DetectedMacro.sharedObject.rawValue) {
140
- let (functions, properties, events, constructor) = collectJSMembers(members)
164
+ let (functions, properties, events, initializer) = collectJSMembers(members)
141
165
  sharedObjects.append(
142
166
  ExportedSharedObject(
143
167
  name: qualified,
144
168
  jsName: stringArgument(of: attribute) ?? name,
145
- constructorParameters: constructor,
169
+ initializer: initializer,
146
170
  functions: functions,
147
171
  properties: properties,
148
172
  events: events,
173
+ accessLevel: accessLevel,
174
+ platforms: platforms,
149
175
  file: file
150
176
  ))
151
177
  return
152
178
  }
153
179
 
154
180
  if attributes.firstAttribute(named: DetectedMacro.record.rawValue) != nil {
155
- records.append(ExportedRecord(name: qualified, properties: collectRecordProperties(members), file: file))
181
+ records.append(
182
+ ExportedRecord(
183
+ name: qualified,
184
+ properties: collectRecordProperties(members),
185
+ accessLevel: accessLevel,
186
+ platforms: platforms,
187
+ file: file
188
+ ))
156
189
  }
157
190
  }
158
191
 
159
192
  /// The exported members of a module / shared-object body: `@JS` functions and properties, `@Event`
160
- /// events, and the single `@JS init` constructor parameters (`nil` when absent).
193
+ /// events, and the single `@JS init` (`nil` when absent).
161
194
  private func collectJSMembers(
162
195
  _ members: MemberBlockItemListSyntax
163
196
  ) -> (
164
197
  functions: [ExportedFunction], properties: [ExportedProperty], events: [ExportedEvent],
165
- constructor: [ExportedParameter]?
198
+ initializer: ExportedInitializer?
166
199
  ) {
167
200
  var functions: [ExportedFunction] = []
168
201
  var properties: [ExportedProperty] = []
169
202
  var events: [ExportedEvent] = []
170
- var constructor: [ExportedParameter]?
203
+ var initializer: ExportedInitializer?
171
204
 
172
- for member in members {
205
+ // The macros bind only the `@JS` members written directly in the body.
206
+ let directMembers = Set(members.map(\.id))
207
+
208
+ for member in flattened(members) {
173
209
  let decl = member.decl
210
+ let platforms = platformResolver.platforms(of: decl)
211
+ guard !platforms.isEmpty else {
212
+ continue
213
+ }
214
+
215
+ if let varDecl = decl.as(VariableDeclSyntax.self),
216
+ varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) == nil,
217
+ let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue) {
218
+ events.append(contentsOf: makeEvents(varDecl: varDecl, attribute: attribute, platforms: platforms))
219
+ continue
220
+ }
221
+
222
+ guard directMembers.contains(member.id) else {
223
+ continue
224
+ }
174
225
 
175
226
  if let initDecl = decl.as(InitializerDeclSyntax.self),
176
227
  initDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) != nil {
177
228
  // At most one `@JS init`; keep the first if a malformed source has more (the macro errors).
178
- if constructor == nil {
179
- constructor = parameters(of: initDecl.signature.parameterClause)
229
+ if initializer == nil {
230
+ initializer = ExportedInitializer(
231
+ parameters: parameters(of: initDecl.signature.parameterClause),
232
+ accessLevel: spelledAccessLevel(of: initDecl.modifiers),
233
+ platforms: platforms
234
+ )
180
235
  }
181
236
  continue
182
237
  }
183
238
 
184
239
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
185
240
  let attribute = funcDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) {
186
- functions.append(makeFunction(funcDecl: funcDecl, attribute: attribute))
241
+ functions.append(makeFunction(funcDecl: funcDecl, attribute: attribute, platforms: platforms))
187
242
  continue
188
243
  }
189
244
 
190
245
  if let varDecl = decl.as(VariableDeclSyntax.self),
191
246
  let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) {
192
- properties.append(contentsOf: makeProperties(varDecl: varDecl, attribute: attribute))
193
- continue
194
- }
195
-
196
- if let varDecl = decl.as(VariableDeclSyntax.self),
197
- let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue) {
198
- events.append(contentsOf: makeEvents(varDecl: varDecl, attribute: attribute))
247
+ properties.append(contentsOf: makeProperties(varDecl: varDecl, attribute: attribute, platforms: platforms))
199
248
  }
200
249
  }
201
250
 
202
- return (functions, properties, events, constructor)
251
+ return (functions, properties, events, initializer)
203
252
  }
204
253
 
205
254
  /// Builds the `ExportedEvent` entries for an `@Event var`, applying the same checks
206
255
  /// `EventMacro.validatedEvent(of:on:)` does. A rejected declaration expands to no event, so
207
256
  /// reporting one would describe a surface that does not exist. `@JS` on the same property is
208
257
  /// caught by the caller, which reaches the `@JS` branch first.
209
- private func makeEvents(varDecl: VariableDeclSyntax, attribute: AttributeSyntax) -> [ExportedEvent] {
258
+ private func makeEvents(
259
+ varDecl: VariableDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
260
+ ) -> [ExportedEvent] {
210
261
  guard varDecl.bindingSpecifier.tokenKind != .keyword(.let),
211
262
  !isTypeLevel(varDecl.modifiers) else {
212
263
  return []
@@ -232,7 +283,9 @@ final class SurfaceVisitor: SyntaxVisitor {
232
283
  name: name,
233
284
  jsName: override ?? defaultEventName(for: name),
234
285
  payload: functionType.parameters.first.map { typeNode(from: $0.type) },
235
- isSync: isSync
286
+ isSync: isSync,
287
+ accessLevel: spelledAccessLevel(of: varDecl.modifiers),
288
+ platforms: platforms
236
289
  ))
237
290
  }
238
291
  return result
@@ -240,7 +293,9 @@ final class SurfaceVisitor: SyntaxVisitor {
240
293
 
241
294
  /// Builds an `ExportedFunction` from a `@JS func`: JS-name fallback, parameters, a `Void` return as
242
295
  /// `nil`, and the effect/static flags.
243
- private func makeFunction(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) -> ExportedFunction {
296
+ private func makeFunction(
297
+ funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
298
+ ) -> ExportedFunction {
244
299
  let effects = funcDecl.signature.effectSpecifiers
245
300
  let returnType = funcDecl.signature.returnClause?.type
246
301
  return ExportedFunction(
@@ -250,14 +305,18 @@ final class SurfaceVisitor: SyntaxVisitor {
250
305
  returns: isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) },
251
306
  isAsync: effects?.asyncSpecifier != nil,
252
307
  isThrowing: effects?.throwsClause?.throwsSpecifier != nil,
253
- isStatic: isTypeLevel(funcDecl.modifiers)
308
+ isStatic: isTypeLevel(funcDecl.modifiers),
309
+ accessLevel: spelledAccessLevel(of: funcDecl.modifiers),
310
+ platforms: platforms
254
311
  )
255
312
  }
256
313
 
257
314
  /// Builds the `ExportedProperty` entries for a `@JS var`/`let`. One declaration can introduce several
258
315
  /// bindings (`var a, b: Int`), so this returns an array. The value type is the annotation, else the
259
316
  /// literal default's inferred type, else `nil`.
260
- private func makeProperties(varDecl: VariableDeclSyntax, attribute: AttributeSyntax) -> [ExportedProperty] {
317
+ private func makeProperties(
318
+ varDecl: VariableDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
319
+ ) -> [ExportedProperty] {
261
320
  let isLet = varDecl.bindingSpecifier.tokenKind == .keyword(.let)
262
321
  let isStatic = isTypeLevel(varDecl.modifiers)
263
322
  let override = stringArgument(of: attribute)
@@ -275,7 +334,11 @@ final class SurfaceVisitor: SyntaxVisitor {
275
334
  jsName: override ?? name,
276
335
  type: type,
277
336
  isSettable: isSettable(binding: binding, isLet: isLet),
278
- isStatic: isStatic
337
+ isStatic: isStatic,
338
+ // Only a stored property has an initializer; a computed one has none to report.
339
+ defaultValue: binding.initializer?.value.trimmedDescription,
340
+ accessLevel: spelledAccessLevel(of: varDecl.modifiers),
341
+ platforms: platforms
279
342
  ))
280
343
  }
281
344
  return result
@@ -314,11 +377,16 @@ final class SurfaceVisitor: SyntaxVisitor {
314
377
  private func collectRecordProperties(_ members: MemberBlockItemListSyntax) -> [ExportedRecordProperty] {
315
378
  var properties: [ExportedRecordProperty] = []
316
379
 
380
+ // `@Record` reads only the properties written directly in the body, so `#if` is not looked into.
317
381
  for member in members {
318
382
  guard let varDecl = member.decl.as(VariableDeclSyntax.self),
319
383
  !isExcludedRecordModifier(varDecl.modifiers) else {
320
384
  continue
321
385
  }
386
+ let platforms = platformResolver.platforms(of: varDecl)
387
+ guard !platforms.isEmpty else {
388
+ continue
389
+ }
322
390
  for binding in varDecl.bindings {
323
391
  if binding.accessorBlock != nil {
324
392
  continue
@@ -335,7 +403,9 @@ final class SurfaceVisitor: SyntaxVisitor {
335
403
  name: ident.identifier.identifierName,
336
404
  type: type,
337
405
  isOptional: annotation.map { isOptionalType($0) } ?? false,
338
- hasDefault: binding.initializer != nil
406
+ defaultValue: binding.initializer?.value.trimmedDescription,
407
+ accessLevel: spelledAccessLevel(of: varDecl.modifiers),
408
+ platforms: platforms
339
409
  ))
340
410
  }
341
411
  }
@@ -355,16 +425,21 @@ final class SurfaceVisitor: SyntaxVisitor {
355
425
  ) -> [ExportedEnumCase] {
356
426
  var cases: [ExportedEnumCase] = []
357
427
 
358
- for member in members {
428
+ for member in flattened(members) {
359
429
  guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
360
430
  continue
361
431
  }
432
+ let platforms = platformResolver.platforms(of: caseDecl)
433
+ guard !platforms.isEmpty else {
434
+ continue
435
+ }
362
436
  for element in caseDecl.elements where element.parameterClause == nil {
363
437
  let name = element.name.identifierName
364
438
  cases.append(
365
439
  ExportedEnumCase(
366
440
  name: name,
367
- rawValue: writtenRawValue(of: element) ?? derivedStringRawValue(for: name, rawType: rawType)
441
+ rawValue: writtenRawValue(of: element) ?? derivedStringRawValue(for: name, rawType: rawType),
442
+ platforms: platforms
368
443
  ))
369
444
  }
370
445
  }
@@ -380,10 +455,15 @@ final class SurfaceVisitor: SyntaxVisitor {
380
455
  private func collectUnionMembers(_ members: MemberBlockItemListSyntax) -> [ExportedUnionMember] {
381
456
  var result: [ExportedUnionMember] = []
382
457
 
458
+ // `@Union` reads only the cases written directly in the body, so `#if` is not looked into.
383
459
  for member in members {
384
460
  guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
385
461
  continue
386
462
  }
463
+ let platforms = platformResolver.platforms(of: caseDecl)
464
+ guard !platforms.isEmpty else {
465
+ continue
466
+ }
387
467
  for element in caseDecl.elements {
388
468
  guard let parameters = element.parameterClause?.parameters,
389
469
  parameters.count == 1, let parameter = parameters.first,
@@ -391,14 +471,15 @@ final class SurfaceVisitor: SyntaxVisitor {
391
471
  continue
392
472
  }
393
473
  result.append(
394
- ExportedUnionMember(name: element.name.identifierName, type: typeNode(from: parameter.type)))
474
+ ExportedUnionMember(
475
+ name: element.name.identifierName, type: typeNode(from: parameter.type), platforms: platforms))
395
476
  }
396
477
  }
397
478
  return result
398
479
  }
399
480
 
400
481
  /// Projects a parameter clause into `ExportedParameter`s: label = first name, name = second (else
401
- /// first), and `optional` when it has a default value or an optional type.
482
+ /// first), `optional` when it has a default value or an optional type, and the default as written.
402
483
  private func parameters(of clause: FunctionParameterClauseSyntax) -> [ExportedParameter] {
403
484
  clause.parameters.map { parameter in
404
485
  let label = parameter.firstName.identifierName
@@ -407,25 +488,26 @@ final class SurfaceVisitor: SyntaxVisitor {
407
488
  label: label,
408
489
  name: name,
409
490
  type: typeNode(from: parameter.type),
410
- isOptional: parameter.defaultValue != nil || isOptionalType(parameter.type)
491
+ isOptional: parameter.defaultValue != nil || isOptionalType(parameter.type),
492
+ defaultValue: parameter.defaultValue?.value.trimmedDescription
411
493
  )
412
494
  }
413
495
  }
414
496
 
415
497
  /// The names of the types enclosing a declaration, outermost first: empty at file scope, and `nil`
416
- /// when the declaration is local to a function body or a closure, or sits in a `#if` block.
498
+ /// when the declaration is local to a function body or a closure.
417
499
  ///
418
- /// File scope uses the same rule as `DetectionVisitor.isTopLevel`, except that `#if` is not looked
419
- /// through: the parent is a `CodeBlockItemSyntax` directly under the source file's top-level item
420
- /// list. A member is reached from its type through `MemberBlockItemSyntax`; an `extension`
421
- /// contributes its extended type as written (without backticks), so its spelling may already be
422
- /// qualified.
500
+ /// File scope uses the same rule as `DetectionVisitor.isTopLevel`: the declaration's item sits in
501
+ /// the source file's top-level item list, directly or through `#if` clauses. A member is reached
502
+ /// from its type through `MemberBlockItemSyntax`, also directly or through `#if` clauses; an
503
+ /// `extension` contributes its extended type as written (without backticks), so its spelling may
504
+ /// already be qualified.
423
505
  private func enclosingTypes(of node: some SyntaxProtocol) -> [String]? {
424
506
  if let item = node.parent?.as(CodeBlockItemSyntax.self) {
425
- return item.parent?.parent?.is(SourceFileSyntax.self) == true ? [] : nil
507
+ return isAtFileScope(item) ? [] : nil
426
508
  }
427
509
  guard let item = node.parent?.as(MemberBlockItemSyntax.self),
428
- let parent = item.parent?.parent?.parent,
510
+ let parent = declaration(owning: item),
429
511
  let scope = enclosingTypes(of: parent) else {
430
512
  return nil
431
513
  }
@@ -441,6 +523,57 @@ final class SurfaceVisitor: SyntaxVisitor {
441
523
  }
442
524
  }
443
525
 
526
+ /// True when a code block item is in the source file's top-level item list, directly or in a clause
527
+ /// of a top-level `#if`.
528
+ private func isAtFileScope(_ item: CodeBlockItemSyntax) -> Bool {
529
+ guard let list = item.parent?.as(CodeBlockItemListSyntax.self) else {
530
+ return false
531
+ }
532
+ if list.parent?.is(SourceFileSyntax.self) == true {
533
+ return true
534
+ }
535
+ guard let ifConfigItem = enclosingIfConfig(of: list)?.parent?.as(CodeBlockItemSyntax.self) else {
536
+ return false
537
+ }
538
+ return isAtFileScope(ifConfigItem)
539
+ }
540
+
541
+ /// The type or extension whose body holds a member, looking through the `#if` clauses the member may
542
+ /// be written in.
543
+ private func declaration(owning item: MemberBlockItemSyntax) -> Syntax? {
544
+ guard let list = item.parent?.as(MemberBlockItemListSyntax.self) else {
545
+ return nil
546
+ }
547
+ if let block = list.parent?.as(MemberBlockSyntax.self) {
548
+ return block.parent
549
+ }
550
+ guard let ifConfigItem = enclosingIfConfig(of: list)?.parent?.as(MemberBlockItemSyntax.self) else {
551
+ return nil
552
+ }
553
+ return declaration(owning: ifConfigItem)
554
+ }
555
+
556
+ /// The `#if` declaration whose clause holds `elements`, or `nil` when they are not in a clause.
557
+ private func enclosingIfConfig(of elements: some SyntaxProtocol) -> IfConfigDeclSyntax? {
558
+ return elements.parent?.as(IfConfigClauseSyntax.self)?.parent?.parent?.as(IfConfigDeclSyntax.self)
559
+ }
560
+
561
+ /// The members of a type body in source order, with the members of every clause of each `#if` in
562
+ /// place of the `#if`. Whether a platform builds a member is left to `PlatformResolver`.
563
+ private func flattened(_ members: MemberBlockItemListSyntax) -> [MemberBlockItemSyntax] {
564
+ return members.flatMap { member -> [MemberBlockItemSyntax] in
565
+ guard let ifConfig = member.decl.as(IfConfigDeclSyntax.self) else {
566
+ return [member]
567
+ }
568
+ return ifConfig.clauses.flatMap { clause -> [MemberBlockItemSyntax] in
569
+ guard case .decls(let decls) = clause.elements else {
570
+ return []
571
+ }
572
+ return flattened(decls)
573
+ }
574
+ }
575
+ }
576
+
444
577
  /// A declared name qualified with the names of its enclosing types: `Options.Encoding`.
445
578
  private func qualifiedName(_ name: String, in scope: [String]) -> String {
446
579
  (scope + [name]).joined(separator: ".")
package/build/index.d.ts CHANGED
@@ -6,11 +6,18 @@ export interface ScanModulesOptions {
6
6
  defines?: string[];
7
7
  /** Overrides the scanner binary path. Defaults to the one shipped with this package. */
8
8
  binaryPath?: string;
9
+ /** Milliseconds after which the scanner is killed and the scan fails. No limit by default. */
10
+ timeout?: number;
9
11
  }
10
- /** Options accepted by `scanExports`. The deep scan doesn't evaluate `#if`, so it takes no flags. */
12
+ /**
13
+ * Options accepted by `scanExports`. The deep scan evaluates `#if` per platform with no flags set, so
14
+ * it takes no define option.
15
+ */
11
16
  export interface ScanExportsOptions {
12
17
  /** Overrides the scanner binary path. Defaults to the one shipped with this package. */
13
18
  binaryPath?: string;
19
+ /** Milliseconds after which the scanner is killed and the scan fails. No limit by default. */
20
+ timeout?: number;
14
21
  }
15
22
  /**
16
23
  * Thrown when the scanner exits non-zero. Exit code 2 is a usage error (bad subcommand or flags) and
@@ -54,7 +61,9 @@ export declare function scanModules(paths: string[], options?: ScanModulesOption
54
61
  * Deep scan of the full JS-exported surface, for TypeScript type generation. Each path is a `.swift`
55
62
  * file or a directory, scanned recursively.
56
63
  *
57
- * Unlike `scanModules`, this doesn't evaluate `#if` blocks, so it takes no define option:
58
- * conditional declarations are reported as if their conditions held.
64
+ * Each declaration and member reports the Apple OSes whose `#if` conditions and `@available`
65
+ * attributes include it. `#if` is read only where the runtime sees what it holds: around types,
66
+ * `@Event` members, and enum cases. No define option is taken: a condition on a custom flag never holds, so
67
+ * what it guards is not reported.
59
68
  */
60
69
  export declare function scanExports(paths: string[], options?: ScanExportsOptions): Promise<ScanExportsResult>;
package/build/index.js CHANGED
@@ -120,9 +120,11 @@ function assertRepresentablePaths(command, paths) {
120
120
  '`.');
121
121
  }
122
122
  }
123
- function runScanner(binaryPath, args) {
123
+ function runScanner(binaryPath, args, timeout = 0) {
124
+ // The error messages name only the subcommand: the paths can be hundreds of package roots.
125
+ const command = `${node_path_1.default.basename(binaryPath)} ${args[0]}`;
124
126
  return new Promise((resolve, reject) => {
125
- (0, node_child_process_1.execFile)(binaryPath, args, { maxBuffer: 64 * 1024 * 1024, encoding: 'utf8' }, (error, stdout, stderr) => {
127
+ const child = (0, node_child_process_1.execFile)(binaryPath, args, { maxBuffer: 64 * 1024 * 1024, encoding: 'utf8', timeout }, (error, stdout, stderr) => {
126
128
  if (error) {
127
129
  // A spawn failure reports a string `code` (e.g. 'ENOENT'); a non-zero exit reports a
128
130
  // number. Only the latter is an exit status.
@@ -131,7 +133,11 @@ function runScanner(binaryPath, args) {
131
133
  reject(new ScannerError(missingBinaryMessage(binaryPath), null, stderr));
132
134
  return;
133
135
  }
134
- reject(new ScannerError(`\`${node_path_1.default.basename(binaryPath)} ${args.join(' ')}\` failed: ${stderr.trim() || error.message}`, exitCode, stderr));
136
+ if (timeout > 0 && error.killed) {
137
+ reject(new ScannerError(`\`${command}\` timed out after ${timeout} ms.`, null, stderr));
138
+ return;
139
+ }
140
+ reject(new ScannerError(`\`${command}\` failed: ${stderr.trim() || error.message}`, exitCode, stderr));
135
141
  return;
136
142
  }
137
143
  try {
@@ -141,6 +147,9 @@ function runScanner(binaryPath, args) {
141
147
  reject(new ScannerError(`Could not parse the scanner's JSON output: ${parseError.message}`, null, stderr));
142
148
  }
143
149
  });
150
+ // The scanner never reads stdin. An executable without the scanner CLI would start the macro
151
+ // plugin server instead and wait on stdin forever.
152
+ child.stdin?.end();
144
153
  });
145
154
  }
146
155
  /**
@@ -160,7 +169,7 @@ async function scanModules(paths, options = {}) {
160
169
  args.push('--define', define);
161
170
  }
162
171
  args.push(...paths);
163
- const result = await runScanner(options.binaryPath ?? getScannerBinaryPath(), args);
172
+ const result = await runScanner(options.binaryPath ?? getScannerBinaryPath(), args, options.timeout);
164
173
  if (result.schemaVersion !== types_1.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION) {
165
174
  throw new ScannerSchemaVersionError('scan-modules', result.schemaVersion, types_1.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION);
166
175
  }
@@ -170,18 +179,17 @@ async function scanModules(paths, options = {}) {
170
179
  * Deep scan of the full JS-exported surface, for TypeScript type generation. Each path is a `.swift`
171
180
  * file or a directory, scanned recursively.
172
181
  *
173
- * Unlike `scanModules`, this doesn't evaluate `#if` blocks, so it takes no define option:
174
- * conditional declarations are reported as if their conditions held.
182
+ * Each declaration and member reports the Apple OSes whose `#if` conditions and `@available`
183
+ * attributes include it. `#if` is read only where the runtime sees what it holds: around types,
184
+ * `@Event` members, and enum cases. No define option is taken: a condition on a custom flag never holds, so
185
+ * what it guards is not reported.
175
186
  */
176
187
  async function scanExports(paths, options = {}) {
177
188
  if (paths.length === 0) {
178
189
  throw new TypeError('scanExports requires at least one path');
179
190
  }
180
191
  assertRepresentablePaths('scanExports', paths);
181
- const result = await runScanner(options.binaryPath ?? getScannerBinaryPath(), [
182
- 'scan-exports',
183
- ...paths,
184
- ]);
192
+ const result = await runScanner(options.binaryPath ?? getScannerBinaryPath(), ['scan-exports', ...paths], options.timeout);
185
193
  if (result.schemaVersion !== types_1.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION) {
186
194
  throw new ScannerSchemaVersionError('scan-exports', result.schemaVersion, types_1.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION);
187
195
  }