expo-modules-macros 0.32.0 → 0.33.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/AGENTS.md +2 -1
- package/Package.swift +10 -1
- package/README.md +1 -1
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +124 -5
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +2 -1
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ResolveRefs.swift +76 -17
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +15 -8
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +174 -20
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +14 -6
- package/apple/Sources/ExpoModulesCLI/Core/Detection.swift +6 -0
- package/apple/Sources/ExpoModulesCLI/Core/DetectionVisitor.swift +41 -2
- package/apple/Sources/ExpoModulesCLI/Core/SourceScan.swift +3 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +64 -30
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +0 -11
- package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +15 -2
- package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
- package/build/types.d.ts +58 -3
- package/package.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
## Layout
|
|
6
6
|
|
|
7
|
-
- `Package.swift`: the SwiftPM package, at the root so that its identity is `expo-modules-macros` (an `apple/` manifest would have the identity `apple`, which conflicts with other packages that keep theirs in `apple/`). The targets name their paths under `apple/`. The `ExpoModulesMacrosPlugin` library product vends the macro target to Swift packages, and its name must differ from the target's. The test targets are declared only when `apple/Tests` exists, because the published npm package leaves it out.
|
|
7
|
+
- `Package.swift`: the SwiftPM package, at the root so that its identity is `expo-modules-macros` (an `apple/` manifest would have the identity `apple`, which conflicts with other packages that keep theirs in `apple/`). The targets name their paths under `apple/`. The `ExpoModulesMacrosPlugin` library product vends the macro target to Swift packages through a target of the same name, and its name must differ from the macro target's. The test targets are declared only when `apple/Tests` exists, because the published npm package leaves it out.
|
|
8
8
|
- `apple/Sources/ExpoModulesMacrosCore`: the macro implementations, in a library target so that both executables below can link them. `Macros/` holds one file per macro, and `Support/` the code that several macros share. `ProvidedMacros.swift` lists every macro with its attribute name and conformances in `ProvidedMacro.all`. `PluginServer.swift` runs the compiler plugin protocol with a provider that resolves the macros of `ProvidedMacro.all` by type name, because the default `CompilerPlugin` lookup expects them in a module named `ExpoModulesMacros`. It uses the `@_spi(PluginMessage)` API of swift-syntax.
|
|
9
9
|
- `apple/Sources/ExpoModulesMacros`: the `.macro` target that the `ExpoModulesMacrosPlugin` product vends to Swift packages. It only runs the plugin server, so Swift packages don't compile the CLI.
|
|
10
|
+
- `apple/Sources/ExpoModulesMacrosPlugin`: an empty target that the `ExpoModulesMacrosPlugin` product holds in place of the macro target, which it depends on. Xcode fails an iOS build when the product holds the macro target directly (see `Package.swift`).
|
|
10
11
|
- `apple/Sources/ExpoModulesMacrosTool`: the executable that ships as `apple/ExpoModulesMacros` and in the Windows and Linux packages. `main.swift` runs the plugin server without arguments and sends any invocation with arguments to the CLI.
|
|
11
12
|
- `apple/Sources/ExpoModulesCLI`: the library behind the command-line tool. `CLI.swift` parses the arguments, `Commands/` holds one directory per subcommand (`ScanModules/` for autolinking, `ScanExports/` for type generation, `Expand/` for `expand`, with `MacroExpander.swift`, which runs the macros in process, and `Fix/` for `fix`, which applies the Fix-Its that `expand` reports), and `Core/` holds the shared parsing, `#if` evaluation and output.
|
|
12
13
|
- `apple/Sources/ExpoModulesOptimized`: declarations for the `@OptimizedFunction` macro.
|
package/Package.swift
CHANGED
|
@@ -14,7 +14,7 @@ let package = Package(
|
|
|
14
14
|
// `#externalMacro(module: "ExpoModulesMacros", ...)` depends on this product, and the targets that
|
|
15
15
|
// depend on it get the plugin too. CocoaPods builds keep loading the prebuilt binary. The name
|
|
16
16
|
// differs from the macro target's, because SwiftPM already has a product of that name for it.
|
|
17
|
-
.library(name: "ExpoModulesMacrosPlugin", targets: ["
|
|
17
|
+
.library(name: "ExpoModulesMacrosPlugin", targets: ["ExpoModulesMacrosPlugin"]),
|
|
18
18
|
.executable(name: "ExpoModulesMacrosTool", targets: ["ExpoModulesMacrosTool"]),
|
|
19
19
|
],
|
|
20
20
|
dependencies: [
|
|
@@ -51,6 +51,15 @@ let package = Package(
|
|
|
51
51
|
],
|
|
52
52
|
path: "apple/Sources/ExpoModulesMacros"
|
|
53
53
|
),
|
|
54
|
+
// The target of the `ExpoModulesMacrosPlugin` product. The product must not hold the macro target
|
|
55
|
+
// itself: Xcode then passes the module map of `ExpoModulesMacrosCore`, which it builds for the
|
|
56
|
+
// host only, to the iOS target that depends on the product, and the build fails with
|
|
57
|
+
// `ExpoModulesMacrosCore.modulemap not found`.
|
|
58
|
+
.target(
|
|
59
|
+
name: "ExpoModulesMacrosPlugin",
|
|
60
|
+
dependencies: ["ExpoModulesMacros"],
|
|
61
|
+
path: "apple/Sources/ExpoModulesMacrosPlugin"
|
|
62
|
+
),
|
|
54
63
|
// The shipped binary, `apple/ExpoModulesMacros` and the Windows and Linux binaries. It is both the
|
|
55
64
|
// compiler plugin and the scanner CLI: the compiler launches it without arguments and speaks the
|
|
56
65
|
// plugin protocol over stdin, while an invocation with arguments dispatches into `ScannerCLI`
|
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ to `OTHER_SWIFT_FLAGS` for `ExpoModulesCore`, every pod that depends on it, and
|
|
|
180
180
|
|
|
181
181
|
## Swift packages
|
|
182
182
|
|
|
183
|
-
The package manifest is `Package.swift` at the root of this package, so a Swift package can depend on it by its path in `node_modules` or by the URL of this repository. Its identity is then `expo-modules-macros`. The `ExpoModulesMacrosPlugin` library product
|
|
183
|
+
The package manifest is `Package.swift` at the root of this package, so a Swift package can depend on it by its path in `node_modules` or by the URL of this repository. Its identity is then `expo-modules-macros`. The `ExpoModulesMacrosPlugin` library product gives the `ExpoModulesMacros` macro target to the targets that depend on it:
|
|
184
184
|
|
|
185
185
|
```swift
|
|
186
186
|
dependencies: [
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/// The deep-scan surface: the full JS-exported shape of every `@ExpoModule`, `@SharedObject`,
|
|
2
|
-
/// `@Record` type, with the per-member detail a
|
|
3
|
-
/// (like the macros): each boundary type becomes a
|
|
4
|
-
/// tagged tree rather than re-parsing Swift type
|
|
1
|
+
/// The deep-scan surface: the full JS-exported shape of every `@ExpoModule`, `@SharedObject`,
|
|
2
|
+
/// `@Record`, `@Union`, and `@ExpoView` type and every `Enumerable` enum, with the per-member detail a
|
|
3
|
+
/// TypeScript type generator needs. Read syntactically (like the macros): each boundary type becomes a
|
|
4
|
+
/// structured `TypeNode` tree so the consumer walks a tagged tree rather than re-parsing Swift type
|
|
5
|
+
/// syntax.
|
|
5
6
|
///
|
|
6
7
|
/// Every declaration and member carries the `platforms` that build it, and the ones that can be
|
|
7
8
|
/// written with an access modifier carry the `accessLevel` they spell. A declaration no platform
|
|
@@ -460,6 +461,122 @@ struct ExportedUnion: Encodable, Equatable {
|
|
|
460
461
|
let file: String
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
/// An `@ExpoView` class with the props and events of the `@ViewProps` struct in its generic argument.
|
|
465
|
+
/// It is reported on its own rather than under a module, because core reaches it through its
|
|
466
|
+
/// `AnyNativeView` conformance.
|
|
467
|
+
struct ExportedView: Encodable, Equatable {
|
|
468
|
+
/// The Swift class name. Only a view declared at file scope is reported, as in `scan-modules`.
|
|
469
|
+
let name: String
|
|
470
|
+
|
|
471
|
+
/// The name the view registers under. `@ExpoView` takes no name, so it is always the class name.
|
|
472
|
+
let jsName: String
|
|
473
|
+
|
|
474
|
+
/// The props type: the generic argument of `@ExpoView<CardProps>`, as written without backticks, or
|
|
475
|
+
/// the qualified name of the scanned `@ViewProps` struct it names (`CardView.Props`). A class without
|
|
476
|
+
/// the generic argument is not reported, because the macro rejects it.
|
|
477
|
+
var propsType: String
|
|
478
|
+
|
|
479
|
+
/// The name `propsType` is looked up under: its spelling without generic arguments, or `nil` when it
|
|
480
|
+
/// isn't a plain or qualified type name. Not encoded.
|
|
481
|
+
var propsTypeLookupName: String? = nil
|
|
482
|
+
|
|
483
|
+
/// The value props of the `@ViewProps` struct, or `nil` when the scan declares no struct named
|
|
484
|
+
/// `propsType` on a platform of the view. A struct shared by several views is copied into each of
|
|
485
|
+
/// them, and a struct declared in several `#if` branches contributes the props of each declaration
|
|
486
|
+
/// whose platforms overlap the view's.
|
|
487
|
+
var props: [ExportedViewProp]? = nil
|
|
488
|
+
|
|
489
|
+
/// The events of the `@ViewProps` struct, or `nil` when `props` is `nil`.
|
|
490
|
+
var events: [ExportedViewEvent]? = nil
|
|
491
|
+
|
|
492
|
+
/// The spelled access modifier, or `nil` when none is written.
|
|
493
|
+
let accessLevel: String?
|
|
494
|
+
|
|
495
|
+
let platforms: [ExportedPlatform]
|
|
496
|
+
|
|
497
|
+
let file: String
|
|
498
|
+
|
|
499
|
+
private enum CodingKeys: String, CodingKey {
|
|
500
|
+
case name, jsName, propsType, props, events, accessLevel, platforms, file
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// One value prop of a `@ViewProps` struct: a stored property that isn't function-typed. Like
|
|
505
|
+
/// `ExportedRecordProperty`, but with no `required`: core resets a prop that React leaves out to its
|
|
506
|
+
/// default, so JS can always omit one.
|
|
507
|
+
struct ExportedViewProp: Encodable, Equatable {
|
|
508
|
+
let name: String
|
|
509
|
+
|
|
510
|
+
/// The annotated type, or the type of a literal initial value. A property with neither is not
|
|
511
|
+
/// reported, because the macro rejects it.
|
|
512
|
+
var type: TypeNode
|
|
513
|
+
|
|
514
|
+
/// The initial value as source text, or `nil` when none is written.
|
|
515
|
+
let defaultValue: String?
|
|
516
|
+
|
|
517
|
+
/// The spelled access modifier, or `nil` when none is written. Never `private` or `fileprivate`,
|
|
518
|
+
/// since those exclude a property from the props.
|
|
519
|
+
let accessLevel: String?
|
|
520
|
+
|
|
521
|
+
let platforms: [ExportedPlatform]
|
|
522
|
+
|
|
523
|
+
/// Optional-typed, read from `type`, so every spelling of an optional counts (`T?`, `T!`,
|
|
524
|
+
/// `Optional<T>`, `(T?)`). Encoded as `optional`.
|
|
525
|
+
var isOptional: Bool {
|
|
526
|
+
if case .optional = type {
|
|
527
|
+
return true
|
|
528
|
+
}
|
|
529
|
+
return false
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private enum CodingKeys: String, CodingKey {
|
|
533
|
+
case name, type, defaultValue, accessLevel, platforms
|
|
534
|
+
case isOptional = "optional"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
func encode(to encoder: Encoder) throws {
|
|
538
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
539
|
+
try container.encode(name, forKey: .name)
|
|
540
|
+
try container.encode(type, forKey: .type)
|
|
541
|
+
try container.encode(isOptional, forKey: .isOptional)
|
|
542
|
+
try container.encodeIfPresent(defaultValue, forKey: .defaultValue)
|
|
543
|
+
try container.encodeIfPresent(accessLevel, forKey: .accessLevel)
|
|
544
|
+
try container.encode(platforms, forKey: .platforms)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/// One event of a `@ViewProps` struct: a function-typed stored property. Its name is the React prop
|
|
549
|
+
/// name as written, with no `on` prefix stripped, so it carries no `jsName`.
|
|
550
|
+
struct ExportedViewEvent: Encodable, Equatable {
|
|
551
|
+
/// The Swift property name, e.g. `onTap`.
|
|
552
|
+
let name: String
|
|
553
|
+
|
|
554
|
+
/// The type of the function's single parameter, or `nil` when it takes none (`() -> Void`) or more
|
|
555
|
+
/// than one.
|
|
556
|
+
var payload: TypeNode?
|
|
557
|
+
|
|
558
|
+
/// The spelled access modifier, or `nil` when none is written.
|
|
559
|
+
let accessLevel: String?
|
|
560
|
+
|
|
561
|
+
let platforms: [ExportedPlatform]
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/// A `@ViewProps` struct the scan found. Not encoded: `ExportedSurface.joiningViewProps(_:)` copies its
|
|
565
|
+
/// props and events into each view that names it, and a struct that no view names is not reported.
|
|
566
|
+
struct ViewPropsDeclaration: Equatable {
|
|
567
|
+
/// The Swift struct name, qualified with the types it is nested in (`CardView.Props`).
|
|
568
|
+
let name: String
|
|
569
|
+
|
|
570
|
+
/// The value props, in declaration order.
|
|
571
|
+
let props: [ExportedViewProp]
|
|
572
|
+
|
|
573
|
+
/// The events, in declaration order.
|
|
574
|
+
let events: [ExportedViewEvent]
|
|
575
|
+
|
|
576
|
+
/// The platforms that build the struct, to match it with the views that build on them.
|
|
577
|
+
let platforms: [ExportedPlatform]
|
|
578
|
+
}
|
|
579
|
+
|
|
463
580
|
/// The exported types grouped by kind, nested under `exports` in the result so the surface is one
|
|
464
581
|
/// self-contained object separate from `stats`.
|
|
465
582
|
struct ExportedSurface: Encodable, Equatable {
|
|
@@ -468,6 +585,7 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
468
585
|
let records: [ExportedRecord]
|
|
469
586
|
let enums: [ExportedEnum]
|
|
470
587
|
let unions: [ExportedUnion]
|
|
588
|
+
let views: [ExportedView]
|
|
471
589
|
}
|
|
472
590
|
|
|
473
591
|
/// How an `@Event` is dispatched to JavaScript, from its option. Mirrors `EventDispatch` in the macros.
|
|
@@ -488,7 +606,8 @@ enum ExportedEventDispatch: String, Encodable, Equatable {
|
|
|
488
606
|
/// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
|
|
489
607
|
/// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
|
|
490
608
|
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes; version 11
|
|
491
|
-
/// replaces the `sync` flag of an event with `dispatch`.
|
|
609
|
+
/// replaces the `sync` flag of an event with `dispatch`. A new field that older consumers can ignore,
|
|
610
|
+
/// such as `views`, doesn't change the version.
|
|
492
611
|
let scanExportsSchemaVersion = 11
|
|
493
612
|
|
|
494
613
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
@@ -74,26 +74,32 @@ struct RefIndex {
|
|
|
74
74
|
/// dropped one at a time (`Media.Status` -> `Status`): a use site may qualify a name with a module
|
|
75
75
|
/// the scan doesn't see, which is how the conformance and raw-type checks already match names.
|
|
76
76
|
func lookup(_ name: String, in scope: String) -> Entry? {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
return scopedLookup(name, in: scope, entries: entries)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// The entry that a use of `name` written inside the type `scope` names, from `entries` keyed by
|
|
82
|
+
/// qualified name. Swift's lookup rules, as `RefIndex.lookup(_:in:)` documents them.
|
|
83
|
+
private func scopedLookup<Entry>(_ name: String, in scope: String, entries: [String: Entry]) -> Entry? {
|
|
84
|
+
let name = name.hasPrefix("Self.") ? String(name.dropFirst("Self.".count)) : name
|
|
85
|
+
var components = scope.split(separator: ".").map(String.init)
|
|
86
|
+
while true {
|
|
87
|
+
if let entry = entries[(components + [name]).joined(separator: ".")] {
|
|
88
|
+
return entry
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if components.isEmpty {
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
components.removeLast()
|
|
94
|
+
}
|
|
95
|
+
var suffix = name.split(separator: ".").map(String.init).dropFirst()
|
|
96
|
+
while !suffix.isEmpty {
|
|
97
|
+
if let entry = entries[suffix.joined(separator: ".")] {
|
|
98
|
+
return entry
|
|
94
99
|
}
|
|
95
|
-
|
|
100
|
+
suffix = suffix.dropFirst()
|
|
96
101
|
}
|
|
102
|
+
return nil
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
/// The JS category a raw-value enum crosses as: its raw value's. A bare `Enumerable` conformance with
|
|
@@ -214,6 +220,59 @@ extension ExportedSurface {
|
|
|
214
220
|
return resolvedMember
|
|
215
221
|
}
|
|
216
222
|
return resolved
|
|
223
|
+
},
|
|
224
|
+
// The props and events are written in the `@ViewProps` struct, so their refs are looked up from
|
|
225
|
+
// it. `joiningViewProps(_:)` runs first and sets `propsType` to the struct's qualified name.
|
|
226
|
+
views: views.map { view in
|
|
227
|
+
var resolved = view
|
|
228
|
+
resolved.props = view.props?.map { prop in
|
|
229
|
+
var resolvedProp = prop
|
|
230
|
+
resolvedProp.type = prop.type.resolvingRefs(using: index, in: view.propsType)
|
|
231
|
+
return resolvedProp
|
|
232
|
+
}
|
|
233
|
+
resolved.events = view.events?.map { event in
|
|
234
|
+
var resolvedEvent = event
|
|
235
|
+
resolvedEvent.payload = event.payload?.resolvingRefs(using: index, in: view.propsType)
|
|
236
|
+
return resolvedEvent
|
|
237
|
+
}
|
|
238
|
+
return resolved
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// This surface with the props and events of each view's `@ViewProps` struct copied into the view.
|
|
244
|
+
/// The struct is looked up from the view like a ref, by its name without generic arguments, so it
|
|
245
|
+
/// may be declared in another file or nested (`CardView.Props`). A found struct gives the view its
|
|
246
|
+
/// qualified name as `propsType`. A view whose struct the scan doesn't declare keeps `props` and
|
|
247
|
+
/// `events` at `nil`, and `propsType` as written.
|
|
248
|
+
///
|
|
249
|
+
/// A struct declared more than once, in `#if` branches, contributes the props and events of each
|
|
250
|
+
/// declaration whose platforms overlap the platforms of the view, as a superclass does in
|
|
251
|
+
/// `mergingInheritedMembers()`. Each prop and event keeps its own `platforms`. A view that builds on
|
|
252
|
+
/// none of those platforms is treated as one whose struct the scan doesn't declare.
|
|
253
|
+
func joiningViewProps(_ declarations: [ViewPropsDeclaration]) -> ExportedSurface {
|
|
254
|
+
let entries = Dictionary(grouping: declarations, by: \.name)
|
|
255
|
+
return ExportedSurface(
|
|
256
|
+
modules: modules,
|
|
257
|
+
sharedObjects: sharedObjects,
|
|
258
|
+
records: records,
|
|
259
|
+
enums: enums,
|
|
260
|
+
unions: unions,
|
|
261
|
+
views: views.map { view in
|
|
262
|
+
let platforms = Set(view.platforms.map(\.name))
|
|
263
|
+
guard let name = view.propsTypeLookupName,
|
|
264
|
+
let variants = scopedLookup(name, in: view.name, entries: entries)?.filter({ declaration in
|
|
265
|
+
declaration.platforms.contains { platforms.contains($0.name) }
|
|
266
|
+
}),
|
|
267
|
+
let first = variants.first
|
|
268
|
+
else {
|
|
269
|
+
return view
|
|
270
|
+
}
|
|
271
|
+
var joined = view
|
|
272
|
+
joined.propsType = first.name
|
|
273
|
+
joined.props = variants.flatMap(\.props)
|
|
274
|
+
joined.events = variants.flatMap(\.events)
|
|
275
|
+
return joined
|
|
217
276
|
}
|
|
218
277
|
)
|
|
219
278
|
}
|
|
@@ -26,11 +26,11 @@ extension Scanner {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/// Scans `paths` for `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
30
|
-
/// enums, and returns their exported surface plus the run's stats.
|
|
31
|
-
/// tests can drive it without argv/stdout.
|
|
29
|
+
/// Scans `paths` for `@ExpoModule`, `@SharedObject`, `@Record`, `@Union`, `@ExpoView`, and
|
|
30
|
+
/// `@ViewProps` types plus `Enumerable` enums, and returns their exported surface plus the run's stats.
|
|
31
|
+
/// Separate from the public entry so tests can drive it without argv/stdout.
|
|
32
32
|
///
|
|
33
|
-
/// The pre-filter omits `@Event`: an event only declares a member of
|
|
33
|
+
/// The pre-filter omits `@Event`: an event only declares a member of a module or shared object, so a
|
|
34
34
|
/// file containing one already matches on its enclosing type. It does include `Enumerable`, which is a
|
|
35
35
|
/// conformance rather than an attribute: an enum is commonly declared in a file of its own, which no
|
|
36
36
|
/// macro attribute would match.
|
|
@@ -40,10 +40,12 @@ func scanExports(paths: [String]) -> ScanExportsResult {
|
|
|
40
40
|
var records: [ExportedRecord] = []
|
|
41
41
|
var enums: [ExportedEnum] = []
|
|
42
42
|
var unions: [ExportedUnion] = []
|
|
43
|
+
var views: [ExportedView] = []
|
|
44
|
+
var viewProps: [ViewPropsDeclaration] = []
|
|
43
45
|
|
|
44
46
|
let stats = scanFiles(
|
|
45
47
|
paths: paths,
|
|
46
|
-
macros: [.expoModule, .sharedObject, .record, .union],
|
|
48
|
+
macros: [.expoModule, .sharedObject, .record, .union, .expoView, .viewProps],
|
|
47
49
|
conformances: [enumerableConformanceName]
|
|
48
50
|
) { source, file in
|
|
49
51
|
let tree = Parser.parse(source: source)
|
|
@@ -54,18 +56,23 @@ func scanExports(paths: [String]) -> ScanExportsResult {
|
|
|
54
56
|
records.append(contentsOf: visitor.records)
|
|
55
57
|
enums.append(contentsOf: visitor.enums)
|
|
56
58
|
unions.append(contentsOf: visitor.unions)
|
|
59
|
+
views.append(contentsOf: visitor.views)
|
|
60
|
+
viewProps.append(contentsOf: visitor.viewProps)
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
// Refs resolve only once every file has been walked: a type parsed in one file may name a type
|
|
60
64
|
// declared in another, so the full set of declarations has to exist before any lookup is valid. The
|
|
61
|
-
// same holds for a
|
|
65
|
+
// same holds for a view's `@ViewProps` struct, which is joined before the refs resolve so that the
|
|
66
|
+
// refs in its props resolve from the struct, and for a superclass, so the inherited members are
|
|
67
|
+
// merged last.
|
|
62
68
|
let surface = ExportedSurface(
|
|
63
69
|
modules: modules,
|
|
64
70
|
sharedObjects: sharedObjects,
|
|
65
71
|
records: records,
|
|
66
72
|
enums: enums,
|
|
67
|
-
unions: unions
|
|
68
|
-
|
|
73
|
+
unions: unions,
|
|
74
|
+
views: views
|
|
75
|
+
).joiningViewProps(viewProps).resolvingRefs().mergingInheritedMembers()
|
|
69
76
|
|
|
70
77
|
return ScanExportsResult(
|
|
71
78
|
schemaVersion: scanExportsSchemaVersion,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
/// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`,
|
|
4
|
-
/// their `@JS` members
|
|
5
|
-
/// Recognition is purely syntactic and re-reads
|
|
6
|
-
/// imported), so it stays in step with `JSFunction` /
|
|
7
|
-
/// `RecordProperty`.
|
|
3
|
+
/// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
4
|
+
/// `@ExpoView`, and `@ViewProps` type: their `@JS` members, record properties, view props, and view
|
|
5
|
+
/// events. The deep counterpart to `DetectionVisitor`. Recognition is purely syntactic and re-reads
|
|
6
|
+
/// what the macros read (the macro target can't be imported), so it stays in step with `JSFunction` /
|
|
7
|
+
/// `JSProperty` / `JSConstructor` / `RecordProperty` / `validatedViewProps`.
|
|
8
8
|
///
|
|
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
|
|
@@ -12,10 +12,10 @@ import SwiftSyntax
|
|
|
12
12
|
///
|
|
13
13
|
/// Each declaration and member reports the platforms that build it (see `PlatformResolver`), and one
|
|
14
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`,
|
|
16
|
-
/// `@Union` read only the members written directly in the type body, so `@JS`
|
|
17
|
-
/// properties,
|
|
18
|
-
/// expands on its own declaration, and the compiler reads enum cases itself.
|
|
15
|
+
/// around types, `@Event` members, and enum cases. `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
16
|
+
/// `@Union`, and `@ViewProps` read only the members written directly in the type body, so `@JS`
|
|
17
|
+
/// members, record properties, union cases, and view props in a `#if` clause are never bound and are
|
|
18
|
+
/// not reported. An `@Event` expands on its own declaration, and the compiler reads enum cases itself.
|
|
19
19
|
final class SurfaceVisitor: SyntaxVisitor {
|
|
20
20
|
private let file: String
|
|
21
21
|
private let platformResolver: PlatformResolver
|
|
@@ -24,6 +24,10 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
24
24
|
private(set) var records: [ExportedRecord] = []
|
|
25
25
|
private(set) var enums: [ExportedEnum] = []
|
|
26
26
|
private(set) var unions: [ExportedUnion] = []
|
|
27
|
+
private(set) var views: [ExportedView] = []
|
|
28
|
+
/// The `@ViewProps` structs, which `ExportedSurface.joiningViewProps(_:)` copies into the views once
|
|
29
|
+
/// every file has been walked.
|
|
30
|
+
private(set) var viewProps: [ViewPropsDeclaration] = []
|
|
27
31
|
|
|
28
32
|
init(file: String, tree: SourceFileSyntax) {
|
|
29
33
|
self.file = file
|
|
@@ -120,8 +124,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/// Routes a class or struct to the right collector based on which Expo macro it carries. A type
|
|
123
|
-
/// carrying none of them is ignored.
|
|
124
|
-
///
|
|
127
|
+
/// carrying none of them is ignored. The macros are mutually exclusive in practice, so the first
|
|
128
|
+
/// match wins.
|
|
125
129
|
///
|
|
126
130
|
/// `name` is the declared name and `scope` the names of the types enclosing it. The reported `name`
|
|
127
131
|
/// is qualified with the scope, while a default `jsName` is the declared name alone, as the macros
|
|
@@ -201,6 +205,38 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
201
205
|
file: file
|
|
202
206
|
)
|
|
203
207
|
)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// `@ExpoView` applies only to a class, and needs the props type as its generic argument.
|
|
212
|
+
if node.is(ClassDeclSyntax.self),
|
|
213
|
+
let attribute = attributes.firstAttribute(named: DetectedMacro.expoView.rawValue),
|
|
214
|
+
let propsType = genericArgumentType(of: attribute)
|
|
215
|
+
{
|
|
216
|
+
// Only a file-scope view is reported, as `scan-modules` reports only those.
|
|
217
|
+
guard scope.isEmpty else {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
// `props` and `events` are filled in by `ExportedSurface.joiningViewProps(_:)`, since the
|
|
221
|
+
// `@ViewProps` struct can be declared in another file.
|
|
222
|
+
views.append(
|
|
223
|
+
ExportedView(
|
|
224
|
+
name: qualified,
|
|
225
|
+
jsName: name,
|
|
226
|
+
propsType: String(propsType.trimmedDescription.filter { $0 != "`" }),
|
|
227
|
+
propsTypeLookupName: spellingWithoutGenericArguments(of: propsType),
|
|
228
|
+
accessLevel: accessLevel,
|
|
229
|
+
platforms: platforms,
|
|
230
|
+
file: file
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// `@ViewProps` applies only to a struct.
|
|
237
|
+
if node.is(StructDeclSyntax.self), attributes.firstAttribute(named: DetectedMacro.viewProps.rawValue) != nil {
|
|
238
|
+
let (props, events) = collectViewProps(members)
|
|
239
|
+
viewProps.append(ViewPropsDeclaration(name: qualified, props: props, events: events, platforms: platforms))
|
|
204
240
|
}
|
|
205
241
|
}
|
|
206
242
|
|
|
@@ -425,7 +461,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
425
461
|
// `@Record` reads only the properties written directly in the body, so `#if` is not looked into.
|
|
426
462
|
for member in members {
|
|
427
463
|
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
428
|
-
!
|
|
464
|
+
!isExcludedByModifier(varDecl.modifiers)
|
|
429
465
|
else {
|
|
430
466
|
continue
|
|
431
467
|
}
|
|
@@ -459,6 +495,76 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
459
495
|
return properties
|
|
460
496
|
}
|
|
461
497
|
|
|
498
|
+
/// The value props and events of a `@ViewProps` struct, mirroring `validatedViewProps` in the macro:
|
|
499
|
+
/// every binding of a `var` or `let` that no modifier excludes, except a computed one. A property
|
|
500
|
+
/// with `willSet`/`didSet` or a `set` counts, as in the macro. A function-typed property is an event,
|
|
501
|
+
/// and every other one a value prop.
|
|
502
|
+
///
|
|
503
|
+
/// The scan stays lenient where the macro reports an error, and leaves out only what can't be
|
|
504
|
+
/// described: a property whose type can't be determined, an optional function type, and an event
|
|
505
|
+
/// that doesn't return `Void`. A value prop the macro rejects for another reason, such as a `let` or
|
|
506
|
+
/// a missing default, is still reported.
|
|
507
|
+
private func collectViewProps(
|
|
508
|
+
_ members: MemberBlockItemListSyntax
|
|
509
|
+
) -> (props: [ExportedViewProp], events: [ExportedViewEvent]) {
|
|
510
|
+
var props: [ExportedViewProp] = []
|
|
511
|
+
var events: [ExportedViewEvent] = []
|
|
512
|
+
|
|
513
|
+
// `@ViewProps` reads only the properties written directly in the body, so `#if` is not looked into.
|
|
514
|
+
for member in members {
|
|
515
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
516
|
+
!isExcludedByModifier(varDecl.modifiers)
|
|
517
|
+
else {
|
|
518
|
+
continue
|
|
519
|
+
}
|
|
520
|
+
let platforms = platformResolver.platforms(of: varDecl)
|
|
521
|
+
guard !platforms.isEmpty else {
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
for binding in varDecl.bindings {
|
|
525
|
+
if binding.accessorBlock != nil && !isSettable(binding: binding, isLet: false) {
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
let name = ident.identifier.identifierName
|
|
532
|
+
let annotation = binding.typeAnnotation?.type
|
|
533
|
+
|
|
534
|
+
if let annotation, let functionType = viewEventFunctionType(of: annotation) {
|
|
535
|
+
// An optional function type and a non-`Void` result are errors in the macro.
|
|
536
|
+
guard !isOptionalEventType(annotation), isVoidEventReturn(functionType.returnClause.type) else {
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
539
|
+
let parameters = functionType.parameters
|
|
540
|
+
events.append(
|
|
541
|
+
ExportedViewEvent(
|
|
542
|
+
name: name,
|
|
543
|
+
payload: parameters.count == 1 ? parameters.first.map { typeNode(from: $0.type) } : nil,
|
|
544
|
+
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
545
|
+
platforms: platforms
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
guard let type = valueTypeNode(annotation: annotation, initializer: binding.initializer?.value) else {
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
props.append(
|
|
555
|
+
ExportedViewProp(
|
|
556
|
+
name: name,
|
|
557
|
+
type: type,
|
|
558
|
+
defaultValue: binding.initializer?.value.trimmedDescription,
|
|
559
|
+
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
560
|
+
platforms: platforms
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return (props, events)
|
|
566
|
+
}
|
|
567
|
+
|
|
462
568
|
/// The declared cases of an enum, in source order. One `case` declaration can introduce several
|
|
463
569
|
/// cases (`case a, b`), so each element is read separately. A case carrying associated values is
|
|
464
570
|
/// skipped: it has no raw value, so it can't cross the boundary as one.
|
|
@@ -706,6 +812,44 @@ private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
|
|
|
706
812
|
return text == "Void" || text == "()" || text == "Swift.Void"
|
|
707
813
|
}
|
|
708
814
|
|
|
815
|
+
/// The function type a `@ViewProps` property declares, or `nil` when the type isn't a function.
|
|
816
|
+
/// Unwraps parentheses, attributes (`@Sendable`), and the optional spellings `T?`, `T!`, and
|
|
817
|
+
/// `Optional<T>`, like `underlyingFunctionType` in `ViewPropsMacro`.
|
|
818
|
+
private func viewEventFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax? {
|
|
819
|
+
if let functionType = type.as(FunctionTypeSyntax.self) {
|
|
820
|
+
return functionType
|
|
821
|
+
}
|
|
822
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first {
|
|
823
|
+
return viewEventFunctionType(of: only.type)
|
|
824
|
+
}
|
|
825
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
826
|
+
return viewEventFunctionType(of: attributed.baseType)
|
|
827
|
+
}
|
|
828
|
+
if let optional = type.as(OptionalTypeSyntax.self) {
|
|
829
|
+
return viewEventFunctionType(of: optional.wrappedType)
|
|
830
|
+
}
|
|
831
|
+
if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
832
|
+
return viewEventFunctionType(of: implicitlyUnwrapped.wrappedType)
|
|
833
|
+
}
|
|
834
|
+
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
835
|
+
identifier.name.text == "Optional",
|
|
836
|
+
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
837
|
+
case .type(let wrapped) = argument.argument
|
|
838
|
+
{
|
|
839
|
+
return viewEventFunctionType(of: wrapped)
|
|
840
|
+
}
|
|
841
|
+
return nil
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// True when a `@ViewProps` event is written as an optional (`(() -> Void)?`, also after attributes),
|
|
845
|
+
/// which the macro rejects. Like the macros' `isOptionalType`, it reads only the outermost type.
|
|
846
|
+
private func isOptionalEventType(_ type: TypeSyntax) -> Bool {
|
|
847
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
848
|
+
return isOptionalEventType(attributed.baseType)
|
|
849
|
+
}
|
|
850
|
+
return isOptionalType(type)
|
|
851
|
+
}
|
|
852
|
+
|
|
709
853
|
/// True if an `@Event` attribute lists the given option member (`@Event(.inline)` or
|
|
710
854
|
/// `@Event("name", EventOptions.inline)`). Like `hasOption` in the macros, but without the array-literal
|
|
711
855
|
/// branch, which `EventOptions...` doesn't accept.
|
|
@@ -836,9 +980,10 @@ private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
|
836
980
|
}
|
|
837
981
|
}
|
|
838
982
|
|
|
839
|
-
/// True when a modifier excludes a property from being a `@Record` field
|
|
840
|
-
/// `private`, `fileprivate`, `lazy`), mirroring `
|
|
841
|
-
|
|
983
|
+
/// True when a modifier excludes a property from being a `@Record` field or a `@ViewProps` prop
|
|
984
|
+
/// (`static`, `class`, `private`, `fileprivate`, `lazy`), mirroring `isExcludedByModifier` in the
|
|
985
|
+
/// macros.
|
|
986
|
+
private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
842
987
|
modifiers.contains { modifier in
|
|
843
988
|
switch modifier.name.tokenKind {
|
|
844
989
|
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
@@ -951,9 +1096,17 @@ func rawValueType(of clause: InheritanceClauseSyntax?) -> TypeNode? {
|
|
|
951
1096
|
return typeNode(from: first)
|
|
952
1097
|
}
|
|
953
1098
|
|
|
954
|
-
/// True when a type is written as an optional: `T?`, `T!`, or `Optional<T>`,
|
|
955
|
-
/// `isOptionalType`.
|
|
1099
|
+
/// True when a type is written as an optional: `T?`, `T!`, or `Optional<T>`, also in parentheses
|
|
1100
|
+
/// (`(T?)`) or behind attributes, mirroring the macros' `isOptionalType`.
|
|
956
1101
|
private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
1102
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
1103
|
+
only.firstName == nil
|
|
1104
|
+
{
|
|
1105
|
+
return isOptionalType(only.type)
|
|
1106
|
+
}
|
|
1107
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
1108
|
+
return isOptionalType(attributed.baseType)
|
|
1109
|
+
}
|
|
957
1110
|
if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
958
1111
|
return true
|
|
959
1112
|
}
|
|
@@ -963,13 +1116,14 @@ private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
|
963
1116
|
return false
|
|
964
1117
|
}
|
|
965
1118
|
|
|
966
|
-
/// The first attribute whose
|
|
967
|
-
///
|
|
1119
|
+
/// The first attribute whose name matches `name` as `attributeName(of:)` reads it, so
|
|
1120
|
+
/// `@ExpoView<CardProps>` and `@ExpoModulesCore.ExpoView<CardProps>` match `ExpoView`. A local copy of
|
|
1121
|
+
/// the macros' helper (the macro target can't be imported here).
|
|
968
1122
|
extension AttributeListSyntax {
|
|
969
1123
|
fileprivate func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
970
1124
|
for element in self {
|
|
971
1125
|
if let attribute = element.as(AttributeSyntax.self),
|
|
972
|
-
|
|
1126
|
+
attributeName(of: attribute) == name
|
|
973
1127
|
{
|
|
974
1128
|
return attribute
|
|
975
1129
|
}
|
|
@@ -34,8 +34,8 @@ public enum Scanner {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/// One class in the `scan-modules` output: an `@ExpoModule` class in `modules`,
|
|
38
|
-
/// class in `sharedObjects`. Trimmed to what `expo-modules-autolinking` needs to register the class:
|
|
37
|
+
/// One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, a `@SharedObject`
|
|
38
|
+
/// class in `sharedObjects`, or an `@ExpoView` class in `views`. Trimmed to what `expo-modules-autolinking` needs to register the class:
|
|
39
39
|
/// the Swift class name, the JS name it registers under, the platforms that include it, and the file
|
|
40
40
|
/// it's in. The richer fields the visitor captures (declaration kind, raw macro arguments,
|
|
41
41
|
/// line/column) are dropped here, because this command doesn't need them (the macro is always on a
|
|
@@ -45,7 +45,7 @@ struct ScannedClass: Codable, Equatable {
|
|
|
45
45
|
let name: String
|
|
46
46
|
|
|
47
47
|
/// The fully-resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override when
|
|
48
|
-
/// present, otherwise the class name. Resolved here (rather than left `nil`) so it matches how the
|
|
48
|
+
/// present, otherwise the class name. `@ExpoView` takes no name, so a view's JS name is its class name. Resolved here (rather than left `nil`) so it matches how the
|
|
49
49
|
/// macro derives the name and the consumer never has to apply the fallback itself.
|
|
50
50
|
let jsName: String
|
|
51
51
|
|
|
@@ -70,7 +70,7 @@ struct ScannedClass: Codable, Equatable {
|
|
|
70
70
|
/// `ScannedClass`, so `expo-modules-autolinking` can verify it understands the output before
|
|
71
71
|
/// trusting it (and fall back to config-declared modules when it doesn't). Version 2 added the
|
|
72
72
|
/// per-module `platforms` list and made the module list platform-agnostic. A new field that older
|
|
73
|
-
/// consumers can ignore, such as `sharedObjects`, doesn't change the version.
|
|
73
|
+
/// consumers can ignore, such as `sharedObjects` or `views`, doesn't change the version.
|
|
74
74
|
let scanModulesSchemaVersion = 2
|
|
75
75
|
|
|
76
76
|
/// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
|
|
@@ -84,6 +84,10 @@ struct ScanModulesResult: Codable, Equatable {
|
|
|
84
84
|
/// `modules`.
|
|
85
85
|
let sharedObjects: [ScannedClass]
|
|
86
86
|
|
|
87
|
+
/// The top-level `@ExpoView` classes, in the same order and with the same platform rules as
|
|
88
|
+
/// `modules`.
|
|
89
|
+
let views: [ScannedClass]
|
|
90
|
+
|
|
87
91
|
/// Warnings for `#if` conditions the scan couldn't answer statically (see `ScanWarning`). Carried
|
|
88
92
|
/// in the report rather than on stderr so the consumer can attach them to its own output.
|
|
89
93
|
let warnings: [ScanWarning]
|
|
@@ -102,7 +106,7 @@ private let platformUniverse: [(condition: String, reported: String)] = [
|
|
|
102
106
|
(condition: "visionOS", reported: "visionOS"),
|
|
103
107
|
]
|
|
104
108
|
|
|
105
|
-
/// Scans the given paths for top-level `@ExpoModule` and `@
|
|
109
|
+
/// Scans the given paths for top-level `@ExpoModule`, `@SharedObject` and `@ExpoView` types and returns every one
|
|
106
110
|
/// found in any
|
|
107
111
|
/// `#if` branch (in file then source order), each with the platforms whose builds include it, plus
|
|
108
112
|
/// the stats for the run — the `scan-modules` command. Kept separate from the public entry (and
|
|
@@ -115,10 +119,11 @@ private let platformUniverse: [(condition: String, reported: String)] = [
|
|
|
115
119
|
func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResult {
|
|
116
120
|
var modules: [ScannedClass] = []
|
|
117
121
|
var sharedObjects: [ScannedClass] = []
|
|
122
|
+
var views: [ScannedClass] = []
|
|
118
123
|
var warnings: [ScanWarning] = []
|
|
119
124
|
var seenWarnings = Set<ScanWarning>()
|
|
120
125
|
|
|
121
|
-
let macros: Set<DetectedMacro> = [.expoModule, .sharedObject]
|
|
126
|
+
let macros: Set<DetectedMacro> = [.expoModule, .sharedObject, .expoView]
|
|
122
127
|
let stats = scanFiles(paths: paths, macros: macros) { source, file in
|
|
123
128
|
let tree = Parser.parse(source: source)
|
|
124
129
|
|
|
@@ -162,6 +167,8 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
162
167
|
switch detection.macro {
|
|
163
168
|
case .sharedObject:
|
|
164
169
|
sharedObjects.append(scanned)
|
|
170
|
+
case .expoView:
|
|
171
|
+
views.append(scanned)
|
|
165
172
|
default:
|
|
166
173
|
modules.append(scanned)
|
|
167
174
|
}
|
|
@@ -172,6 +179,7 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
172
179
|
schemaVersion: scanModulesSchemaVersion,
|
|
173
180
|
modules: modules,
|
|
174
181
|
sharedObjects: sharedObjects,
|
|
182
|
+
views: views,
|
|
175
183
|
warnings: warnings,
|
|
176
184
|
stats: stats
|
|
177
185
|
)
|
|
@@ -7,6 +7,8 @@ enum DetectedMacro: String, Codable, CaseIterable {
|
|
|
7
7
|
case sharedObject = "SharedObject"
|
|
8
8
|
case record = "Record"
|
|
9
9
|
case union = "Union"
|
|
10
|
+
case expoView = "ExpoView"
|
|
11
|
+
case viewProps = "ViewProps"
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/// A single argument passed to a macro, e.g. `"Foo"` or `classes: [Bar.self]`. The label is `nil`
|
|
@@ -43,6 +45,10 @@ struct Detection: Codable, Equatable {
|
|
|
43
45
|
/// `@SharedObject("Baz")`, otherwise `nil` (the name defaults to `name` at expansion time).
|
|
44
46
|
let jsName: String?
|
|
45
47
|
|
|
48
|
+
/// The generic argument of the attribute as written, which is the props type of `@ExpoView<CardProps>`,
|
|
49
|
+
/// otherwise `nil`.
|
|
50
|
+
var genericArgument: String? = nil
|
|
51
|
+
|
|
46
52
|
/// Every argument passed to the macro, in source order, e.g. `@ExpoModule("Foo", classes: [Bar.self])`
|
|
47
53
|
/// yields a positional `"Foo"` and a `classes:` argument. Empty when the macro is written bare.
|
|
48
54
|
let arguments: [MacroArgument]
|
|
@@ -3,7 +3,7 @@ import SwiftIfConfig
|
|
|
3
3
|
import SwiftSyntax
|
|
4
4
|
|
|
5
5
|
/// Walks a parsed source file and records top-level declarations carrying `@ExpoModule`, `@JS`,
|
|
6
|
-
/// `@SharedObject`, or `@
|
|
6
|
+
/// `@SharedObject`, `@Record`, `@Union`, `@ExpoView`, or `@ViewProps`. Only file-scope declarations are considered: these macros apply to
|
|
7
7
|
/// top-level types, so descending into type and function bodies would only surface false positives.
|
|
8
8
|
/// Recognition mirrors the macros themselves — a purely syntactic match on the spelled attribute
|
|
9
9
|
/// name — so it sees the same declarations the compiler would hand the plugin, without compiling
|
|
@@ -119,7 +119,7 @@ final class DetectionVisitor: SyntaxVisitor {
|
|
|
119
119
|
) {
|
|
120
120
|
for element in attributes {
|
|
121
121
|
guard let attribute = element.as(AttributeSyntax.self),
|
|
122
|
-
let macro = DetectedMacro(rawValue: attribute
|
|
122
|
+
let macro = DetectedMacro(rawValue: attributeName(of: attribute)),
|
|
123
123
|
detectedMacros.contains(macro)
|
|
124
124
|
else {
|
|
125
125
|
continue
|
|
@@ -132,6 +132,7 @@ final class DetectionVisitor: SyntaxVisitor {
|
|
|
132
132
|
declarationKind: kind,
|
|
133
133
|
accessLevel: accessLevel(of: modifiers),
|
|
134
134
|
jsName: stringArgument(of: attribute),
|
|
135
|
+
genericArgument: genericArgument(of: attribute),
|
|
135
136
|
arguments: arguments(of: attribute),
|
|
136
137
|
file: file,
|
|
137
138
|
line: location.line,
|
|
@@ -155,6 +156,44 @@ private func accessLevel(of modifiers: DeclModifierListSyntax) -> String {
|
|
|
155
156
|
return "internal"
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
/// The attribute's name without a generic argument clause, so `@ExpoView<CardProps>` is `ExpoView`. A
|
|
160
|
+
/// name qualified with the module that declares the macros is read without the module, so
|
|
161
|
+
/// `@ExpoModulesCore.ExpoView<CardProps>` is `ExpoView` too.
|
|
162
|
+
func attributeName(of attribute: AttributeSyntax) -> String {
|
|
163
|
+
if let identifier = attribute.attributeName.as(IdentifierTypeSyntax.self) {
|
|
164
|
+
return identifier.name.text
|
|
165
|
+
}
|
|
166
|
+
if let member = attribute.attributeName.as(MemberTypeSyntax.self),
|
|
167
|
+
member.baseType.as(IdentifierTypeSyntax.self)?.name.text == macrosModuleName
|
|
168
|
+
{
|
|
169
|
+
return member.name.text
|
|
170
|
+
}
|
|
171
|
+
return attribute.attributeName.trimmedDescription
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The module that declares the macros, which an attribute may be qualified with:
|
|
175
|
+
/// `@ExpoModulesCore.ExpoView<CardProps>`.
|
|
176
|
+
let macrosModuleName = "ExpoModulesCore"
|
|
177
|
+
|
|
178
|
+
/// The first generic argument of the attribute as written, e.g. `@ExpoView<CardProps>` -> "CardProps".
|
|
179
|
+
/// Returns `nil` when the attribute has no generic argument clause.
|
|
180
|
+
private func genericArgument(of attribute: AttributeSyntax) -> String? {
|
|
181
|
+
return genericArgumentType(of: attribute)?.trimmedDescription
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// The first generic argument of the attribute, `CardProps` in `@ExpoView<CardProps>` and in
|
|
185
|
+
/// `@ExpoModulesCore.ExpoView<CardProps>`, or `nil` when the attribute has no generic argument clause.
|
|
186
|
+
/// Like `genericArgument(of:)` in `ExpoViewMacro`.
|
|
187
|
+
func genericArgumentType(of attribute: AttributeSyntax) -> TypeSyntax? {
|
|
188
|
+
let clause =
|
|
189
|
+
attribute.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause
|
|
190
|
+
?? attribute.attributeName.as(MemberTypeSyntax.self)?.genericArgumentClause
|
|
191
|
+
guard let first = clause?.arguments.first, case .type(let type) = first.argument else {
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
return type
|
|
195
|
+
}
|
|
196
|
+
|
|
158
197
|
/// Every argument passed to the attribute, in source order, each as a label (or `nil` when
|
|
159
198
|
/// positional) plus the value expression's source text. Returns an empty array when the attribute
|
|
160
199
|
/// is written bare (`@ExpoModule`) or with empty parens.
|
|
@@ -106,9 +106,10 @@ struct MacroPrefilter {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/// A pre-filter for attributes that `DetectedMacro` doesn't list, such as every macro that `expand`
|
|
109
|
-
/// runs.
|
|
109
|
+
/// runs. Each name also matches qualified with the module that declares the macros
|
|
110
|
+
/// (`@ExpoModulesCore.ExpoView`), since the scanners read that spelling as the bare one.
|
|
110
111
|
init(attributeNames: Set<String>, conformances: Set<String> = []) {
|
|
111
|
-
attributes = attributeNames.
|
|
112
|
+
attributes = attributeNames.flatMap { [Array($0.utf8), Array("\(macrosModuleName).\($0)".utf8)] }
|
|
112
113
|
bareNames = conformances.map { Array($0.utf8) }
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -3,11 +3,11 @@ import SwiftSyntax
|
|
|
3
3
|
import SwiftSyntaxBuilder
|
|
4
4
|
import SwiftSyntaxMacros
|
|
5
5
|
|
|
6
|
-
/// Member macro applied to an `
|
|
6
|
+
/// Member and extension macro applied to an `ExpoNativeView` subclass, binding it to its props type:
|
|
7
7
|
///
|
|
8
8
|
/// @ExpoView<CardProps>
|
|
9
|
-
/// final class CardView:
|
|
10
|
-
///
|
|
9
|
+
/// final class CardView: ExpoNativeView {
|
|
10
|
+
/// func didUpdateProps(_ diff: CardProps.Diff) {
|
|
11
11
|
/// if diff.changed(.color) {
|
|
12
12
|
/// backgroundColor = props.color
|
|
13
13
|
/// }
|
|
@@ -21,23 +21,25 @@ import SwiftSyntaxMacros
|
|
|
21
21
|
/// compile error at the author's own line ("requires that 'X' conform to 'AnyViewProps'")
|
|
22
22
|
/// rather than a macro diagnostic. This macro therefore validates nothing about the props type.
|
|
23
23
|
///
|
|
24
|
-
/// The
|
|
24
|
+
/// The expansion is a typealias and a conformance:
|
|
25
25
|
///
|
|
26
26
|
/// public typealias Props = CardProps
|
|
27
|
+
/// extension CardView: AnyNativeView {}
|
|
27
28
|
///
|
|
28
|
-
///
|
|
29
|
-
/// `
|
|
30
|
-
///
|
|
31
|
-
///
|
|
32
|
-
///
|
|
33
|
-
/// static, neither needing a builder to be evaluated.
|
|
29
|
+
/// There is **no view definition**: no `ViewDefinition`, no `View(…) { }` builder, no `Props(_:)` or
|
|
30
|
+
/// `Events(_:)` element. Core reaches the view through the `AnyNativeView` conformance: it reads the
|
|
31
|
+
/// props type from the `Props` typealias, which satisfies the protocol's associated type, and the
|
|
32
|
+
/// event names from the props type's own `_eventNames` (synthesized by `@ViewProps`). Both are
|
|
33
|
+
/// static, so no builder has to be evaluated.
|
|
34
34
|
///
|
|
35
|
-
/// Views also get no `_decorateView` hook
|
|
36
|
-
///
|
|
37
|
-
///
|
|
35
|
+
/// Views also get no `_decorateView` hook: `_decorateModule` and `_decorateSharedObject` exist to bind
|
|
36
|
+
/// members into a JS object, and a view has no such object. Props reach a view through Fabric's
|
|
37
|
+
/// mounting layer, not a JS call, so a view's runtime entry point is the props update hook.
|
|
38
38
|
///
|
|
39
|
-
/// `didUpdateProps` is a core
|
|
40
|
-
|
|
39
|
+
/// `didUpdateProps` is an `AnyNativeView` requirement with a default in core, not synthesized here.
|
|
40
|
+
/// It isn't an `override`: `ExpoNativeView` has no such method, because its parameter type depends on
|
|
41
|
+
/// the props type.
|
|
42
|
+
public struct ExpoViewMacro: MemberMacro, ExtensionMacro {
|
|
41
43
|
public static func expansion(
|
|
42
44
|
of node: AttributeSyntax,
|
|
43
45
|
providingMembersOf declaration: some DeclGroupSyntax,
|
|
@@ -87,38 +89,66 @@ public struct ExpoViewMacro: MemberMacro {
|
|
|
87
89
|
"""
|
|
88
90
|
]
|
|
89
91
|
}
|
|
92
|
+
|
|
93
|
+
public static func expansion(
|
|
94
|
+
of node: AttributeSyntax,
|
|
95
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
96
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
97
|
+
conformingTo protocols: [TypeSyntax],
|
|
98
|
+
in context: some MacroExpansionContext
|
|
99
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
100
|
+
// A class that the member macro rejects gets no conformance either: the member macro has already
|
|
101
|
+
// reported the error, and a conformance without the `Props` typealias would only add a second
|
|
102
|
+
// error about the associated type.
|
|
103
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
104
|
+
inheritsFromAny(classDecl, names: expoViewBaseClassNames),
|
|
105
|
+
classDecl.genericParameterClause == nil,
|
|
106
|
+
existingPropsMemberKind(in: classDecl) == nil,
|
|
107
|
+
genericArgument(of: node) != nil,
|
|
108
|
+
!inheritsProtocol(named: nativeViewProtocolName, in: declaration)
|
|
109
|
+
else {
|
|
110
|
+
return []
|
|
111
|
+
}
|
|
112
|
+
let ext: DeclSyntax = """
|
|
113
|
+
extension \(type.trimmed): \(raw: nativeViewProtocolName) {}
|
|
114
|
+
"""
|
|
115
|
+
guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
return [extDecl]
|
|
119
|
+
}
|
|
90
120
|
}
|
|
91
121
|
|
|
92
122
|
// MARK: - Diagnostics
|
|
93
123
|
|
|
94
|
-
/// The error for a class that doesn't name `
|
|
124
|
+
/// The error for a class that doesn't name `ExpoNativeView` in its inheritance clause, carrying a fix-it
|
|
95
125
|
/// that adds it.
|
|
96
126
|
///
|
|
97
127
|
/// The fix-it only applies where the edit is unambiguous. With no inheritance clause at all it
|
|
98
|
-
/// inserts `:
|
|
128
|
+
/// inserts `: ExpoNativeView`; with an existing clause it prepends `ExpoNativeView` as the first entry, since
|
|
99
129
|
/// Swift requires the superclass to come before any protocol. A class that already names some other
|
|
100
130
|
/// superclass gets the diagnostic without a fix-it: replacing that superclass is a decision the macro
|
|
101
131
|
/// can't make (see the indirect-inheritance note on `expoViewBaseClassNames`).
|
|
102
132
|
private func missingBaseClassDiagnostic(for classDecl: ClassDeclSyntax) -> Diagnostic {
|
|
103
133
|
let message = ExpoViewDiagnosticMessage(
|
|
104
|
-
"@ExpoView class must inherit from
|
|
134
|
+
"@ExpoView class must inherit from ExpoNativeView. Add `: ExpoNativeView` to the class declaration.",
|
|
105
135
|
id: "expoview-missing-base-class"
|
|
106
136
|
)
|
|
107
137
|
var fixIts: [FixIt] = []
|
|
108
138
|
|
|
109
139
|
if classDecl.inheritanceClause == nil {
|
|
110
|
-
// `class CardView {` → `class CardView:
|
|
140
|
+
// `class CardView {` → `class CardView: ExpoNativeView {`. The name carries the space before `{` as
|
|
111
141
|
// trailing trivia, so move it onto the clause to keep the spacing.
|
|
112
142
|
let name = classDecl.name
|
|
113
143
|
let clause = InheritanceClauseSyntax(
|
|
114
144
|
colon: .colonToken(trailingTrivia: .space),
|
|
115
145
|
inheritedTypes: InheritedTypeListSyntax([
|
|
116
|
-
InheritedTypeSyntax(type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("
|
|
146
|
+
InheritedTypeSyntax(type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("ExpoNativeView"))))
|
|
117
147
|
])
|
|
118
148
|
)
|
|
119
149
|
fixIts.append(
|
|
120
150
|
FixIt(
|
|
121
|
-
message: ExpoViewFixItMessage("Inherit from '
|
|
151
|
+
message: ExpoViewFixItMessage("Inherit from 'ExpoNativeView'", id: "expoview-add-base-class"),
|
|
122
152
|
changes: [
|
|
123
153
|
.replace(
|
|
124
154
|
oldNode: Syntax(classDecl),
|
|
@@ -132,16 +162,16 @@ private func missingBaseClassDiagnostic(for classDecl: ClassDeclSyntax) -> Diagn
|
|
|
132
162
|
)
|
|
133
163
|
)
|
|
134
164
|
} else if let clause = classDecl.inheritanceClause, inheritsOnlyProtocolsByConvention(clause) {
|
|
135
|
-
// `class CardView: Identifiable {` → `class CardView:
|
|
165
|
+
// `class CardView: Identifiable {` → `class CardView: ExpoNativeView, Identifiable {`.
|
|
136
166
|
var inherited = clause.inheritedTypes
|
|
137
167
|
if var first = inherited.first {
|
|
138
168
|
first.trailingComma = .commaToken(trailingTrivia: .space)
|
|
139
|
-
first.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("
|
|
169
|
+
first.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("ExpoNativeView")))
|
|
140
170
|
inherited.insert(first, at: inherited.startIndex)
|
|
141
171
|
}
|
|
142
172
|
fixIts.append(
|
|
143
173
|
FixIt(
|
|
144
|
-
message: ExpoViewFixItMessage("Inherit from '
|
|
174
|
+
message: ExpoViewFixItMessage("Inherit from 'ExpoNativeView'", id: "expoview-add-base-class"),
|
|
145
175
|
changes: [
|
|
146
176
|
.replace(
|
|
147
177
|
oldNode: Syntax(clause),
|
|
@@ -157,7 +187,7 @@ private func missingBaseClassDiagnostic(for classDecl: ClassDeclSyntax) -> Diagn
|
|
|
157
187
|
|
|
158
188
|
/// Whether every entry in the clause looks like a protocol rather than a superclass, by the
|
|
159
189
|
/// capitalization-free heuristic that a Swift superclass must come first: if the first entry is one
|
|
160
|
-
/// the macro would be replacing, prepending `
|
|
190
|
+
/// the macro would be replacing, prepending `ExpoNativeView` would produce two superclasses. A macro can't
|
|
161
191
|
/// resolve names, so this only reports true when the clause is empty of anything that could be a
|
|
162
192
|
/// base class the author chose deliberately. Conservative by design: a wrong guess here would emit a
|
|
163
193
|
/// fix-it that doesn't compile.
|
|
@@ -223,14 +253,14 @@ private func existingPropsMemberKind(in classDecl: ClassDeclSyntax) -> String? {
|
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
/// Base classes an `@ExpoView` may inherit from. The check is syntactic, matching the name in the
|
|
226
|
-
/// class's own inheritance clause: both `:
|
|
256
|
+
/// class's own inheritance clause: both `: ExpoNativeView` and a qualified `: ExpoModulesCore.ExpoNativeView`
|
|
227
257
|
/// pass, since `baseIdentifier` reads the trailing name.
|
|
228
258
|
///
|
|
229
|
-
/// Indirect inheritance does not: a view whose superclass is itself an `
|
|
230
|
-
/// (`class Base:
|
|
259
|
+
/// Indirect inheritance does not: a view whose superclass is itself an `ExpoNativeView` subclass
|
|
260
|
+
/// (`class Base: ExpoNativeView` then `class CardView: Base`) is rejected, because a macro can't resolve
|
|
231
261
|
/// the superclass to check. `@SharedObject` has the same limitation, through the same helper. The
|
|
232
|
-
/// workaround is to apply `@ExpoView` to the class that names `
|
|
233
|
-
private let expoViewBaseClassNames: Set<String> = ["
|
|
262
|
+
/// workaround is to apply `@ExpoView` to the class that names `ExpoNativeView` directly.
|
|
263
|
+
private let expoViewBaseClassNames: Set<String> = ["ExpoNativeView"]
|
|
234
264
|
|
|
235
265
|
/// The attribute's single generic argument, verbatim (`@ExpoView<CardProps>` → `"CardProps"`), or
|
|
236
266
|
/// `nil` when the attribute carries no generic argument clause.
|
|
@@ -257,3 +287,7 @@ private func genericArgument(of attribute: AttributeSyntax) -> String? {
|
|
|
257
287
|
}
|
|
258
288
|
return type.trimmedDescription
|
|
259
289
|
}
|
|
290
|
+
|
|
291
|
+
/// The core protocol that `@ExpoView` conforms the class to. Core registers the view and decodes its
|
|
292
|
+
/// props through it.
|
|
293
|
+
private let nativeViewProtocolName = "AnyNativeView"
|
|
@@ -861,17 +861,6 @@ private func isVoidType(_ type: TypeSyntax) -> Bool {
|
|
|
861
861
|
return ["Void", "Swift.Void"].contains(type.trimmedDescription)
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
/// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
|
|
865
|
-
/// label would make it a tuple, which Swift rejects with a single element anyway.
|
|
866
|
-
private func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
|
|
867
|
-
guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
868
|
-
only.firstName == nil
|
|
869
|
-
else {
|
|
870
|
-
return type
|
|
871
|
-
}
|
|
872
|
-
return withoutParentheses(only.type)
|
|
873
|
-
}
|
|
874
|
-
|
|
875
864
|
/// True if the binding is computed with a `mutating get`.
|
|
876
865
|
private func hasMutatingGetter(_ binding: PatternBindingSyntax) -> Bool {
|
|
877
866
|
guard case .accessors(let accessors) = binding.accessorBlock?.accessors else {
|
|
@@ -25,7 +25,7 @@ public struct ProvidedMacro {
|
|
|
25
25
|
conformances: ["JavaScriptDecodable", "JavaScriptEncodable"]
|
|
26
26
|
),
|
|
27
27
|
ProvidedMacro(name: "ViewProps", type: ViewPropsMacro.self, conformances: ["AnyViewProps"]),
|
|
28
|
-
ProvidedMacro(name: "ExpoView", type: ExpoViewMacro.self),
|
|
28
|
+
ProvidedMacro(name: "ExpoView", type: ExpoViewMacro.self, conformances: ["AnyNativeView"]),
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -111,9 +111,11 @@ internal func hasAsyncVariant(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
111
111
|
return asyncVariantOption(of: attribute) != nil
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T
|
|
115
|
-
///
|
|
114
|
+
/// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`, also in
|
|
115
|
+
/// parentheses (`(T?)`), which Swift treats as the same type. Used to decide argument requiredness (an
|
|
116
|
+
/// optional parameter may be omitted) and record-field nullability.
|
|
116
117
|
internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
118
|
+
let type = withoutParentheses(type)
|
|
117
119
|
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
118
120
|
return isOptionalType(attributed.baseType)
|
|
119
121
|
}
|
|
@@ -159,6 +161,17 @@ internal struct SpecifiedType {
|
|
|
159
161
|
/// a fresh decoded value satisfies either way.
|
|
160
162
|
internal let ownershipSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
|
|
161
163
|
|
|
164
|
+
/// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
|
|
165
|
+
/// label would make it a tuple, which Swift rejects with a single element anyway.
|
|
166
|
+
internal func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
|
|
167
|
+
guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
168
|
+
only.firstName == nil
|
|
169
|
+
else {
|
|
170
|
+
return type
|
|
171
|
+
}
|
|
172
|
+
return withoutParentheses(only.type)
|
|
173
|
+
}
|
|
174
|
+
|
|
162
175
|
/// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
|
|
163
176
|
/// optional.
|
|
164
177
|
internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Empty on purpose: this target only carries the dependency on the macro target (see Package.swift).
|
package/build/types.d.ts
CHANGED
|
@@ -368,6 +368,58 @@ export interface ExportedUnion extends ExportedAccess, ExportedAvailability {
|
|
|
368
368
|
members: ExportedUnionMember[];
|
|
369
369
|
file: string;
|
|
370
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* One value prop of a `@ViewProps` struct: a stored property that isn't function-typed. It has no
|
|
373
|
+
* `required`: native code resets a prop that React leaves out to its default, so JS can always omit
|
|
374
|
+
* one.
|
|
375
|
+
*/
|
|
376
|
+
export interface ExportedViewProp extends ExportedAccess, ExportedAvailability {
|
|
377
|
+
name: string;
|
|
378
|
+
type: TypeNode;
|
|
379
|
+
/** Optional-typed: true exactly when `type` is an `optional` node. */
|
|
380
|
+
optional: boolean;
|
|
381
|
+
/** The initial value as Swift source text, absent when none is written. */
|
|
382
|
+
defaultValue?: string;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* One event of a `@ViewProps` struct: a function-typed stored property. Its name is the React prop
|
|
386
|
+
* name as written, with no `on` prefix stripped, so it has no `jsName`.
|
|
387
|
+
*/
|
|
388
|
+
export interface ExportedViewEvent extends ExportedAccess, ExportedAvailability {
|
|
389
|
+
/** The Swift property name, e.g. `onTap`. */
|
|
390
|
+
name: string;
|
|
391
|
+
/**
|
|
392
|
+
* The type of the function's single parameter, absent when it takes none (`() -> Void`) or more
|
|
393
|
+
* than one.
|
|
394
|
+
*/
|
|
395
|
+
payload?: TypeNode;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* An `@ExpoView` class with the props and events of the `@ViewProps` struct in its generic argument.
|
|
399
|
+
* It is reported on its own rather than under a module.
|
|
400
|
+
*/
|
|
401
|
+
export interface ExportedView extends ExportedAccess, ExportedAvailability {
|
|
402
|
+
/** The Swift class name. Only a view declared at file scope is reported. */
|
|
403
|
+
name: string;
|
|
404
|
+
/** The name the view registers under. `@ExpoView` takes no name, so it is always the class name. */
|
|
405
|
+
jsName: string;
|
|
406
|
+
/**
|
|
407
|
+
* The props type: the generic argument of `@ExpoView<CardProps>`, as written without backticks
|
|
408
|
+
* (`ExternalProps<Int>`), or the qualified name of the scanned `@ViewProps` struct it names
|
|
409
|
+
* (`CardView.Props`).
|
|
410
|
+
*/
|
|
411
|
+
propsType: string;
|
|
412
|
+
/**
|
|
413
|
+
* The value props of the `@ViewProps` struct, in declaration order. Absent when the scan declares
|
|
414
|
+
* no struct named `propsType` on a platform of the view, so unknown props differ from no props. A
|
|
415
|
+
* struct declared in several `#if` branches contributes the props of each declaration whose
|
|
416
|
+
* platforms overlap the view's, and each prop keeps its own `platforms`.
|
|
417
|
+
*/
|
|
418
|
+
props?: ExportedViewProp[];
|
|
419
|
+
/** The events of the `@ViewProps` struct, in declaration order. Absent when `props` is absent. */
|
|
420
|
+
events?: ExportedViewEvent[];
|
|
421
|
+
file: string;
|
|
422
|
+
}
|
|
371
423
|
/** The exported types grouped by kind. */
|
|
372
424
|
export interface ExportedSurface {
|
|
373
425
|
modules: ExportedModule[];
|
|
@@ -375,6 +427,7 @@ export interface ExportedSurface {
|
|
|
375
427
|
records: ExportedRecord[];
|
|
376
428
|
enums: ExportedEnum[];
|
|
377
429
|
unions: ExportedUnion[];
|
|
430
|
+
views: ExportedView[];
|
|
378
431
|
}
|
|
379
432
|
/** A `#if` condition the scan couldn't answer statically. */
|
|
380
433
|
export interface ScanWarning {
|
|
@@ -392,14 +445,14 @@ export interface ScanStats {
|
|
|
392
445
|
durationMs: number;
|
|
393
446
|
}
|
|
394
447
|
/**
|
|
395
|
-
* One class in the `scan-modules` output: an `@ExpoModule` class in `modules`,
|
|
396
|
-
* class in `sharedObjects`.
|
|
448
|
+
* One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, a `@SharedObject`
|
|
449
|
+
* class in `sharedObjects`, or an `@ExpoView` class in `views`.
|
|
397
450
|
*/
|
|
398
451
|
export interface ScannedClass {
|
|
399
452
|
name: string;
|
|
400
453
|
/**
|
|
401
454
|
* The resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override, else the class
|
|
402
|
-
* name.
|
|
455
|
+
* name. A view's JS name is its class name.
|
|
403
456
|
*/
|
|
404
457
|
jsName: string;
|
|
405
458
|
/**
|
|
@@ -422,6 +475,8 @@ export interface ScanModulesResult {
|
|
|
422
475
|
modules: ScannedClass[];
|
|
423
476
|
/** The top-level `@SharedObject` classes, with the same platform rules as `modules`. */
|
|
424
477
|
sharedObjects: ScannedClass[];
|
|
478
|
+
/** The top-level `@ExpoView` classes, with the same platform rules as `modules`. */
|
|
479
|
+
views: ScannedClass[];
|
|
425
480
|
warnings: ScanWarning[];
|
|
426
481
|
stats: ScanStats;
|
|
427
482
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Swift macro plugin for Expo modules",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typescript": "^5.4.0"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"@expo/modules-macros-linux-arm64": "0.
|
|
30
|
-
"@expo/modules-macros-linux-x64": "0.
|
|
31
|
-
"@expo/modules-macros-win32-arm64": "0.
|
|
32
|
-
"@expo/modules-macros-win32-x64": "0.
|
|
29
|
+
"@expo/modules-macros-linux-arm64": "0.33.0",
|
|
30
|
+
"@expo/modules-macros-linux-x64": "0.33.0",
|
|
31
|
+
"@expo/modules-macros-win32-arm64": "0.33.0",
|
|
32
|
+
"@expo/modules-macros-win32-x64": "0.33.0"
|
|
33
33
|
}
|
|
34
34
|
}
|