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
|
@@ -39,6 +39,10 @@ import SwiftSyntaxMacros
|
|
|
39
39
|
/// event without an initial value an empty closure (`{}`, `{ _ in }`, ...) and leaves the value
|
|
40
40
|
/// props at their initial values. It is never generated when the type declares its own `init()`.
|
|
41
41
|
/// Like any initializer in the struct body, the generated one removes the memberwise initializer.
|
|
42
|
+
/// - `_resetViewProp(_:in:)`: sets one value prop back to its default, by assigning the property's
|
|
43
|
+
/// initial value or `nil`, so a reset evaluates only that prop's default. It is a static member of
|
|
44
|
+
/// the struct body, because an initial value can name a static member without `Self.` or a member of
|
|
45
|
+
/// an enclosing type, which an instance method or the extension can't.
|
|
42
46
|
///
|
|
43
47
|
/// An event's function type must return `Void`, because JavaScript can't return a value to it.
|
|
44
48
|
///
|
|
@@ -47,7 +51,16 @@ import SwiftSyntaxMacros
|
|
|
47
51
|
/// - `allProps`: every bit set, the changed set on the first application.
|
|
48
52
|
/// - `propSet(for:)`: one name's bit, for folding raw changed keys into the mask.
|
|
49
53
|
/// - `_decode(_:from:in:)`: assigns one value prop from a JavaScript value. `null` and `undefined`
|
|
50
|
-
/// reset it
|
|
54
|
+
/// reset it with `_resetViewProp(_:in:)`, and any other value decodes through
|
|
55
|
+
/// `JavaScriptDecodable`.
|
|
56
|
+
///
|
|
57
|
+
/// `_decode` and `_resetViewProp` run on the JavaScript actor. The other generated types and members
|
|
58
|
+
/// and the conformance are `nonisolated`, so core can use them on any thread also when the module
|
|
59
|
+
/// builds with a default isolation, such as `-default-isolation MainActor`. The props' own
|
|
60
|
+
/// declarations still take that isolation, so a struct with an event prop, a value prop whose type
|
|
61
|
+
/// isn't `Sendable`, or a computed prop needs `nonisolated` on its declaration, and the author's
|
|
62
|
+
/// `init()` must be `nonisolated` too. So must a conformance that the author declares
|
|
63
|
+
/// (`: nonisolated AnyViewProps`).
|
|
51
64
|
///
|
|
52
65
|
/// Author-facing shape — no conformance to spell out:
|
|
53
66
|
///
|
|
@@ -83,6 +96,9 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
83
96
|
if let initializer = defaultInitializer(props: props) {
|
|
84
97
|
members.append(initializer)
|
|
85
98
|
}
|
|
99
|
+
if let reset = resetFunction(valueProps: props.valueProps) {
|
|
100
|
+
members.append(reset)
|
|
101
|
+
}
|
|
86
102
|
return members
|
|
87
103
|
}
|
|
88
104
|
|
|
@@ -120,29 +136,33 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
120
136
|
? "0"
|
|
121
137
|
: "0b" + String(repeating: "1", count: props.valueProps.count)
|
|
122
138
|
|
|
123
|
-
// With no value prop
|
|
139
|
+
// With no value prop, `PropName` has no case, so the function can't be called and its body is
|
|
140
|
+
// empty, like the one of `_decode`. Swift reports any statement in it, also an empty `switch`, as
|
|
141
|
+
// "will never be executed". The body starts with a line break, so the empty one closes on the
|
|
142
|
+
// next line.
|
|
124
143
|
let propSetBody: String
|
|
125
144
|
if props.valueProps.isEmpty {
|
|
126
|
-
propSetBody = "
|
|
145
|
+
propSetBody = ""
|
|
127
146
|
} else {
|
|
128
147
|
let cases = props.valueProps
|
|
129
148
|
.map { " case .\($0.name):\n return .\($0.name)" }
|
|
130
149
|
.joined(separator: "\n")
|
|
131
|
-
propSetBody = " switch name {\n\(cases)\n }"
|
|
150
|
+
propSetBody = "\n switch name {\n\(cases)\n }"
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
let decodeBody = decodeFunctionBody(valueProps: props.valueProps)
|
|
135
154
|
|
|
136
|
-
|
|
155
|
+
// `nonisolated` keeps the conformance and its members usable on the JavaScript thread when the
|
|
156
|
+
// module builds with a default isolation, which would otherwise isolate them to the main actor.
|
|
157
|
+
let conformanceClause = alreadyConforms ? "" : ": nonisolated \(viewPropsProtocolName)"
|
|
137
158
|
let ext: DeclSyntax = """
|
|
138
159
|
extension \(type.trimmed)\(raw: conformanceClause) {
|
|
139
|
-
public static let allProps = PropSet(rawValue: \(raw: allPropsMask))
|
|
160
|
+
nonisolated public static let allProps = PropSet(rawValue: \(raw: allPropsMask))
|
|
140
161
|
|
|
141
162
|
/// `@inlinable` so core's raw-key fold can inline the lookup across the module boundary:
|
|
142
163
|
/// it runs once per changed key per props batch.
|
|
143
164
|
@inlinable
|
|
144
|
-
public static func propSet(for name: PropName) -> PropSet {
|
|
145
|
-
\(raw: propSetBody)
|
|
165
|
+
nonisolated public static func propSet(for name: PropName) -> PropSet {\(raw: propSetBody)
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
@JavaScriptActor
|
|
@@ -488,9 +508,11 @@ private struct ViewProp {
|
|
|
488
508
|
/// The property's declared type, verbatim, or the type of its literal initial value. `_decode`
|
|
489
509
|
/// doesn't spell it: the decoded type is inferred from the assignment.
|
|
490
510
|
let type: String
|
|
491
|
-
/// Whether the prop is computed with a `mutating get`, which `
|
|
511
|
+
/// Whether the prop is computed with a `mutating get`, which `_resetViewProp` can only read from a
|
|
492
512
|
/// mutable value.
|
|
493
513
|
let hasMutatingGetter: Bool
|
|
514
|
+
/// What `_resetViewProp` assigns when React removes the prop. Events aren't reset.
|
|
515
|
+
let defaultValue: PropDefault
|
|
494
516
|
|
|
495
517
|
/// The name with any escaping backticks removed: the JS-visible key, so the enum's raw value and
|
|
496
518
|
/// `_eventNames` both read `"default"`, never `` "`default`" ``.
|
|
@@ -499,6 +521,49 @@ private struct ViewProp {
|
|
|
499
521
|
}
|
|
500
522
|
}
|
|
501
523
|
|
|
524
|
+
/// The value that a value prop goes back to when React removes it. Each one equals the prop's value
|
|
525
|
+
/// in `Self()`.
|
|
526
|
+
private enum PropDefault {
|
|
527
|
+
/// The initial value of a stored property, as written. Evaluating it again gives the value that
|
|
528
|
+
/// `Self()` gives the property.
|
|
529
|
+
case initialValue(ExprSyntax)
|
|
530
|
+
/// The implicit `nil` of a `var` whose type is written as `T?` or `T!`.
|
|
531
|
+
case implicitNil
|
|
532
|
+
/// The prop's value in a new `Self()`, when the macro can't tell it from the declaration: the
|
|
533
|
+
/// struct declares its own `init()`, which can set any value, the prop is computed, the property
|
|
534
|
+
/// has an attribute, such as a property wrapper, whose value after `init(wrappedValue:)` can differ
|
|
535
|
+
/// from its initial value, or the initial value has a `#` expression, such as `#function` or
|
|
536
|
+
/// `#line`, whose value depends on where it's evaluated.
|
|
537
|
+
case newInstance
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/// The default of a property with the given binding, for a struct that does or doesn't declare its own
|
|
541
|
+
/// `init()`.
|
|
542
|
+
private func propDefault(
|
|
543
|
+
of binding: PatternBindingSyntax,
|
|
544
|
+
in varDecl: VariableDeclSyntax,
|
|
545
|
+
declaredType: TypeSyntax?,
|
|
546
|
+
declaresUsableInitializer: Bool
|
|
547
|
+
) -> PropDefault {
|
|
548
|
+
if declaresUsableInitializer || !isStoredBinding(binding) || !varDecl.attributes.isEmpty {
|
|
549
|
+
return .newInstance
|
|
550
|
+
}
|
|
551
|
+
if let initializer = binding.initializer {
|
|
552
|
+
// A macro expansion, also a literal such as `#function`, can depend on where it's evaluated, so
|
|
553
|
+
// a copy in `_resetViewProp` could give another value.
|
|
554
|
+
let hasPoundExpression = initializer.value.tokens(viewMode: .sourceAccurate).contains {
|
|
555
|
+
$0.tokenKind == .pound
|
|
556
|
+
}
|
|
557
|
+
return hasPoundExpression ? .newInstance : .initialValue(initializer.value)
|
|
558
|
+
}
|
|
559
|
+
if varDecl.bindingSpecifier.tokenKind == .keyword(.var), let declaredType, hasImplicitNilDefault(declaredType) {
|
|
560
|
+
return .implicitNil
|
|
561
|
+
}
|
|
562
|
+
// A value prop without a default is reported, so this is only reached for an event, which isn't
|
|
563
|
+
// reset.
|
|
564
|
+
return .newInstance
|
|
565
|
+
}
|
|
566
|
+
|
|
502
567
|
/// An event prop declared without an initial value, which the generated `init()` assigns.
|
|
503
568
|
private struct EventDefault {
|
|
504
569
|
/// The property name as written, backticks included.
|
|
@@ -664,7 +729,17 @@ private func validatedViewProps(
|
|
|
664
729
|
// Only stored storage takes an initial value; a computed prop with a setter needs none.
|
|
665
730
|
let needsInitialValue =
|
|
666
731
|
binding.initializer == nil && isStoredBinding(binding) && !declaresUsableInitializer
|
|
667
|
-
let prop = ViewProp(
|
|
732
|
+
let prop = ViewProp(
|
|
733
|
+
name: name,
|
|
734
|
+
type: resolvedType,
|
|
735
|
+
hasMutatingGetter: hasMutatingGetter(binding),
|
|
736
|
+
defaultValue: propDefault(
|
|
737
|
+
of: binding,
|
|
738
|
+
in: varDecl,
|
|
739
|
+
declaredType: declaredType,
|
|
740
|
+
declaresUsableInitializer: declaresUsableInitializer
|
|
741
|
+
)
|
|
742
|
+
)
|
|
668
743
|
if let eventFunctionType {
|
|
669
744
|
// An event is dispatched to JavaScript, which can't hand a value back to the caller.
|
|
670
745
|
if !returnsVoid(eventFunctionType) {
|
|
@@ -861,17 +936,6 @@ private func isVoidType(_ type: TypeSyntax) -> Bool {
|
|
|
861
936
|
return ["Void", "Swift.Void"].contains(type.trimmedDescription)
|
|
862
937
|
}
|
|
863
938
|
|
|
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
939
|
/// True if the binding is computed with a `mutating get`.
|
|
876
940
|
private func hasMutatingGetter(_ binding: PatternBindingSyntax) -> Bool {
|
|
877
941
|
guard case .accessors(let accessors) = binding.accessorBlock?.accessors else {
|
|
@@ -937,6 +1001,10 @@ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax?
|
|
|
937
1001
|
/// Emitted even when empty, so a props type with only events still conforms. An enum with no cases
|
|
938
1002
|
/// can't declare a raw type, so the empty one conforms to `RawRepresentable` by hand, with `String`
|
|
939
1003
|
/// as its `RawValue` like the others.
|
|
1004
|
+
///
|
|
1005
|
+
/// The enum is `@frozen`, so the `switch` in the `@inlinable` `propSet(for:)` compiles in a module
|
|
1006
|
+
/// built with library evolution, where it would otherwise need a case for unknown values. The cases
|
|
1007
|
+
/// and their order are already part of the ABI, as the bits of `PropSet`.
|
|
940
1008
|
private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
941
1009
|
// An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
|
|
942
1010
|
// the wire key visible in the generated source and independent of that implicit rule.
|
|
@@ -950,7 +1018,7 @@ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
950
1018
|
.joined(separator: "\n")
|
|
951
1019
|
if valueProps.isEmpty {
|
|
952
1020
|
return """
|
|
953
|
-
public enum PropName: RawRepresentable, CaseIterable {
|
|
1021
|
+
@frozen nonisolated public enum PropName: RawRepresentable, CaseIterable {
|
|
954
1022
|
public init?(rawValue: String) {
|
|
955
1023
|
return nil
|
|
956
1024
|
}
|
|
@@ -963,7 +1031,7 @@ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
963
1031
|
"""
|
|
964
1032
|
}
|
|
965
1033
|
return """
|
|
966
|
-
public enum PropName: String, CaseIterable {
|
|
1034
|
+
@frozen nonisolated public enum PropName: String, CaseIterable {
|
|
967
1035
|
\(raw: cases)
|
|
968
1036
|
}
|
|
969
1037
|
"""
|
|
@@ -981,7 +1049,7 @@ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
981
1049
|
|
|
982
1050
|
if valueProps.isEmpty {
|
|
983
1051
|
return """
|
|
984
|
-
public struct PropSet: OptionSet, Sendable {
|
|
1052
|
+
nonisolated public struct PropSet: OptionSet, Sendable {
|
|
985
1053
|
public let rawValue: UInt64
|
|
986
1054
|
|
|
987
1055
|
public init(rawValue: UInt64) {
|
|
@@ -991,7 +1059,7 @@ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
991
1059
|
"""
|
|
992
1060
|
}
|
|
993
1061
|
return """
|
|
994
|
-
public struct PropSet: OptionSet, Sendable {
|
|
1062
|
+
nonisolated public struct PropSet: OptionSet, Sendable {
|
|
995
1063
|
public let rawValue: UInt64
|
|
996
1064
|
|
|
997
1065
|
public init(rawValue: UInt64) {
|
|
@@ -1009,31 +1077,23 @@ private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
1009
1077
|
private func eventNamesConstant(eventProps: [ViewProp]) -> DeclSyntax {
|
|
1010
1078
|
let names = eventProps.map { "\"\($0.wireName)\"" }.joined(separator: ", ")
|
|
1011
1079
|
return """
|
|
1012
|
-
public static let _eventNames: [String] = [\(raw: names)]
|
|
1080
|
+
nonisolated public static let _eventNames: [String] = [\(raw: names)]
|
|
1013
1081
|
"""
|
|
1014
1082
|
}
|
|
1015
1083
|
|
|
1016
1084
|
/// The body of `_decode`, which core calls once per changed prop. React sends a removed prop as
|
|
1017
|
-
/// `null`, so `null` and `undefined` reset the prop
|
|
1018
|
-
///
|
|
1019
|
-
/// static member of `AnyViewProps`, reached through `Self.`, so a prop
|
|
1020
|
-
///
|
|
1021
|
-
///
|
|
1022
|
-
///
|
|
1085
|
+
/// `null`, so `null` and `undefined` reset the prop with `_resetViewProp`; any other value decodes
|
|
1086
|
+
/// through `JavaScriptDecodable`, with the type inferred from the assignment. `_decodeViewProp` is a
|
|
1087
|
+
/// static member of `AnyViewProps`, reached through `Self.`, so a prop with the same name doesn't
|
|
1088
|
+
/// shadow it. The checks run once, before the switch, so each case is a single assignment. Members
|
|
1089
|
+
/// are reached through `self.` so that a prop named like a parameter (`value`, `runtime`) isn't
|
|
1090
|
+
/// shadowed by it.
|
|
1023
1091
|
///
|
|
1024
1092
|
/// With no value prop the body is empty: `PropName` is uninhabited, so the function is never called.
|
|
1025
|
-
///
|
|
1026
|
-
/// `defaults` is a `var` only when a prop has a `mutating get`, which can't be read from a constant.
|
|
1027
|
-
/// Otherwise it's a `let`, so the common case gets no "never mutated" warning.
|
|
1028
1093
|
private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
|
|
1029
1094
|
if valueProps.isEmpty {
|
|
1030
1095
|
return ""
|
|
1031
1096
|
}
|
|
1032
|
-
let defaultsSpecifier = valueProps.contains(where: \.hasMutatingGetter) ? "var" : "let"
|
|
1033
|
-
let resetCases =
|
|
1034
|
-
valueProps
|
|
1035
|
-
.map { " case .\($0.name):\n self.\($0.name) = defaults.\($0.name)" }
|
|
1036
|
-
.joined(separator: "\n")
|
|
1037
1097
|
let decodeCases =
|
|
1038
1098
|
valueProps
|
|
1039
1099
|
.map { " case .\($0.name):\n self.\($0.name) = try Self._decodeViewProp(value, in: runtime)" }
|
|
@@ -1043,10 +1103,7 @@ private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
|
|
|
1043
1103
|
return """
|
|
1044
1104
|
|
|
1045
1105
|
if value.isNull() || value.isUndefined() {
|
|
1046
|
-
|
|
1047
|
-
switch name {
|
|
1048
|
-
\(resetCases)
|
|
1049
|
-
}
|
|
1106
|
+
Self._resetViewProp(name, in: &self)
|
|
1050
1107
|
return
|
|
1051
1108
|
}
|
|
1052
1109
|
switch name {
|
|
@@ -1055,6 +1112,79 @@ private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
|
|
|
1055
1112
|
"""
|
|
1056
1113
|
}
|
|
1057
1114
|
|
|
1115
|
+
/// `_resetViewProp(_:in:)`, which sets one value prop back to its default when React removes it. Each
|
|
1116
|
+
/// case assigns the prop's initial value or `nil`, so a reset evaluates only that prop's default. A
|
|
1117
|
+
/// prop whose default the macro can't tell from the declaration reads it from a new `Self()`.
|
|
1118
|
+
///
|
|
1119
|
+
/// A static function of the struct body resolves names as a property initializer does: a static
|
|
1120
|
+
/// member without `Self.` and a member of an enclosing type, which `_decode` in the extension can't
|
|
1121
|
+
/// name. The parameters are `_name` and `_props`, with a number after the name when an initial value
|
|
1122
|
+
/// uses it, so they don't hide a declaration that an initial value names.
|
|
1123
|
+
///
|
|
1124
|
+
/// A default argument such as `#function` or `#line` of a function that an initial value calls gets
|
|
1125
|
+
/// the location of `_resetViewProp`, which the macro can't see.
|
|
1126
|
+
///
|
|
1127
|
+
/// `nil` with no value prop: `PropName` is uninhabited, so `_decode` never resets.
|
|
1128
|
+
private func resetFunction(valueProps: [ViewProp]) -> DeclSyntax? {
|
|
1129
|
+
if valueProps.isEmpty {
|
|
1130
|
+
return nil
|
|
1131
|
+
}
|
|
1132
|
+
let usedNames = Set(
|
|
1133
|
+
valueProps.flatMap { prop -> [String] in
|
|
1134
|
+
guard case .initialValue(let expression) = prop.defaultValue else {
|
|
1135
|
+
return []
|
|
1136
|
+
}
|
|
1137
|
+
return expression.tokens(viewMode: .sourceAccurate).compactMap { token in
|
|
1138
|
+
guard case .identifier(let text) = token.tokenKind else {
|
|
1139
|
+
return nil
|
|
1140
|
+
}
|
|
1141
|
+
return withoutBackticks(text)
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
)
|
|
1145
|
+
let nameParameter = unusedName("_name", in: usedNames)
|
|
1146
|
+
let propsParameter = unusedName("_props", in: usedNames)
|
|
1147
|
+
let cases =
|
|
1148
|
+
valueProps
|
|
1149
|
+
.map { prop -> String in
|
|
1150
|
+
let assignment: String
|
|
1151
|
+
switch prop.defaultValue {
|
|
1152
|
+
case .initialValue(let expression):
|
|
1153
|
+
assignment = "\(propsParameter).\(prop.name) = \(expression.trimmedDescription)"
|
|
1154
|
+
case .implicitNil:
|
|
1155
|
+
assignment = "\(propsParameter).\(prop.name) = nil"
|
|
1156
|
+
case .newInstance where prop.hasMutatingGetter:
|
|
1157
|
+
// A mutating getter can't be read from a constant. The local is scoped to its case, where
|
|
1158
|
+
// no initial value is copied.
|
|
1159
|
+
assignment = "var _defaults = Self()\n \(propsParameter).\(prop.name) = _defaults.\(prop.name)"
|
|
1160
|
+
case .newInstance:
|
|
1161
|
+
assignment = "\(propsParameter).\(prop.name) = Self().\(prop.name)"
|
|
1162
|
+
}
|
|
1163
|
+
return " case .\(prop.name):\n \(assignment)"
|
|
1164
|
+
}
|
|
1165
|
+
.joined(separator: "\n")
|
|
1166
|
+
return """
|
|
1167
|
+
@JavaScriptActor
|
|
1168
|
+
static func _resetViewProp(_ \(raw: nameParameter): PropName, in \(raw: propsParameter): inout Self) {
|
|
1169
|
+
switch \(raw: nameParameter) {
|
|
1170
|
+
\(raw: cases)
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
"""
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/// `base`, or `base` with the lowest number after it that gives a name not in `names`.
|
|
1177
|
+
private func unusedName(_ base: String, in names: Set<String>) -> String {
|
|
1178
|
+
if !names.contains(base) {
|
|
1179
|
+
return base
|
|
1180
|
+
}
|
|
1181
|
+
var number = 1
|
|
1182
|
+
while names.contains("\(base)\(number)") {
|
|
1183
|
+
number += 1
|
|
1184
|
+
}
|
|
1185
|
+
return "\(base)\(number)"
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1058
1188
|
/// The public `init()` that `AnyViewProps` requires, when Swift's own can't satisfy it: it gives each
|
|
1059
1189
|
/// event prop without an initial value an empty closure, and leaves the value props at their initial
|
|
1060
1190
|
/// values. `nil` when the type declares a usable `init()`, or when Swift's `init()` is enough (see
|
|
@@ -1065,7 +1195,7 @@ private func defaultInitializer(props: ViewPropsModel) -> DeclSyntax? {
|
|
|
1065
1195
|
}
|
|
1066
1196
|
if props.eventDefaults.isEmpty {
|
|
1067
1197
|
return """
|
|
1068
|
-
public init() {
|
|
1198
|
+
nonisolated public init() {
|
|
1069
1199
|
}
|
|
1070
1200
|
"""
|
|
1071
1201
|
}
|
|
@@ -1073,7 +1203,7 @@ private func defaultInitializer(props: ViewPropsModel) -> DeclSyntax? {
|
|
|
1073
1203
|
.map { " self.\($0.name) = \($0.emptyClosure)" }
|
|
1074
1204
|
.joined(separator: "\n")
|
|
1075
1205
|
return """
|
|
1076
|
-
public init() {
|
|
1206
|
+
nonisolated public init() {
|
|
1077
1207
|
\(raw: assignments)
|
|
1078
1208
|
}
|
|
1079
1209
|
"""
|
|
@@ -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
|
}
|
|
@@ -17,10 +17,17 @@ import SwiftSyntax
|
|
|
17
17
|
/// A synchronous function marked `@JS(.withAsync("name"))` also binds a promise-returning variant
|
|
18
18
|
/// under that name (see `asyncVariant`). It decodes and encodes like an `async` function, and calls
|
|
19
19
|
/// the function on the concurrent pool.
|
|
20
|
+
///
|
|
21
|
+
/// A synchronous function can take the JavaScript runtime as its last parameter,
|
|
22
|
+
/// `in runtime: borrowing JavaScriptRuntime`. JavaScript doesn't see it: the binding passes the runtime
|
|
23
|
+
/// of the call.
|
|
20
24
|
internal struct JSFunction {
|
|
21
25
|
let swiftName: String
|
|
22
26
|
private(set) var jsName: String
|
|
27
|
+
/// The parameters that take JavaScript arguments, without the runtime parameter.
|
|
23
28
|
let parameters: [FunctionParameterSyntax]
|
|
29
|
+
/// The label of the parameter that takes the JavaScript runtime, or `nil` when the function has none.
|
|
30
|
+
private let runtimeLabel: String?
|
|
24
31
|
/// The declared return type as written, or `nil` when the function returns `Void`/nothing.
|
|
25
32
|
let returnType: String?
|
|
26
33
|
/// How the return value encodes when its type mentions `Any`, or `nil` for any other type.
|
|
@@ -39,7 +46,9 @@ internal struct JSFunction {
|
|
|
39
46
|
self.swiftName = funcDecl.name.text
|
|
40
47
|
self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
|
|
41
48
|
self.nameToken = funcDecl.name
|
|
42
|
-
|
|
49
|
+
let declaredParameters = funcDecl.signature.parameterClause.parameters
|
|
50
|
+
self.parameters = declaredParameters.filter { !isRuntimeParameter($0) }
|
|
51
|
+
self.runtimeLabel = declaredParameters.first(where: isRuntimeParameter)?.firstName.text
|
|
43
52
|
|
|
44
53
|
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
45
54
|
self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
|
|
@@ -264,6 +273,9 @@ internal struct JSFunction {
|
|
|
264
273
|
}
|
|
265
274
|
callArguments.append(label == "_" ? value : "\(label): \(value)")
|
|
266
275
|
}
|
|
276
|
+
if let runtimeLabel {
|
|
277
|
+
callArguments.append(runtimeLabel == "_" ? "runtime" : "\(runtimeLabel): runtime")
|
|
278
|
+
}
|
|
267
279
|
return callArguments.joined(separator: ", ")
|
|
268
280
|
}
|
|
269
281
|
|
|
@@ -338,12 +350,14 @@ internal struct JSFunction {
|
|
|
338
350
|
// returns nothing binds through the `Void` overload (`JavaScriptRuntime.VoidFunctionClosure`)
|
|
339
351
|
// instead, which leaves JS's result as `undefined`. A call through it is faster than through a
|
|
340
352
|
// result-slot closure that writes nothing. The closure declares `-> Void` because a body of several
|
|
341
|
-
// statements without a `return` is otherwise ambiguous between the overloads.
|
|
353
|
+
// statements without a `return` is otherwise ambiguous between the overloads. A function that takes
|
|
354
|
+
// the runtime binds through the result-slot overload even when it returns nothing, so it gets the
|
|
355
|
+
// runtime as the closure's parameter instead of capturing the decorator's.
|
|
342
356
|
let captures = receiver.captureClause
|
|
343
357
|
let parameters =
|
|
344
358
|
isAsync
|
|
345
359
|
? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
346
|
-
: returnType == nil
|
|
360
|
+
: returnType == nil && runtimeLabel == nil
|
|
347
361
|
? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) -> Void"
|
|
348
362
|
: resultSlotClosureParameters
|
|
349
363
|
|
|
@@ -4,12 +4,18 @@ import SwiftSyntax
|
|
|
4
4
|
/// have a single constructor). Instead of a `Constructor { … }` DSL entry, the macro synthesizes an
|
|
5
5
|
/// override of `SharedObject._constructSharedObject(...)` that decodes the JS arguments and returns a
|
|
6
6
|
/// fresh instance; unlike the method/property bindings it produces the native instance rather than
|
|
7
|
-
/// recovering one.
|
|
7
|
+
/// recovering one. Like a function, it can take the JavaScript runtime as its last parameter.
|
|
8
8
|
internal struct JSConstructor {
|
|
9
|
+
/// The parameters that take JavaScript arguments, without the runtime parameter.
|
|
9
10
|
let parameters: [FunctionParameterSyntax]
|
|
11
|
+
/// The label of the parameter that takes the JavaScript runtime, or `nil` when the initializer has
|
|
12
|
+
/// none.
|
|
13
|
+
private let runtimeLabel: String?
|
|
10
14
|
|
|
11
15
|
init(initDecl: InitializerDeclSyntax) {
|
|
12
|
-
|
|
16
|
+
let declaredParameters = initDecl.signature.parameterClause.parameters
|
|
17
|
+
self.parameters = declaredParameters.filter { !isRuntimeParameter($0) }
|
|
18
|
+
self.runtimeLabel = declaredParameters.first(where: isRuntimeParameter)?.firstName.text
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
/// The body statements, indented with `indent`: arity guard, per-argument decode through
|
|
@@ -39,6 +45,9 @@ internal struct JSConstructor {
|
|
|
39
45
|
let label = parameter.firstName.text
|
|
40
46
|
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
41
47
|
}
|
|
48
|
+
if let runtimeLabel {
|
|
49
|
+
callArguments.append(runtimeLabel == "_" ? "runtime" : "\(runtimeLabel): runtime")
|
|
50
|
+
}
|
|
42
51
|
|
|
43
52
|
lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
|
|
44
53
|
|
|
@@ -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
|
}
|
|
@@ -126,6 +128,17 @@ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
|
126
128
|
return false
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
/// True when the parameter takes the JavaScript runtime instead of a JavaScript argument: its type,
|
|
132
|
+
/// without specifiers, is written as `JavaScriptRuntime` (or `….JavaScriptRuntime`), or as an optional
|
|
133
|
+
/// of it. The binding passes the runtime of the call, and JavaScript doesn't see the parameter. `@JS`
|
|
134
|
+
/// requires it to be the last parameter, labeled `in` or `runtime`, `borrowing` and not optional (see
|
|
135
|
+
/// `diagnoseRuntimeParameter`). A macro can't resolve types, so a typealias or a subclass of
|
|
136
|
+
/// `JavaScriptRuntime` isn't detected.
|
|
137
|
+
internal func isRuntimeParameter(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
138
|
+
let type = SpecifiedType(parameter.type).type
|
|
139
|
+
return baseIdentifier(of: optionalWrappedType(type) ?? type) == "JavaScriptRuntime"
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
/// A parameter's type split from the specifiers written in front of it (`borrowing`, `consuming`,
|
|
130
143
|
/// `sending`, `inout`, `isolated`, ...). The specifiers aren't part of the type: the decode call and
|
|
131
144
|
/// the conformance assertion need the bare type (`borrowing Point.decode(...)` isn't valid Swift), and
|
|
@@ -159,6 +172,17 @@ internal struct SpecifiedType {
|
|
|
159
172
|
/// a fresh decoded value satisfies either way.
|
|
160
173
|
internal let ownershipSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
|
|
161
174
|
|
|
175
|
+
/// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
|
|
176
|
+
/// label would make it a tuple, which Swift rejects with a single element anyway.
|
|
177
|
+
internal func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
|
|
178
|
+
guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
179
|
+
only.firstName == nil
|
|
180
|
+
else {
|
|
181
|
+
return type
|
|
182
|
+
}
|
|
183
|
+
return withoutParentheses(only.type)
|
|
184
|
+
}
|
|
185
|
+
|
|
162
186
|
/// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
|
|
163
187
|
/// optional.
|
|
164
188
|
internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
@@ -607,8 +631,9 @@ internal func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool
|
|
|
607
631
|
|
|
608
632
|
/// True if the type's inheritance clause already lists a protocol with the given name. Matches
|
|
609
633
|
/// either the bare identifier (`Record`) or a qualified member access ending in the name
|
|
610
|
-
/// (`ExpoModulesCore.Record`)
|
|
611
|
-
///
|
|
634
|
+
/// (`ExpoModulesCore.Record`), also with `nonisolated` or an attribute such as `@preconcurrency`
|
|
635
|
+
/// before it. Used by the extension macros to skip a conformance the author already spelled out.
|
|
636
|
+
/// Works for `struct`, `class`, and `enum` declarations; any other declaration kind has no
|
|
612
637
|
/// inheritance clause to read and reports `false`.
|
|
613
638
|
internal func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
|
|
614
639
|
let inheritanceClause: InheritanceClauseSyntax?
|
|
@@ -624,5 +649,11 @@ internal func inheritsProtocol(named name: String, in declaration: some DeclGrou
|
|
|
624
649
|
guard let inherited = inheritanceClause?.inheritedTypes else {
|
|
625
650
|
return false
|
|
626
651
|
}
|
|
627
|
-
return inherited.contains {
|
|
652
|
+
return inherited.contains { entry in
|
|
653
|
+
var type = entry.type
|
|
654
|
+
while let attributed = type.as(AttributedTypeSyntax.self) {
|
|
655
|
+
type = attributed.baseType
|
|
656
|
+
}
|
|
657
|
+
return baseIdentifier(of: type) == name
|
|
658
|
+
}
|
|
628
659
|
}
|
|
@@ -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.34.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.34.0",
|
|
30
|
+
"@expo/modules-macros-linux-x64": "0.34.0",
|
|
31
|
+
"@expo/modules-macros-win32-arm64": "0.34.0",
|
|
32
|
+
"@expo/modules-macros-win32-x64": "0.34.0"
|
|
33
33
|
}
|
|
34
34
|
}
|