expo-modules-macros 0.0.1 → 0.14.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/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- package/package.json +22 -5
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftDiagnostics
|
|
3
|
+
import SwiftSyntax
|
|
4
|
+
import SwiftSyntaxBuilder
|
|
5
|
+
import SwiftSyntaxMacros
|
|
6
|
+
|
|
7
|
+
/// Member + extension macro applied to a view-props type. Like `@Record`, **every stored property**
|
|
8
|
+
/// that is not `static`, `private`, `fileprivate`, `lazy` or computed is part of the surface — no
|
|
9
|
+
/// `@Field` wrapper — but unlike a record, the properties split into two kinds by their type:
|
|
10
|
+
///
|
|
11
|
+
/// - a **function-typed** property is an **event** (`var onTap: (TapEvent) -> Void`), dispatched by
|
|
12
|
+
/// name through the view's event emitter. No JS function object is ever decoded into it.
|
|
13
|
+
/// - every other property is a **value prop**, decoded from the raw props Fabric delivers.
|
|
14
|
+
///
|
|
15
|
+
/// The macro synthesizes the identity surface the batched reaction model reads:
|
|
16
|
+
///
|
|
17
|
+
/// - `PropName`: a `String`-backed `CaseIterable` enum, one case per value prop, whose raw value is
|
|
18
|
+
/// the wire key. It doubles as the string → prop translation table, so the runtime maps a raw
|
|
19
|
+
/// changed key with `PropName(rawValue:)` and names a prop in a log with `prop.rawValue`.
|
|
20
|
+
/// - `PropSet`: an `OptionSet` over `UInt64`, one bit per value prop in declaration order. This is the
|
|
21
|
+
/// membership currency: the changed set on a diff is a bitmask, so asking what changed costs no
|
|
22
|
+
/// allocation and no hashing.
|
|
23
|
+
/// - `_eventNames`: the event props' names, verbatim, for core to register at view creation.
|
|
24
|
+
/// - `typealias Diff = PropsDiff<Self>`, the nested spelling of core's one generic diff.
|
|
25
|
+
///
|
|
26
|
+
/// Author-facing shape — no conformance to spell out:
|
|
27
|
+
///
|
|
28
|
+
/// @ViewProps
|
|
29
|
+
/// struct CardProps {
|
|
30
|
+
/// var color: UIColor = .red // value prop, bit 0
|
|
31
|
+
/// var radius: CGFloat = 0 // value prop, bit 1
|
|
32
|
+
/// var onTap: (TapEvent) -> Void // event, no bit
|
|
33
|
+
/// }
|
|
34
|
+
///
|
|
35
|
+
/// The diff type itself is deliberately **not** generated. Core's generic
|
|
36
|
+
/// `PropsDiff<Props: AnyViewProps>` owns both storage (`old`/`new`/`changedProps`) and behavior
|
|
37
|
+
/// (`changed(_:)`, `oldValue(_:)`, `isInitial`, `changedNames`), reaching the per-props types through
|
|
38
|
+
/// the conformance's associated types — so the query API can grow without a macro release.
|
|
39
|
+
///
|
|
40
|
+
/// Event names are emitted **verbatim**, with no `on`-prefix stripping. This differs from `@Event` on
|
|
41
|
+
/// modules and shared objects, which strips it (`onStatusChange` → `"statusChange"`): a view event prop
|
|
42
|
+
/// is a React prop name that Fabric carries as-is, so the native and JS spellings must match exactly.
|
|
43
|
+
public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
44
|
+
public static func expansion(
|
|
45
|
+
of node: AttributeSyntax,
|
|
46
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
47
|
+
conformingTo protocols: [TypeSyntax],
|
|
48
|
+
in context: some MacroExpansionContext
|
|
49
|
+
) throws -> [DeclSyntax] {
|
|
50
|
+
let props = try validatedViewProps(of: declaration)
|
|
51
|
+
|
|
52
|
+
var members: [DeclSyntax] = []
|
|
53
|
+
members.append(propNameEnum(valueProps: props.valueProps))
|
|
54
|
+
members.append(propSetStruct(valueProps: props.valueProps))
|
|
55
|
+
members.append(eventNamesConstant(eventProps: props.eventProps))
|
|
56
|
+
members.append(diffTypealias())
|
|
57
|
+
return members
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Auto-conforms the type to `AnyViewProps`, supplying the two requirements that can only be written
|
|
61
|
+
/// per-props: `allProps` (every bit set, the changed set on the first application) and
|
|
62
|
+
/// `propSet(for:)` (one name's bit, for folding raw changed keys into the mask).
|
|
63
|
+
///
|
|
64
|
+
/// A conformance the author already spelled out in the inheritance clause is not repeated.
|
|
65
|
+
public static func expansion(
|
|
66
|
+
of node: AttributeSyntax,
|
|
67
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
68
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
69
|
+
conformingTo protocols: [TypeSyntax],
|
|
70
|
+
in context: some MacroExpansionContext
|
|
71
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
72
|
+
guard declaration.is(StructDeclSyntax.self) else {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
// Both roles read the same model, but a validation failure is the member macro's to report:
|
|
76
|
+
// expansion runs each role independently, so throwing here too would surface every diagnostic
|
|
77
|
+
// twice. When the model doesn't validate, the member macro has already emitted the error and
|
|
78
|
+
// there is nothing to extend.
|
|
79
|
+
guard let props = try? validatedViewProps(of: declaration) else {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
let alreadyConforms = inheritsProtocol(named: viewPropsProtocolName, in: declaration)
|
|
83
|
+
|
|
84
|
+
// `allProps` is read on every first application of props, once per view instance, so it's a
|
|
85
|
+
// stored constant with the mask already folded rather than a computed property rebuilding an
|
|
86
|
+
// array literal per call. The macro knows the bit count, so the literal is exact: n props
|
|
87
|
+
// occupy bits 0..<n, which is the low n bits set.
|
|
88
|
+
let allPropsMask = props.valueProps.isEmpty
|
|
89
|
+
? "0"
|
|
90
|
+
: "0b" + String(repeating: "1", count: props.valueProps.count)
|
|
91
|
+
|
|
92
|
+
// With no value prop there's nothing to switch over, and an empty `switch` over an uninhabited
|
|
93
|
+
// enum doesn't compile — return the empty set instead.
|
|
94
|
+
let propSetBody: String
|
|
95
|
+
if props.valueProps.isEmpty {
|
|
96
|
+
propSetBody = " return []"
|
|
97
|
+
} else {
|
|
98
|
+
let cases = props.valueProps
|
|
99
|
+
.map { " case .\($0.name):\n return .\($0.name)" }
|
|
100
|
+
.joined(separator: "\n")
|
|
101
|
+
propSetBody = " switch name {\n\(cases)\n }"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let conformanceClause = alreadyConforms ? "" : ": \(viewPropsProtocolName)"
|
|
105
|
+
let ext: DeclSyntax = """
|
|
106
|
+
extension \(type.trimmed)\(raw: conformanceClause) {
|
|
107
|
+
public static let allProps = PropSet(rawValue: \(raw: allPropsMask))
|
|
108
|
+
|
|
109
|
+
/// `@inlinable` so core's raw-key fold can inline the lookup across the module boundary:
|
|
110
|
+
/// it runs once per changed key per props batch.
|
|
111
|
+
@inlinable
|
|
112
|
+
public static func propSet(for name: PropName) -> PropSet {
|
|
113
|
+
\(raw: propSetBody)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
"""
|
|
117
|
+
guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
return [extDecl]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// The core protocol carrying the `PropName`/`PropSet` associated types plus `allProps` and
|
|
125
|
+
/// `propSet(for:)`, through which the generic `PropsDiff` reaches a specific props type.
|
|
126
|
+
private let viewPropsProtocolName = "AnyViewProps"
|
|
127
|
+
|
|
128
|
+
/// The mask is a single `UInt64`, so a props type can carry at most this many value props. Event
|
|
129
|
+
/// props take no bit and don't count.
|
|
130
|
+
private let maxValueProps = 64
|
|
131
|
+
|
|
132
|
+
// MARK: - Diagnostics
|
|
133
|
+
|
|
134
|
+
/// The error for a leftover `@Field`, attached to the attribute itself and carrying a fix-it that
|
|
135
|
+
/// deletes it. `@ViewProps` treats every stored property as a prop, so the attribute has no meaning
|
|
136
|
+
/// here; left in place it would wrap the value in `Field<T>` and the props type would decode against
|
|
137
|
+
/// the wrong type.
|
|
138
|
+
private func fieldAttributeDiagnostic(
|
|
139
|
+
for attribute: AttributeSyntax,
|
|
140
|
+
on varDecl: VariableDeclSyntax
|
|
141
|
+
) -> Diagnostic {
|
|
142
|
+
// Rebuild the attribute list without this entry, so the fix-it removes the attribute and the
|
|
143
|
+
// trivia it carried rather than leaving a blank line behind.
|
|
144
|
+
var attributes = varDecl.attributes
|
|
145
|
+
if let index = attributes.firstIndex(where: { element in
|
|
146
|
+
guard case .attribute(let candidate) = element else {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
return candidate == attribute
|
|
150
|
+
}) {
|
|
151
|
+
attributes.remove(at: index)
|
|
152
|
+
}
|
|
153
|
+
let fixIt = FixIt(
|
|
154
|
+
message: ViewPropsFixItMessage("Remove the '@Field' attribute", id: "viewprops-remove-field"),
|
|
155
|
+
changes: [
|
|
156
|
+
.replace(
|
|
157
|
+
oldNode: Syntax(varDecl),
|
|
158
|
+
newNode: Syntax(varDecl.with(\.attributes, attributes))
|
|
159
|
+
)
|
|
160
|
+
]
|
|
161
|
+
)
|
|
162
|
+
let message = ViewPropsDiagnosticMessage(
|
|
163
|
+
"@Field is no longer used — @ViewProps treats every stored property as a prop. Remove the @Field attribute",
|
|
164
|
+
id: "viewprops-field-attribute"
|
|
165
|
+
)
|
|
166
|
+
return Diagnostic(node: attribute, message: message, fixIts: [fixIt])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// The error for an optional event prop, attached to the declared type and carrying a fix-it that
|
|
170
|
+
/// unwraps it. Events are registered once at view creation from a static name list, so there is no
|
|
171
|
+
/// shape that could express "sometimes emitted".
|
|
172
|
+
private func optionalEventDiagnostic(for type: TypeSyntax, name: String) -> Diagnostic {
|
|
173
|
+
var fixIts: [FixIt] = []
|
|
174
|
+
if let unwrapped = unwrappedOptionalType(type) {
|
|
175
|
+
fixIts.append(
|
|
176
|
+
FixIt(
|
|
177
|
+
message: ViewPropsFixItMessage("Make '\(name)' non-optional", id: "viewprops-unwrap-event"),
|
|
178
|
+
changes: [
|
|
179
|
+
.replace(
|
|
180
|
+
oldNode: Syntax(type),
|
|
181
|
+
newNode: Syntax(unwrapped.with(\.trailingTrivia, type.trailingTrivia))
|
|
182
|
+
)
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
let message = ViewPropsDiagnosticMessage(
|
|
188
|
+
"Event props cannot be optional — '\(name)' is registered once at view creation, so its presence can't vary. Drop the '?'",
|
|
189
|
+
id: "viewprops-optional-event"
|
|
190
|
+
)
|
|
191
|
+
return Diagnostic(node: type, message: message, fixIts: fixIts)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// The type an optional wraps, with the enclosing parentheses of a parenthesized function type kept
|
|
195
|
+
/// (`(() -> Void)?` unwraps to `() -> Void`, not to a stray `(() -> Void)`). Returns `nil` for a
|
|
196
|
+
/// spelling the fix-it can't rewrite mechanically.
|
|
197
|
+
private func unwrappedOptionalType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
198
|
+
guard let wrapped = optionalWrappedType(type) else {
|
|
199
|
+
return nil
|
|
200
|
+
}
|
|
201
|
+
return innerFunctionType(of: wrapped) ?? wrapped
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// The function type inside a single-element parenthesized type, so unwrapping `(() -> Void)?`
|
|
205
|
+
/// yields `() -> Void` rather than keeping the now-redundant parentheses.
|
|
206
|
+
private func innerFunctionType(of type: TypeSyntax) -> TypeSyntax? {
|
|
207
|
+
guard let tuple = type.as(TupleTypeSyntax.self),
|
|
208
|
+
tuple.elements.count == 1,
|
|
209
|
+
let only = tuple.elements.first,
|
|
210
|
+
only.type.is(FunctionTypeSyntax.self) else {
|
|
211
|
+
return nil
|
|
212
|
+
}
|
|
213
|
+
return only.type
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private struct ViewPropsDiagnosticMessage: DiagnosticMessage {
|
|
217
|
+
let message: String
|
|
218
|
+
let diagnosticID: MessageID
|
|
219
|
+
let severity: DiagnosticSeverity = .error
|
|
220
|
+
|
|
221
|
+
init(_ message: String, id: String) {
|
|
222
|
+
self.message = message
|
|
223
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private struct ViewPropsFixItMessage: FixItMessage {
|
|
228
|
+
let message: String
|
|
229
|
+
let fixItID: MessageID
|
|
230
|
+
|
|
231
|
+
init(_ message: String, id: String) {
|
|
232
|
+
self.message = message
|
|
233
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// MARK: - Property model
|
|
238
|
+
|
|
239
|
+
/// One stored property of the props type, classified as a value prop or an event.
|
|
240
|
+
private struct ViewProp {
|
|
241
|
+
/// The property name as written, backticks included for an escaped name. Every identifier position
|
|
242
|
+
/// in the generated code uses this, since `case default` and `static let default` don't parse.
|
|
243
|
+
let name: String
|
|
244
|
+
/// The property's declared type, verbatim. Retained for the decode surface, which lands with the
|
|
245
|
+
/// core props contract.
|
|
246
|
+
let type: String
|
|
247
|
+
|
|
248
|
+
/// The name with any escaping backticks removed: the JS-visible key, so the enum's raw value and
|
|
249
|
+
/// `_eventNames` both read `"default"`, never `` "`default`" ``.
|
|
250
|
+
var wireName: String {
|
|
251
|
+
return name.trimmingCharacters(in: CharacterSet(charactersIn: "`"))
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private struct ViewPropsModel {
|
|
256
|
+
/// Value props, in declaration order. The order is the bit order, so it is part of the ABI
|
|
257
|
+
/// between a compiled view and the runtime that fills the mask.
|
|
258
|
+
let valueProps: [ViewProp]
|
|
259
|
+
/// Event props, in declaration order.
|
|
260
|
+
let eventProps: [ViewProp]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Reads and validates the props type's stored properties, splitting them into value props and events.
|
|
264
|
+
///
|
|
265
|
+
/// Rejects what can't be expressed: a non-`struct` declaration (the class form is the SwiftUI path and
|
|
266
|
+
/// needs the observable protocol, which is deferred), a leftover `@Field` attribute, an optional
|
|
267
|
+
/// function type, a property with no determinable type, and more than 64 value props.
|
|
268
|
+
private func validatedViewProps(of declaration: some DeclGroupSyntax) throws -> ViewPropsModel {
|
|
269
|
+
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
|
|
270
|
+
throw MacroExpansionErrorMessage(
|
|
271
|
+
"@ViewProps can only be applied to a struct — the class form (for SwiftUI views) is not supported yet"
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
// A generic props type can't work: the synthesized extension would have to repeat the generic
|
|
275
|
+
// parameter list and its constraints, and core reaches the props type through a view's static
|
|
276
|
+
// `Props` typealias, which names one concrete type.
|
|
277
|
+
if structDecl.genericParameterClause != nil {
|
|
278
|
+
throw MacroExpansionErrorMessage(
|
|
279
|
+
"@ViewProps does not support generic types — a view's props type must be concrete"
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var valueProps: [ViewProp] = []
|
|
284
|
+
var eventProps: [ViewProp] = []
|
|
285
|
+
|
|
286
|
+
for member in declaration.memberBlock.members {
|
|
287
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
if isExcludedByModifier(varDecl.modifiers) {
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
// `@Field` is the v1 property wrapper and has no meaning here — every stored property is already
|
|
294
|
+
// part of the surface. Left in place it would wrap the value in `Field<T>`, so the props type
|
|
295
|
+
// would decode against the wrong type. Flag it rather than emit code built on it.
|
|
296
|
+
if let fieldAttribute = varDecl.attributes.firstAttribute(named: "Field") {
|
|
297
|
+
throw DiagnosticsError(diagnostics: [fieldAttributeDiagnostic(for: fieldAttribute, on: varDecl)])
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for binding in varDecl.bindings {
|
|
301
|
+
// A computed property is never a prop, but an accessor block alone doesn't mean computed:
|
|
302
|
+
// `willSet`/`didSet` observers imply stored storage. `bindingIsSettable` draws exactly that
|
|
303
|
+
// line, so a stored property with observers stays part of the surface.
|
|
304
|
+
if binding.accessorBlock != nil && !bindingIsSettable(binding) {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
// A tuple-destructuring binding (`var (a, b) = (1, 2)`) has no single name to key a prop on,
|
|
308
|
+
// and silently dropping it would leave the props type missing fields the author declared.
|
|
309
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
310
|
+
if binding.pattern.is(TuplePatternSyntax.self) {
|
|
311
|
+
throw MacroExpansionErrorMessage(
|
|
312
|
+
"@ViewProps does not support tuple-destructuring properties — declare each prop separately"
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
// An escaped name (`` var `default`: Int ``) needs both spellings: `identifier.text` keeps the
|
|
318
|
+
// backticks, which every identifier position requires (`case \`default\``), while the wire key
|
|
319
|
+
// and `_eventNames` need the bare name.
|
|
320
|
+
let name = ident.identifier.text
|
|
321
|
+
|
|
322
|
+
// Prefer the explicit annotation. When it's omitted, recover the type from a literal default
|
|
323
|
+
// (`var title = ""`). Anything a syntactic macro can't resolve still needs an annotation,
|
|
324
|
+
// since the decode surface names the type.
|
|
325
|
+
let declaredType = binding.typeAnnotation?.type
|
|
326
|
+
let resolvedType = declaredType?.trimmedDescription
|
|
327
|
+
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
328
|
+
guard let resolvedType else {
|
|
329
|
+
throw MacroExpansionErrorMessage(
|
|
330
|
+
"@ViewProps props must declare an explicit type — '\(name)' has none"
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// A function type means an event. An *optional* function type would make the event's presence
|
|
335
|
+
// dynamic, but events are registered once at view creation from a static name list, so there
|
|
336
|
+
// is no shape that could express "sometimes emitted".
|
|
337
|
+
if let declaredType, isOptionalType(declaredType), underlyingFunctionType(of: declaredType) != nil {
|
|
338
|
+
throw DiagnosticsError(diagnostics: [
|
|
339
|
+
optionalEventDiagnostic(for: declaredType, name: name)
|
|
340
|
+
])
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let isEvent = declaredType.map { underlyingFunctionType(of: $0) != nil } ?? false
|
|
344
|
+
// `PropSet` stores its mask in `rawValue`, so a value prop of that name would emit a static
|
|
345
|
+
// member shadowing it and the option set would not compile ("circular reference"). The error
|
|
346
|
+
// would point at generated code, so catch it here and name the property.
|
|
347
|
+
if !isEvent && name.trimmingCharacters(in: CharacterSet(charactersIn: "`")) == "rawValue" {
|
|
348
|
+
throw MacroExpansionErrorMessage(
|
|
349
|
+
"'rawValue' can't be used as a prop name — it collides with the synthesized PropSet's storage. Rename the property"
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
let prop = ViewProp(name: name, type: resolvedType)
|
|
353
|
+
if isEvent {
|
|
354
|
+
eventProps.append(prop)
|
|
355
|
+
} else {
|
|
356
|
+
valueProps.append(prop)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
guard valueProps.count <= maxValueProps else {
|
|
362
|
+
throw MacroExpansionErrorMessage(
|
|
363
|
+
"@ViewProps supports at most \(maxValueProps) value props (the changed-props mask is a UInt64), but this type declares \(valueProps.count); '\(valueProps[maxValueProps].name)' is the first over the limit. Event props don't count toward it"
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
return ViewPropsModel(valueProps: valueProps, eventProps: eventProps)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// The function type a props property declares, unwrapping any number of enclosing parentheses and an
|
|
370
|
+
/// optional wrapper, or `nil` when the type isn't a function. `@Sendable`/`@escaping` and other
|
|
371
|
+
/// attributed forms unwrap to the function type underneath.
|
|
372
|
+
private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax? {
|
|
373
|
+
if let functionType = type.as(FunctionTypeSyntax.self) {
|
|
374
|
+
return functionType
|
|
375
|
+
}
|
|
376
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first {
|
|
377
|
+
return underlyingFunctionType(of: only.type)
|
|
378
|
+
}
|
|
379
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
380
|
+
return underlyingFunctionType(of: attributed.baseType)
|
|
381
|
+
}
|
|
382
|
+
if let optional = type.as(OptionalTypeSyntax.self) {
|
|
383
|
+
return underlyingFunctionType(of: optional.wrappedType)
|
|
384
|
+
}
|
|
385
|
+
if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
386
|
+
return underlyingFunctionType(of: implicitlyUnwrapped.wrappedType)
|
|
387
|
+
}
|
|
388
|
+
// The long spelling of an optional: `Optional<() -> Void>` has to reach the same diagnostic as
|
|
389
|
+
// `(() -> Void)?` and `(() -> Void)!`, or the same declaration would be an event in one spelling
|
|
390
|
+
// and a value prop in another.
|
|
391
|
+
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
392
|
+
identifier.name.text == "Optional",
|
|
393
|
+
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
394
|
+
case .type(let wrapped) = argument.argument {
|
|
395
|
+
return underlyingFunctionType(of: wrapped)
|
|
396
|
+
}
|
|
397
|
+
return nil
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// MARK: - Synthesized members
|
|
401
|
+
|
|
402
|
+
/// The `PropName` enum: one case per value prop, `String`-backed so the raw value is the wire key.
|
|
403
|
+
/// Emitted even when empty (an uninhabited enum is legal and keeps the conformance's associated type
|
|
404
|
+
/// satisfied), so a props type with only events still conforms.
|
|
405
|
+
private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
406
|
+
// An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
|
|
407
|
+
// the wire key visible in the generated source and independent of that implicit rule.
|
|
408
|
+
let cases = valueProps
|
|
409
|
+
.map { prop in
|
|
410
|
+
prop.name == prop.wireName
|
|
411
|
+
? " case \(prop.name)"
|
|
412
|
+
: " case \(prop.name) = \"\(prop.wireName)\""
|
|
413
|
+
}
|
|
414
|
+
.joined(separator: "\n")
|
|
415
|
+
if valueProps.isEmpty {
|
|
416
|
+
return """
|
|
417
|
+
public enum PropName: String, CaseIterable {
|
|
418
|
+
}
|
|
419
|
+
"""
|
|
420
|
+
}
|
|
421
|
+
return """
|
|
422
|
+
public enum PropName: String, CaseIterable {
|
|
423
|
+
\(raw: cases)
|
|
424
|
+
}
|
|
425
|
+
"""
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// The `PropSet` option set: one static member per value prop, at its declaration-order bit. This is
|
|
429
|
+
/// what a diff's `changedProps` holds, and what `changed(_:)` tests against, so the whole changed set
|
|
430
|
+
/// is one machine word.
|
|
431
|
+
private func propSetStruct(valueProps: [ViewProp]) -> DeclSyntax {
|
|
432
|
+
let members = valueProps.enumerated()
|
|
433
|
+
.map { index, prop in
|
|
434
|
+
" public static let \(prop.name) = PropSet(rawValue: 1 << \(index))"
|
|
435
|
+
}
|
|
436
|
+
.joined(separator: "\n")
|
|
437
|
+
|
|
438
|
+
if valueProps.isEmpty {
|
|
439
|
+
return """
|
|
440
|
+
public struct PropSet: OptionSet, Sendable {
|
|
441
|
+
public let rawValue: UInt64
|
|
442
|
+
|
|
443
|
+
public init(rawValue: UInt64) {
|
|
444
|
+
self.rawValue = rawValue
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
"""
|
|
448
|
+
}
|
|
449
|
+
return """
|
|
450
|
+
public struct PropSet: OptionSet, Sendable {
|
|
451
|
+
public let rawValue: UInt64
|
|
452
|
+
|
|
453
|
+
public init(rawValue: UInt64) {
|
|
454
|
+
self.rawValue = rawValue
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
\(raw: members)
|
|
458
|
+
}
|
|
459
|
+
"""
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/// The event props' names, verbatim, for core to register at view creation. Verbatim because a view
|
|
463
|
+
/// event prop is a React prop name Fabric carries as-is — unlike `@Event` on a module, which strips
|
|
464
|
+
/// the `on` prefix.
|
|
465
|
+
private func eventNamesConstant(eventProps: [ViewProp]) -> DeclSyntax {
|
|
466
|
+
let names = eventProps.map { "\"\($0.wireName)\"" }.joined(separator: ", ")
|
|
467
|
+
return """
|
|
468
|
+
public static let _eventNames: [String] = [\(raw: names)]
|
|
469
|
+
"""
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// The nested spelling of core's one generic diff, so a view writes `MyProps.Diff`.
|
|
473
|
+
private func diffTypealias() -> DeclSyntax {
|
|
474
|
+
return """
|
|
475
|
+
public typealias Diff = PropsDiff<Self>
|
|
476
|
+
"""
|
|
477
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// An attached macro that generates an optimized synchronous function descriptor.
|
|
2
|
+
/// Creates a peer function that returns `OptimizedFunctionDescriptor`, for use with
|
|
3
|
+
/// the `Function("name", descriptor)` overload in ModuleDefinition result builders.
|
|
4
|
+
///
|
|
5
|
+
/// Usage:
|
|
6
|
+
///
|
|
7
|
+
/// @OptimizedFunction
|
|
8
|
+
/// private func addNumbers(a: Double, b: Double) -> Double {
|
|
9
|
+
/// return a + b
|
|
10
|
+
/// }
|
|
11
|
+
///
|
|
12
|
+
/// // In definition():
|
|
13
|
+
/// Function("addNumbers", addNumbers())
|
|
14
|
+
///
|
|
15
|
+
/// The generated peer function uses the optimized JSI bridge path with
|
|
16
|
+
/// @convention(block) closures for maximum performance.
|
|
17
|
+
@attached(peer, names: arbitrary)
|
|
18
|
+
public macro OptimizedFunction() =
|
|
19
|
+
#externalMacro(module: "ExpoModulesMacros", type: "OptimizedFunctionAttachedMacro")
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Command-line front end for the scanner: parses the subcommand and paths, then delegates to the
|
|
4
|
+
/// matching `Scanner` entry (which runs the scan and writes its JSON output). Lives in the library
|
|
5
|
+
/// so the macro plugin executable can dispatch into it: the compiler always launches that executable
|
|
6
|
+
/// without arguments and speaks the plugin protocol over stdin, so any argument means a scanner
|
|
7
|
+
/// invocation. Each path may be a `.swift` file or a directory (scanned recursively for `.swift`
|
|
8
|
+
/// files).
|
|
9
|
+
///
|
|
10
|
+
/// Subcommands:
|
|
11
|
+
/// scan-modules <path>... fast: top-level `@ExpoModule` types, for autolinking
|
|
12
|
+
/// scan-exports <path>... deep: full JS-exported surface, for TS type generation
|
|
13
|
+
public enum ScannerCLI {
|
|
14
|
+
/// Runs the CLI for the given arguments (argv without the executable path) and returns the
|
|
15
|
+
/// process exit code: `0` on success, `1` if encoding the report fails, `2` on a usage error.
|
|
16
|
+
public static func run(arguments: [String]) -> Int32 {
|
|
17
|
+
// `-h`/`--help` anywhere is treated as a help request: print usage to stdout and exit 0.
|
|
18
|
+
if arguments.contains(where: { $0 == "-h" || $0 == "--help" }) {
|
|
19
|
+
printUsage(to: .standardOutput)
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
guard let subcommand = arguments.first else {
|
|
24
|
+
printUsage()
|
|
25
|
+
return 2
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var paths: [String] = []
|
|
29
|
+
var platform: String?
|
|
30
|
+
var defines: [String] = []
|
|
31
|
+
|
|
32
|
+
var rest = arguments.dropFirst().makeIterator()
|
|
33
|
+
while let argument = rest.next() {
|
|
34
|
+
switch argument {
|
|
35
|
+
case "--platform":
|
|
36
|
+
guard let value = rest.next() else {
|
|
37
|
+
return usageError("--platform requires a value")
|
|
38
|
+
}
|
|
39
|
+
platform = value
|
|
40
|
+
case "--define":
|
|
41
|
+
guard let value = rest.next() else {
|
|
42
|
+
return usageError("--define requires a value")
|
|
43
|
+
}
|
|
44
|
+
defines.append(value)
|
|
45
|
+
default:
|
|
46
|
+
paths.append(argument)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
switch subcommand {
|
|
51
|
+
case "scan-modules":
|
|
52
|
+
// scan-modules is platform-agnostic: each reported module carries the platforms that include
|
|
53
|
+
// it, and the consumer filters. An option selecting one platform would silently drop data.
|
|
54
|
+
guard platform == nil else {
|
|
55
|
+
return usageError("scan-modules does not take --platform; each module reports its 'platforms' and the consumer filters")
|
|
56
|
+
}
|
|
57
|
+
guard !paths.isEmpty else {
|
|
58
|
+
return usageError("scan-modules requires at least one path")
|
|
59
|
+
}
|
|
60
|
+
return Scanner.runModules(paths: paths, defines: defines)
|
|
61
|
+
|
|
62
|
+
case "scan-exports":
|
|
63
|
+
// The exports surface visitor doesn't evaluate `#if` blocks yet, so accepting the options
|
|
64
|
+
// here would silently do nothing.
|
|
65
|
+
guard platform == nil, defines.isEmpty else {
|
|
66
|
+
return usageError("scan-exports does not support --platform or --define")
|
|
67
|
+
}
|
|
68
|
+
guard !paths.isEmpty else {
|
|
69
|
+
return usageError("scan-exports requires at least one path")
|
|
70
|
+
}
|
|
71
|
+
return Scanner.runExports(paths: paths)
|
|
72
|
+
|
|
73
|
+
default:
|
|
74
|
+
return usageError("unknown subcommand '\(subcommand)'")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The invoked executable's basename, so the usage text matches however the tool was launched
|
|
80
|
+
/// (the `ExpoModulesMacros` shipped in the package, or a locally built `ExpoModulesMacros-tool`).
|
|
81
|
+
private var toolName: String {
|
|
82
|
+
return (CommandLine.arguments.first as NSString?)?.lastPathComponent ?? "ExpoModulesScanner"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private var usageText: String {
|
|
86
|
+
"""
|
|
87
|
+
usage: \(toolName) <subcommand> [options] <path> [<path> ...]
|
|
88
|
+
|
|
89
|
+
subcommands:
|
|
90
|
+
scan-modules fast scan for top-level @ExpoModule types (autolinking)
|
|
91
|
+
scan-exports deep scan of the full JS-exported surface (type generation)
|
|
92
|
+
|
|
93
|
+
options (scan-modules only):
|
|
94
|
+
--define <flag> treat a conditional compilation flag (e.g. DEBUG) as set when resolving
|
|
95
|
+
each module's 'platforms' list; repeatable
|
|
96
|
+
|
|
97
|
+
options:
|
|
98
|
+
-h, --help print this help and exit
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Prints the usage text to the given handle. Goes to stdout when help was explicitly requested
|
|
104
|
+
/// (a successful action), stderr when it accompanies a usage error.
|
|
105
|
+
private func printUsage(to handle: FileHandle = .standardError) {
|
|
106
|
+
handle.write(Data(usageText.utf8))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Reports a usage error on stderr, followed by the usage text, and returns the usage exit code.
|
|
110
|
+
private func usageError(_ message: String) -> Int32 {
|
|
111
|
+
FileHandle.standardError.write(Data("error: \(message)\n".utf8))
|
|
112
|
+
printUsage()
|
|
113
|
+
return 2
|
|
114
|
+
}
|