expo-modules-macros 0.25.0 → 0.27.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 +8 -8
- package/README.md +40 -3
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.swift +10 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/CLI.swift +45 -2
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +120 -0
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +183 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ExportedSurface.swift +3 -2
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ResolveRefs.swift +9 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/SurfaceVisitor.swift +10 -3
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/TupleTypeAliases.swift +126 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/TypeNode.swift +47 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/SourceScan.swift +8 -2
- package/apple/Sources/ExpoModulesMacros/Expand/MacroExpander.swift +484 -0
- package/apple/Sources/ExpoModulesMacros/{EventMacro.swift → Macros/EventMacro.swift} +23 -10
- package/apple/Sources/ExpoModulesMacros/{ExpoModuleMacro.swift → Macros/ExpoModuleMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{JSMacro.swift → Macros/JSMacro.swift} +71 -54
- package/apple/Sources/ExpoModulesMacros/{RecordMacro.swift → Macros/RecordMacro.swift} +18 -5
- package/apple/Sources/ExpoModulesMacros/{SharedObjectMacro.swift → Macros/SharedObjectMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{UnionMacro.swift → Macros/UnionMacro.swift} +16 -5
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +9 -14
- package/apple/Sources/ExpoModulesMacros/ProvidedMacros.swift +31 -0
- package/apple/Sources/ExpoModulesMacros/{ClosureArguments.swift → Support/ClosureArguments.swift} +30 -4
- package/apple/Sources/ExpoModulesMacros/{DecorateModuleBuilder.swift → Support/DecorateModuleBuilder.swift} +5 -1
- package/apple/Sources/ExpoModulesMacros/{MacroHelpers.swift → Support/MacroHelpers.swift} +11 -4
- package/apple/Sources/ExpoModulesMacros/Support/TupleTypes.swift +272 -0
- package/apple/Sources/ExpoModulesMacros/Support/TypeAliases.swift +75 -0
- package/apple/Sources/ExpoModulesMacros/{TypeConformanceAssertion.swift → Support/TypeConformanceAssertion.swift} +104 -41
- package/build/index.d.ts +31 -1
- package/build/index.js +34 -0
- package/build/types.d.ts +77 -2
- package/build/types.js +4 -3
- package/package.json +5 -5
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/Availability.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ScanExports.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Modules → ExpoModulesCLI/Commands/ScanModules}/ScanModules.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/Detection.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/DetectionVisitor.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/ScanBuildConfiguration.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/StandardStreams.swift +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoModulesOptimizedMacro.swift → Macros/ExpoModulesOptimizedMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoViewMacro.swift → Macros/ExpoViewMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ViewPropsMacro.swift → Macros/ViewPropsMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{FreeFormTypes.swift → Support/FreeFormTypes.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSConstructor.swift → Support/JSConstructor.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSNameCollisions.swift → Support/JSNameCollisions.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{OptimizedFunctionHelpers.swift → Support/OptimizedFunctionHelpers.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{Receiver.swift → Support/Receiver.swift} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// The type aliases declared in the body of an `@ExpoModule` or `@SharedObject` class whose aliased type
|
|
4
|
+
/// holds a tuple, resolved to that type. Mirrors `TupleTypeAliases` in the macros, which resolve these
|
|
5
|
+
/// aliases in the `@JS` members the class binds, so that a member typed with such an alias reports the
|
|
6
|
+
/// tuple it converts as instead of an unresolvable ref.
|
|
7
|
+
///
|
|
8
|
+
/// Like the macros, it reads only the class body: an alias declared anywhere else stays a ref. A generic
|
|
9
|
+
/// alias isn't resolved, and neither is an alias in an `@Event` payload, which `@Event` converts on its
|
|
10
|
+
/// own.
|
|
11
|
+
struct TupleTypeAliases {
|
|
12
|
+
/// The resolved type of each alias, by name.
|
|
13
|
+
private let resolved: [String: TypeNode]
|
|
14
|
+
|
|
15
|
+
/// Collects the aliases in `members`, the body of the type named `scope`. A tuple alias keeps its name
|
|
16
|
+
/// qualified with `scope`, like the other declarations in the scan, so a consumer can name the type it
|
|
17
|
+
/// stands for.
|
|
18
|
+
init(in members: MemberBlockItemListSyntax, scope: String) {
|
|
19
|
+
var declared: [String: TypeNode] = [:]
|
|
20
|
+
for member in members {
|
|
21
|
+
guard let alias = member.decl.as(TypeAliasDeclSyntax.self), alias.genericParameterClause == nil else {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
let name = alias.name.text
|
|
25
|
+
declared[name] = typeNode(from: alias.initializer.value).named("\(scope).\(name)")
|
|
26
|
+
}
|
|
27
|
+
// Aliases may refer to each other, so each one is resolved through the others. An alias that refers
|
|
28
|
+
// back to itself is invalid Swift, and is left as written after `declared.count` rounds.
|
|
29
|
+
var resolved = declared
|
|
30
|
+
for _ in 0..<declared.count {
|
|
31
|
+
resolved = resolved.mapValues { $0.replacingAliases(resolved) }
|
|
32
|
+
}
|
|
33
|
+
self.resolved = resolved.filter { $0.value.holdsTuple }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// `node` with each ref to an alias that holds a tuple replaced by the aliased type.
|
|
37
|
+
func resolving(_ node: TypeNode) -> TypeNode {
|
|
38
|
+
guard !resolved.isEmpty else {
|
|
39
|
+
return node
|
|
40
|
+
}
|
|
41
|
+
return node.replacingAliases(resolved)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func resolving(_ function: ExportedFunction) -> ExportedFunction {
|
|
45
|
+
var resolvedFunction = function
|
|
46
|
+
resolvedFunction.parameters = function.parameters.map(resolving)
|
|
47
|
+
resolvedFunction.returns = function.returns.map(resolving)
|
|
48
|
+
return resolvedFunction
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func resolving(_ property: ExportedProperty) -> ExportedProperty {
|
|
52
|
+
var resolvedProperty = property
|
|
53
|
+
resolvedProperty.type = property.type.map(resolving)
|
|
54
|
+
return resolvedProperty
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func resolving(_ parameter: ExportedParameter) -> ExportedParameter {
|
|
58
|
+
var resolvedParameter = parameter
|
|
59
|
+
resolvedParameter.type = resolving(parameter.type)
|
|
60
|
+
return resolvedParameter
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
extension TypeNode {
|
|
65
|
+
/// This node with each ref named in `aliases`, bare (`Span`) or qualified with `Self` (`Self.Span`),
|
|
66
|
+
/// replaced by the aliased type.
|
|
67
|
+
fileprivate func replacingAliases(_ aliases: [String: TypeNode]) -> TypeNode {
|
|
68
|
+
switch self {
|
|
69
|
+
case .primitive, .unknown, .sharedRef:
|
|
70
|
+
return self
|
|
71
|
+
case .ref(let name, _, _):
|
|
72
|
+
let unqualified = name.hasPrefix("Self.") ? String(name.dropFirst("Self.".count)) : name
|
|
73
|
+
return aliases[unqualified] ?? self
|
|
74
|
+
case .optional(let wrapped):
|
|
75
|
+
return .optional(wrapped: wrapped.replacingAliases(aliases))
|
|
76
|
+
case .array(let element):
|
|
77
|
+
return .array(element: element.replacingAliases(aliases))
|
|
78
|
+
case .dictionary(let key, let value):
|
|
79
|
+
return .dictionary(key: key.replacingAliases(aliases), value: value.replacingAliases(aliases))
|
|
80
|
+
case .promise(let value):
|
|
81
|
+
return .promise(value: value.replacingAliases(aliases))
|
|
82
|
+
case .tuple(let elements, let name):
|
|
83
|
+
return .tuple(elements: elements.map { $0.replacingAliases(aliases) }, name: name)
|
|
84
|
+
case .object(let properties, let name):
|
|
85
|
+
return .object(
|
|
86
|
+
properties: properties.map { TypeNodeProperty(name: $0.name, type: $0.type.replacingAliases(aliases)) },
|
|
87
|
+
name: name
|
|
88
|
+
)
|
|
89
|
+
case .function(let parameters, let returns, let isAsync, let isThrowing):
|
|
90
|
+
return .function(
|
|
91
|
+
parameters: parameters.map { $0.replacingAliases(aliases) },
|
|
92
|
+
returns: returns?.replacingAliases(aliases),
|
|
93
|
+
isAsync: isAsync,
|
|
94
|
+
isThrowing: isThrowing
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// This node named `name` when it's a tuple or an object, which an alias names. Any other node, such as
|
|
100
|
+
/// an array of tuples, is returned as it is: its elements carry the names of their own aliases.
|
|
101
|
+
fileprivate func named(_ name: String) -> TypeNode {
|
|
102
|
+
switch self {
|
|
103
|
+
case .tuple(let elements, _):
|
|
104
|
+
return .tuple(elements: elements, name: name)
|
|
105
|
+
case .object(let properties, _):
|
|
106
|
+
return .object(properties: properties, name: name)
|
|
107
|
+
default:
|
|
108
|
+
return self
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// True for a type the macros convert through the tuple methods: a tuple (`.tuple`, or `.object` for a
|
|
113
|
+
/// labeled one), or an optional, array or `String`-keyed dictionary that holds one.
|
|
114
|
+
fileprivate var holdsTuple: Bool {
|
|
115
|
+
switch self {
|
|
116
|
+
case .tuple, .object:
|
|
117
|
+
return true
|
|
118
|
+
case .optional(let inner), .array(let inner):
|
|
119
|
+
return inner.holdsTuple
|
|
120
|
+
case .dictionary(.primitive("String", _), let value):
|
|
121
|
+
return value.holdsTuple
|
|
122
|
+
default:
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -72,12 +72,22 @@ indirect enum TypeNode: Equatable {
|
|
|
72
72
|
/// node parsed at a use site has never seen.
|
|
73
73
|
case ref(name: String, refKind: RefKind? = nil, jsTypeOverride: JSType? = nil)
|
|
74
74
|
|
|
75
|
+
/// An unlabeled tuple of two or more elements, `(A, B)`, which crosses as an array with one value per
|
|
76
|
+
/// element, like a TypeScript tuple. `name` is the qualified name of the type alias it was written
|
|
77
|
+
/// through, such as `MyModule.Pair`, or `nil` when it was spelled out.
|
|
78
|
+
case tuple(elements: [TypeNode], name: String? = nil)
|
|
79
|
+
|
|
80
|
+
/// An object with a fixed set of properties, which a labeled tuple of two or more elements,
|
|
81
|
+
/// `(x: A, y: B)`, crosses as: one property per label. `name` is the qualified name of the type alias it
|
|
82
|
+
/// was written through, such as `MyModule.Span`, or `nil` when it was spelled out.
|
|
83
|
+
case object(properties: [TypeNodeProperty], name: String? = nil)
|
|
84
|
+
|
|
75
85
|
/// `SharedRef<T>` (or `ExpoModulesCore.SharedRef<T>`): a shared object holding a native `T`, which
|
|
76
86
|
/// JS sees as an instance of `expo.SharedRef`. `native` is `T` parsed like any other type, but it
|
|
77
87
|
/// never crosses the boundary itself, so refs in it are not resolved.
|
|
78
88
|
case sharedRef(native: TypeNode)
|
|
79
89
|
|
|
80
|
-
/// A spelling the parser doesn't model (generic parameter,
|
|
90
|
+
/// A spelling the parser doesn't model (generic parameter, metatype, …), kept verbatim so
|
|
81
91
|
/// nothing is lost. Has no `typeof`.
|
|
82
92
|
case unknown(text: String)
|
|
83
93
|
|
|
@@ -87,7 +97,7 @@ indirect enum TypeNode: Equatable {
|
|
|
87
97
|
switch self {
|
|
88
98
|
case .primitive(_, let jsType):
|
|
89
99
|
return jsType
|
|
90
|
-
case .array, .dictionary, .promise, .sharedRef:
|
|
100
|
+
case .array, .dictionary, .promise, .tuple, .object, .sharedRef:
|
|
91
101
|
return .object
|
|
92
102
|
case .ref(_, _, let jsTypeOverride):
|
|
93
103
|
// An unresolved ref, and every resolved one but an enum, is an object.
|
|
@@ -102,6 +112,12 @@ indirect enum TypeNode: Equatable {
|
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
/// A property of a `.object` node: its name, and the type of its value.
|
|
116
|
+
struct TypeNodeProperty: Encodable, Equatable {
|
|
117
|
+
let name: String
|
|
118
|
+
let type: TypeNode
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
extension TypeNode: Encodable {
|
|
106
122
|
private enum CodingKeys: String, CodingKey {
|
|
107
123
|
case refKind
|
|
@@ -110,7 +126,7 @@ extension TypeNode: Encodable {
|
|
|
110
126
|
// The `typeof` category. Spelled `typeof` in JSON (what the consumer reads); the Swift property is
|
|
111
127
|
// `jsType` since it's the cleaner Swift name.
|
|
112
128
|
case jsType = "typeof"
|
|
113
|
-
case wrapped, element, key, value, parameters, returns, text, native
|
|
129
|
+
case wrapped, element, key, value, parameters, returns, text, native, elements, properties
|
|
114
130
|
// A closure type's effects, matching `ExportedFunction`'s TS-keyword spellings.
|
|
115
131
|
case isAsync = "async"
|
|
116
132
|
case isThrowing = "throws"
|
|
@@ -148,6 +164,14 @@ extension TypeNode: Encodable {
|
|
|
148
164
|
try container.encode("ref", forKey: .kind)
|
|
149
165
|
try container.encode(name, forKey: .name)
|
|
150
166
|
try container.encodeIfPresent(refKind, forKey: .refKind)
|
|
167
|
+
case .tuple(let elements, let name):
|
|
168
|
+
try container.encode("tuple", forKey: .kind)
|
|
169
|
+
try container.encode(elements, forKey: .elements)
|
|
170
|
+
try container.encodeIfPresent(name, forKey: .name)
|
|
171
|
+
case .object(let properties, let name):
|
|
172
|
+
try container.encode("object", forKey: .kind)
|
|
173
|
+
try container.encode(properties, forKey: .properties)
|
|
174
|
+
try container.encodeIfPresent(name, forKey: .name)
|
|
151
175
|
case .sharedRef(let native):
|
|
152
176
|
try container.encode("sharedRef", forKey: .kind)
|
|
153
177
|
try container.encode(native, forKey: .native)
|
|
@@ -228,7 +252,22 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
|
|
|
228
252
|
}
|
|
229
253
|
return .ref(name: member.trimmedDescription)
|
|
230
254
|
}
|
|
231
|
-
//
|
|
255
|
+
// `(A, B)` / `(x: A, y: B)`, matching the tuples the macros convert: two or more elements, all
|
|
256
|
+
// labeled or none, without a variadic or `inout` element.
|
|
257
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count >= 2,
|
|
258
|
+
tuple.elements.allSatisfy({ $0.ellipsis == nil && $0.inoutKeyword == nil })
|
|
259
|
+
{
|
|
260
|
+
let labels = tuple.elements.compactMap { $0.firstName?.text }
|
|
261
|
+
if labels.isEmpty {
|
|
262
|
+
return .tuple(elements: tuple.elements.map { typeNode(from: $0.type) })
|
|
263
|
+
}
|
|
264
|
+
if labels.count == tuple.elements.count {
|
|
265
|
+
return .object(
|
|
266
|
+
properties: zip(labels, tuple.elements).map { TypeNodeProperty(name: $0, type: typeNode(from: $1.type)) }
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Other tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
|
|
232
271
|
return .unknown(text: type.trimmedDescription)
|
|
233
272
|
}
|
|
234
273
|
|
|
@@ -316,6 +355,10 @@ extension TypeNode {
|
|
|
316
355
|
return "Promise<\(value.spelling)>"
|
|
317
356
|
case .sharedRef(let native):
|
|
318
357
|
return "SharedRef<\(native.spelling)>"
|
|
358
|
+
case .tuple(let elements, _):
|
|
359
|
+
return "(\(elements.map(\.spelling).joined(separator: ", ")))"
|
|
360
|
+
case .object(let properties, _):
|
|
361
|
+
return "(\(properties.map { "\($0.name): \($0.type.spelling)" }.joined(separator: ", ")))"
|
|
319
362
|
case .function(let parameters, let returns, _, _):
|
|
320
363
|
return "(\(parameters.map { $0.spelling }.joined(separator: ", "))) -> \(returns?.spelling ?? "Void")"
|
|
321
364
|
case .unknown(let text):
|
|
@@ -102,7 +102,13 @@ struct MacroPrefilter {
|
|
|
102
102
|
private let bareNames: [[UInt8]]
|
|
103
103
|
|
|
104
104
|
init(macros: Set<DetectedMacro>, conformances: Set<String> = []) {
|
|
105
|
-
|
|
105
|
+
self.init(attributeNames: Set(macros.map(\.rawValue)), conformances: conformances)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// A pre-filter for attributes that `DetectedMacro` doesn't list, such as every macro that `expand`
|
|
109
|
+
/// runs.
|
|
110
|
+
init(attributeNames: Set<String>, conformances: Set<String> = []) {
|
|
111
|
+
attributes = attributeNames.map { Array($0.utf8) }
|
|
106
112
|
bareNames = conformances.map { Array($0.utf8) }
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -196,7 +202,7 @@ func swiftFiles(in paths: [String]) -> [String] {
|
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
/// Whether `path` is a directory (following symbolic links), or `nil` if nothing exists there.
|
|
199
|
-
|
|
205
|
+
func isDirectory(atPath path: String, fileManager: FileManager = .default) -> Bool? {
|
|
200
206
|
#if canImport(FoundationEssentials)
|
|
201
207
|
var isDirectory = false
|
|
202
208
|
#else
|