expo-modules-macros 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/macos.yml +16 -3
- package/AGENTS.md +7 -3
- package/Package.swift +10 -1
- package/README.md +3 -3
- 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 +206 -21
- 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/JSMacro.swift +174 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +176 -46
- package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
- package/apple/Sources/ExpoModulesMacrosCore/Support/DecorateModuleBuilder.swift +17 -3
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSConstructor.swift +11 -2
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +36 -5
- package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
- package/build/types.d.ts +58 -3
- package/package.json +5 -5
|
@@ -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.
|
|
@@ -533,8 +639,10 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
533
639
|
|
|
534
640
|
/// Projects a parameter clause into `ExportedParameter`s: label = first name, name = second (else
|
|
535
641
|
/// first), `optional` when it has a default value or an optional type, and the default as written.
|
|
642
|
+
/// A parameter that takes the JavaScript runtime (its type is written as `JavaScriptRuntime`) is left
|
|
643
|
+
/// out: the binding passes the runtime itself, and JavaScript doesn't see the parameter.
|
|
536
644
|
private func parameters(of clause: FunctionParameterClauseSyntax) -> [ExportedParameter] {
|
|
537
|
-
clause.parameters.map { parameter in
|
|
645
|
+
clause.parameters.filter { !takesRuntime($0) }.map { parameter in
|
|
538
646
|
let label = parameter.firstName.identifierName
|
|
539
647
|
let name = parameter.secondName?.identifierName ?? label
|
|
540
648
|
return ExportedParameter(
|
|
@@ -694,6 +802,35 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
694
802
|
return type.as(FunctionTypeSyntax.self)
|
|
695
803
|
}
|
|
696
804
|
|
|
805
|
+
/// True when a parameter takes the JavaScript runtime: its type, without specifiers such as `borrowing`,
|
|
806
|
+
/// is written as `JavaScriptRuntime` or `….JavaScriptRuntime`, or as an optional of it. This matches
|
|
807
|
+
/// `isRuntimeParameter` in the macros: a type that also carries attributes (`@Sendable`) isn't one.
|
|
808
|
+
private func takesRuntime(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
809
|
+
var type = parameter.type
|
|
810
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
811
|
+
guard attributed.attributes.isEmpty else {
|
|
812
|
+
return false
|
|
813
|
+
}
|
|
814
|
+
type = attributed.baseType
|
|
815
|
+
}
|
|
816
|
+
if let optional = type.as(OptionalTypeSyntax.self) {
|
|
817
|
+
type = optional.wrappedType
|
|
818
|
+
} else if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
819
|
+
type = implicitlyUnwrapped.wrappedType
|
|
820
|
+
} else if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == "Optional",
|
|
821
|
+
let argument = identifier.genericArgumentClause?.arguments.first, case .type(let wrapped) = argument.argument
|
|
822
|
+
{
|
|
823
|
+
type = wrapped
|
|
824
|
+
}
|
|
825
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
826
|
+
return identifier.name.text == "JavaScriptRuntime"
|
|
827
|
+
}
|
|
828
|
+
if let member = type.as(MemberTypeSyntax.self) {
|
|
829
|
+
return member.name.text == "JavaScriptRuntime"
|
|
830
|
+
}
|
|
831
|
+
return false
|
|
832
|
+
}
|
|
833
|
+
|
|
697
834
|
/// True when an event's function type returns `Void`. The shared `isVoidType` is not reused: it
|
|
698
835
|
/// accepts neither `Swift.Void` nor a parenthesized `(Void)`, so it would drop valid events.
|
|
699
836
|
private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
|
|
@@ -706,6 +843,44 @@ private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
|
|
|
706
843
|
return text == "Void" || text == "()" || text == "Swift.Void"
|
|
707
844
|
}
|
|
708
845
|
|
|
846
|
+
/// The function type a `@ViewProps` property declares, or `nil` when the type isn't a function.
|
|
847
|
+
/// Unwraps parentheses, attributes (`@Sendable`), and the optional spellings `T?`, `T!`, and
|
|
848
|
+
/// `Optional<T>`, like `underlyingFunctionType` in `ViewPropsMacro`.
|
|
849
|
+
private func viewEventFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax? {
|
|
850
|
+
if let functionType = type.as(FunctionTypeSyntax.self) {
|
|
851
|
+
return functionType
|
|
852
|
+
}
|
|
853
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first {
|
|
854
|
+
return viewEventFunctionType(of: only.type)
|
|
855
|
+
}
|
|
856
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
857
|
+
return viewEventFunctionType(of: attributed.baseType)
|
|
858
|
+
}
|
|
859
|
+
if let optional = type.as(OptionalTypeSyntax.self) {
|
|
860
|
+
return viewEventFunctionType(of: optional.wrappedType)
|
|
861
|
+
}
|
|
862
|
+
if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
863
|
+
return viewEventFunctionType(of: implicitlyUnwrapped.wrappedType)
|
|
864
|
+
}
|
|
865
|
+
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
866
|
+
identifier.name.text == "Optional",
|
|
867
|
+
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
868
|
+
case .type(let wrapped) = argument.argument
|
|
869
|
+
{
|
|
870
|
+
return viewEventFunctionType(of: wrapped)
|
|
871
|
+
}
|
|
872
|
+
return nil
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/// True when a `@ViewProps` event is written as an optional (`(() -> Void)?`, also after attributes),
|
|
876
|
+
/// which the macro rejects. Like the macros' `isOptionalType`, it reads only the outermost type.
|
|
877
|
+
private func isOptionalEventType(_ type: TypeSyntax) -> Bool {
|
|
878
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
879
|
+
return isOptionalEventType(attributed.baseType)
|
|
880
|
+
}
|
|
881
|
+
return isOptionalType(type)
|
|
882
|
+
}
|
|
883
|
+
|
|
709
884
|
/// True if an `@Event` attribute lists the given option member (`@Event(.inline)` or
|
|
710
885
|
/// `@Event("name", EventOptions.inline)`). Like `hasOption` in the macros, but without the array-literal
|
|
711
886
|
/// branch, which `EventOptions...` doesn't accept.
|
|
@@ -836,9 +1011,10 @@ private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
|
836
1011
|
}
|
|
837
1012
|
}
|
|
838
1013
|
|
|
839
|
-
/// True when a modifier excludes a property from being a `@Record` field
|
|
840
|
-
/// `private`, `fileprivate`, `lazy`), mirroring `
|
|
841
|
-
|
|
1014
|
+
/// True when a modifier excludes a property from being a `@Record` field or a `@ViewProps` prop
|
|
1015
|
+
/// (`static`, `class`, `private`, `fileprivate`, `lazy`), mirroring `isExcludedByModifier` in the
|
|
1016
|
+
/// macros.
|
|
1017
|
+
private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
842
1018
|
modifiers.contains { modifier in
|
|
843
1019
|
switch modifier.name.tokenKind {
|
|
844
1020
|
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
@@ -951,9 +1127,17 @@ func rawValueType(of clause: InheritanceClauseSyntax?) -> TypeNode? {
|
|
|
951
1127
|
return typeNode(from: first)
|
|
952
1128
|
}
|
|
953
1129
|
|
|
954
|
-
/// True when a type is written as an optional: `T?`, `T!`, or `Optional<T>`,
|
|
955
|
-
/// `isOptionalType`.
|
|
1130
|
+
/// True when a type is written as an optional: `T?`, `T!`, or `Optional<T>`, also in parentheses
|
|
1131
|
+
/// (`(T?)`) or behind attributes, mirroring the macros' `isOptionalType`.
|
|
956
1132
|
private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
1133
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
1134
|
+
only.firstName == nil
|
|
1135
|
+
{
|
|
1136
|
+
return isOptionalType(only.type)
|
|
1137
|
+
}
|
|
1138
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
1139
|
+
return isOptionalType(attributed.baseType)
|
|
1140
|
+
}
|
|
957
1141
|
if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
958
1142
|
return true
|
|
959
1143
|
}
|
|
@@ -963,13 +1147,14 @@ private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
|
963
1147
|
return false
|
|
964
1148
|
}
|
|
965
1149
|
|
|
966
|
-
/// The first attribute whose
|
|
967
|
-
///
|
|
1150
|
+
/// The first attribute whose name matches `name` as `attributeName(of:)` reads it, so
|
|
1151
|
+
/// `@ExpoView<CardProps>` and `@ExpoModulesCore.ExpoView<CardProps>` match `ExpoView`. A local copy of
|
|
1152
|
+
/// the macros' helper (the macro target can't be imported here).
|
|
968
1153
|
extension AttributeListSyntax {
|
|
969
1154
|
fileprivate func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
970
1155
|
for element in self {
|
|
971
1156
|
if let attribute = element.as(AttributeSyntax.self),
|
|
972
|
-
|
|
1157
|
+
attributeName(of: attribute) == name
|
|
973
1158
|
{
|
|
974
1159
|
return attribute
|
|
975
1160
|
}
|
|
@@ -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
|
|