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,433 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Reads the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork".
|
|
5
|
+
Returns nil if the attribute has no arguments or the first argument is not a string literal.
|
|
6
|
+
*/
|
|
7
|
+
internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
8
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
9
|
+
let first = args.first,
|
|
10
|
+
let str = first.expression.as(StringLiteralExprSyntax.self),
|
|
11
|
+
let segment = str.segments.first?.as(StringSegmentSyntax.self) else {
|
|
12
|
+
return nil
|
|
13
|
+
}
|
|
14
|
+
return segment.content.text
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
Reads a labeled boolean-literal argument of an attribute, e.g. `@Event(sync: true)` -> true.
|
|
19
|
+
Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
|
|
20
|
+
*/
|
|
21
|
+
internal func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool? {
|
|
22
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
23
|
+
return nil
|
|
24
|
+
}
|
|
25
|
+
for arg in args where arg.label?.text == label {
|
|
26
|
+
guard let literal = arg.expression.as(BooleanLiteralExprSyntax.self) else {
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
return literal.literal.tokenKind == .keyword(.true)
|
|
30
|
+
}
|
|
31
|
+
return nil
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// True if the attribute lists the given `JSOptions` member, e.g. `@JS(.concurrent)` or
|
|
35
|
+
/// `@JS("name", [.concurrent])` -> true for "concurrent". Options are written as member-access
|
|
36
|
+
/// expressions (`.concurrent`), optionally inside an array literal when more than one is combined, so
|
|
37
|
+
/// both spellings are scanned. The leading string literal, when present, is the JS name and is skipped.
|
|
38
|
+
internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
39
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
for arg in args {
|
|
43
|
+
if namesOption(arg.expression, option) {
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
if let array = arg.expression.as(ArrayExprSyntax.self),
|
|
47
|
+
array.elements.contains(where: { namesOption($0.expression, option) }) {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// True when the expression is the member access `.<option>` (or `JSOptions.<option>`).
|
|
55
|
+
private func namesOption(_ expression: ExprSyntax, _ option: String) -> Bool {
|
|
56
|
+
guard let member = expression.as(MemberAccessExprSyntax.self) else {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return member.declName.baseName.text == option
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// True if the `@JS`-marked declaration opted into running off the JavaScript thread with
|
|
63
|
+
/// `@JS(.concurrent)`. Such a member is left unstamped and gets `@concurrent` instead, so its body
|
|
64
|
+
/// runs on the concurrent pool rather than inheriting the JS thread.
|
|
65
|
+
internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
66
|
+
guard let attribute = memberAttributes(of: decl).firstAttribute(named: "JS") else {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
return hasJSOption(attribute, named: "concurrent")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
|
|
73
|
+
/// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
|
|
74
|
+
internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
75
|
+
if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == "Optional" {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
|
|
85
|
+
/// optional.
|
|
86
|
+
internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
87
|
+
if let optional = type.as(OptionalTypeSyntax.self) {
|
|
88
|
+
return optional.wrappedType
|
|
89
|
+
}
|
|
90
|
+
if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
91
|
+
return implicitlyUnwrapped.wrappedType
|
|
92
|
+
}
|
|
93
|
+
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
94
|
+
identifier.name.text == "Optional",
|
|
95
|
+
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
96
|
+
case .type(let wrapped) = argument.argument {
|
|
97
|
+
return wrapped
|
|
98
|
+
}
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// True when the modifiers make the member type-level (`static` or `class`). A shared object routes
|
|
103
|
+
/// such members to its constructor (JS-static) and instance members to its prototype; the `@JS`
|
|
104
|
+
/// conformance assertion also uses this to emit its peer in the matching metatype context.
|
|
105
|
+
internal func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
106
|
+
return modifiers.contains {
|
|
107
|
+
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// True if a trailing occurrence of this parameter may be omitted by the JS caller: it either has a
|
|
112
|
+
/// default value (Swift applies it) or is an optional type (an absent slot becomes `nil`). The arity
|
|
113
|
+
/// range and the per-arity call branches are derived from this.
|
|
114
|
+
internal func isOmittable(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
115
|
+
return hasDefaultValue(parameter) || isOptionalType(parameter.type)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// True if the parameter declares a default value (`b: Int = 5`). An omitted defaulted slot is left
|
|
119
|
+
/// out of the call so Swift fills in the default, distinguishing it from an omitted optional slot
|
|
120
|
+
/// (passed `nil`).
|
|
121
|
+
internal func hasDefaultValue(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
122
|
+
return parameter.defaultValue != nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
True if the declaration is a `@Event(sync: true)` property. A sync event dispatches inline on the
|
|
127
|
+
JS thread instead of scheduling, so `@ExpoModule`/`@SharedObject` stamp it with `@JavaScriptActor`,
|
|
128
|
+
making "must be called on the JS thread" a compile-time guarantee at the call site. Async events
|
|
129
|
+
(the default) are deliberately not stamped: their `emit` schedules onto the JS thread itself, so
|
|
130
|
+
they stay callable from any thread.
|
|
131
|
+
*/
|
|
132
|
+
internal func isSyncEventMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
133
|
+
guard let varDecl = decl.as(VariableDeclSyntax.self),
|
|
134
|
+
let attribute = varDecl.attributes.firstAttribute(named: "Event") else {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
return boolArgument(of: attribute, label: "sync") == true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
|
|
142
|
+
and returns the type names referenced (e.g. `["Foo", "Bar"]`). Each element must be a
|
|
143
|
+
`<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
|
|
144
|
+
*/
|
|
145
|
+
internal func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
|
|
146
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
147
|
+
return []
|
|
148
|
+
}
|
|
149
|
+
for arg in args where arg.label?.text == label {
|
|
150
|
+
guard let array = arg.expression.as(ArrayExprSyntax.self) else {
|
|
151
|
+
return []
|
|
152
|
+
}
|
|
153
|
+
return array.elements.compactMap { element -> String? in
|
|
154
|
+
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
155
|
+
memberAccess.declName.baseName.text == "self",
|
|
156
|
+
let base = memberAccess.base else {
|
|
157
|
+
return nil
|
|
158
|
+
}
|
|
159
|
+
// A bare `CardView.self` has a `DeclReferenceExprSyntax` base; a qualified
|
|
160
|
+
// `Outer.CardView.self` has a `MemberAccessExprSyntax` one. Both name a type the generated
|
|
161
|
+
// code can spell, so take the base verbatim rather than only its last component: dropping the
|
|
162
|
+
// qualified form would silently omit the entry, and the type would never be registered.
|
|
163
|
+
if base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self) {
|
|
164
|
+
return base.trimmedDescription
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return []
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
Returns true if the class's inheritance clause names any of the given identifiers.
|
|
174
|
+
Matches by base identifier only, so `Module`, `ExpoModulesCore.Module`, and
|
|
175
|
+
`Module<Foo>` all match an entry of "Module".
|
|
176
|
+
*/
|
|
177
|
+
internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>) -> Bool {
|
|
178
|
+
guard let inherited = classDecl.inheritanceClause?.inheritedTypes else {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
for entry in inherited {
|
|
182
|
+
if let name = baseIdentifier(of: entry.type), names.contains(name) {
|
|
183
|
+
return true
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
Returns the rightmost identifier of a type, e.g. `Foo` for `Foo`, `Foo` for
|
|
191
|
+
`Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
|
|
192
|
+
*/
|
|
193
|
+
internal func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
194
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
195
|
+
return identifier.name.text
|
|
196
|
+
}
|
|
197
|
+
if let member = type.as(MemberTypeSyntax.self) {
|
|
198
|
+
return member.name.text
|
|
199
|
+
}
|
|
200
|
+
return nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
True if the declaration carries a `@JS` attribute. Works for functions, properties, and inits;
|
|
205
|
+
returns false for any other decl kind.
|
|
206
|
+
*/
|
|
207
|
+
internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
208
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
209
|
+
return funcDecl.attributes.firstAttribute(named: "JS") != nil
|
|
210
|
+
}
|
|
211
|
+
if let varDecl = decl.as(VariableDeclSyntax.self) {
|
|
212
|
+
return varDecl.attributes.firstAttribute(named: "JS") != nil
|
|
213
|
+
}
|
|
214
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
215
|
+
return initDecl.attributes.firstAttribute(named: "JS") != nil
|
|
216
|
+
}
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Decides whether the macro should stamp `@JavaScriptActor` on a `@JS`-marked member.
|
|
221
|
+
/// The macro defers to the user when they've already chosen an isolation:
|
|
222
|
+
/// - the member opted out with `@JS(.concurrent)`
|
|
223
|
+
/// - the `nonisolated` modifier is present on the member
|
|
224
|
+
/// - any attribute whose name matches a known global actor (`@MainActor`, `@JavaScriptActor`)
|
|
225
|
+
/// or follows the `*Actor` naming convention is present on the member or its enclosing type
|
|
226
|
+
/// `async` members are stamped too: an `async` function starts its execution on the JS thread and
|
|
227
|
+
/// stays there until the first suspension point, where it may hop to another executor.
|
|
228
|
+
internal func shouldStampJavaScriptActor(
|
|
229
|
+
on member: DeclSyntaxProtocol,
|
|
230
|
+
enclosedBy enclosing: some DeclGroupSyntax
|
|
231
|
+
) -> Bool {
|
|
232
|
+
if isConcurrentJSMember(member) {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let modifiers = memberModifiers(of: member)
|
|
237
|
+
if modifiers.contains(where: { $0.name.text == "nonisolated" }) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let memberAttributes = memberAttributes(of: member)
|
|
242
|
+
if memberAttributes.contains(where: hasGlobalActorShape) {
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if enclosing.attributes.contains(where: hasGlobalActorShape) {
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func memberModifiers(of decl: DeclSyntaxProtocol) -> DeclModifierListSyntax {
|
|
254
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
255
|
+
return funcDecl.modifiers
|
|
256
|
+
}
|
|
257
|
+
if let varDecl = decl.as(VariableDeclSyntax.self) {
|
|
258
|
+
return varDecl.modifiers
|
|
259
|
+
}
|
|
260
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
261
|
+
return initDecl.modifiers
|
|
262
|
+
}
|
|
263
|
+
return DeclModifierListSyntax()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
internal func memberAttributes(of decl: DeclSyntaxProtocol) -> AttributeListSyntax {
|
|
267
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
268
|
+
return funcDecl.attributes
|
|
269
|
+
}
|
|
270
|
+
if let varDecl = decl.as(VariableDeclSyntax.self) {
|
|
271
|
+
return varDecl.attributes
|
|
272
|
+
}
|
|
273
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
274
|
+
return initDecl.attributes
|
|
275
|
+
}
|
|
276
|
+
return AttributeListSyntax()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool {
|
|
280
|
+
guard let attribute = element.as(AttributeSyntax.self) else {
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
let name = attribute.attributeName.trimmedDescription
|
|
284
|
+
return name.hasSuffix("Actor")
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
|
|
288
|
+
/// when the expression isn't one of those literals. Used to recover a property's type when it has no
|
|
289
|
+
/// annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
|
|
290
|
+
/// Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
|
|
291
|
+
/// macro can't know (function calls, collection literals, member access) return `nil`.
|
|
292
|
+
internal func inferredLiteralType(of expression: ExprSyntax) -> String? {
|
|
293
|
+
if expression.is(StringLiteralExprSyntax.self) {
|
|
294
|
+
return "String"
|
|
295
|
+
}
|
|
296
|
+
if expression.is(FloatLiteralExprSyntax.self) {
|
|
297
|
+
return "Double"
|
|
298
|
+
}
|
|
299
|
+
if expression.is(IntegerLiteralExprSyntax.self) {
|
|
300
|
+
return "Int"
|
|
301
|
+
}
|
|
302
|
+
if expression.is(BooleanLiteralExprSyntax.self) {
|
|
303
|
+
return "Bool"
|
|
304
|
+
}
|
|
305
|
+
return nil
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
extension AttributeListSyntax {
|
|
309
|
+
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
310
|
+
for element in self {
|
|
311
|
+
if let attr = element.as(AttributeSyntax.self),
|
|
312
|
+
attr.attributeName.trimmedDescription == name {
|
|
313
|
+
return attr
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return nil
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// A type spelled so it's valid in expression position (before `.decode`/`.encode`, before
|
|
321
|
+
/// `.getDynamicType()`, or after `as!`). Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
322
|
+
/// type-annotation position, so a trailing `!` is rewritten to `?` (`T!` and `T?` are both
|
|
323
|
+
/// `Optional<T>`, which the conversion layer treats identically). Other type spellings pass through
|
|
324
|
+
/// unchanged.
|
|
325
|
+
internal func expressionType(_ type: String) -> String {
|
|
326
|
+
guard type.hasSuffix("!") else {
|
|
327
|
+
return type
|
|
328
|
+
}
|
|
329
|
+
return type.dropLast() + "?"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// The decode expression for a boundary type read from `valueExpression` (a `JavaScriptUnownedValue`),
|
|
333
|
+
/// as it appears after `try`. A free-form type (`Any`, `[Any]`, `[String: Any]`) can't conform to
|
|
334
|
+
/// `JavaScriptDecodable`, so it decodes through the dedicated `JavaScriptValue.decodeAny…` entry point
|
|
335
|
+
/// keyed to its shape; every other type decodes through its own static `decode`, spelled in expression
|
|
336
|
+
/// position (`T!` rewritten to `T?`).
|
|
337
|
+
internal func decodeCall(_ type: String, from valueExpression: String) -> String {
|
|
338
|
+
if let method = freeFormDecodeMethod(for: type) {
|
|
339
|
+
return "JavaScriptValue.\(method)(\(valueExpression), in: runtime)"
|
|
340
|
+
}
|
|
341
|
+
return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// MARK: - @JS property collection
|
|
345
|
+
|
|
346
|
+
/// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
|
|
347
|
+
/// Shared between `@ExpoModule` and `@SharedObject` — the resulting properties are receiver-agnostic;
|
|
348
|
+
/// the decorator that emits them picks the receiver (module `self` vs. shared-object `_self`).
|
|
349
|
+
internal func collectProperties(
|
|
350
|
+
varDecl: VariableDeclSyntax,
|
|
351
|
+
attribute: AttributeSyntax
|
|
352
|
+
) -> [JSProperty] {
|
|
353
|
+
let jsNameOverride = jsNameArgument(of: attribute)
|
|
354
|
+
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
355
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
356
|
+
|
|
357
|
+
return varDecl.bindings.compactMap { binding in
|
|
358
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
359
|
+
return nil
|
|
360
|
+
}
|
|
361
|
+
let swiftName = ident.identifier.text
|
|
362
|
+
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
363
|
+
// when there's none. `nil` falls back to inference at the use site.
|
|
364
|
+
let valueType = binding.typeAnnotation?.type.trimmedDescription
|
|
365
|
+
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
366
|
+
return JSProperty(
|
|
367
|
+
swiftName: swiftName,
|
|
368
|
+
jsName: jsNameOverride ?? swiftName,
|
|
369
|
+
valueType: valueType,
|
|
370
|
+
isSettable: isVar && bindingIsSettable(binding)
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// Whether a `var` binding is settable from JS. A stored property (no accessor block) is settable;
|
|
376
|
+
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
377
|
+
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
378
|
+
/// observers imply stored storage, which is also settable.
|
|
379
|
+
internal func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
380
|
+
guard let accessorBlock = binding.accessorBlock else {
|
|
381
|
+
return true
|
|
382
|
+
}
|
|
383
|
+
switch accessorBlock.accessors {
|
|
384
|
+
case .accessors(let accessors):
|
|
385
|
+
return accessors.contains { accessor in
|
|
386
|
+
switch accessor.accessorSpecifier.tokenKind {
|
|
387
|
+
case .keyword(.set), .keyword(.willSet), .keyword(.didSet):
|
|
388
|
+
return true
|
|
389
|
+
default:
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
case .getter:
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// True if the variable declaration carries a modifier that excludes it from being a stored property
|
|
399
|
+
/// of the surface: `static`, `class` (type-level storage), `private`, `fileprivate`, or `lazy`.
|
|
400
|
+
/// Shared by `@Record` and `@ViewProps`, which apply the same "every stored property counts" rule.
|
|
401
|
+
internal func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
402
|
+
for modifier in modifiers {
|
|
403
|
+
switch modifier.name.tokenKind {
|
|
404
|
+
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
405
|
+
return true
|
|
406
|
+
default:
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/// True if the type's inheritance clause already lists a protocol with the given name. Matches
|
|
414
|
+
/// either the bare identifier (`Record`) or a qualified member access ending in the name
|
|
415
|
+
/// (`ExpoModulesCore.Record`). Used by the extension macros to skip a conformance the author already
|
|
416
|
+
/// spelled out. Works for `struct`, `class`, and `enum` declarations; any other declaration kind has no
|
|
417
|
+
/// inheritance clause to read and reports `false`.
|
|
418
|
+
internal func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
|
|
419
|
+
let inheritanceClause: InheritanceClauseSyntax?
|
|
420
|
+
if let structDecl = declaration.as(StructDeclSyntax.self) {
|
|
421
|
+
inheritanceClause = structDecl.inheritanceClause
|
|
422
|
+
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
|
|
423
|
+
inheritanceClause = classDecl.inheritanceClause
|
|
424
|
+
} else if let enumDecl = declaration.as(EnumDeclSyntax.self) {
|
|
425
|
+
inheritanceClause = enumDecl.inheritanceClause
|
|
426
|
+
} else {
|
|
427
|
+
return false
|
|
428
|
+
}
|
|
429
|
+
guard let inherited = inheritanceClause?.inheritedTypes else {
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
return inherited.contains { baseIdentifier(of: $0.type) == name }
|
|
433
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import SwiftSyntaxMacros
|
|
2
|
+
|
|
3
|
+
/// Generates Objective-C type encoding string for the given signature
|
|
4
|
+
internal func generateTypeEncoding(returnType: String, paramTypes: [String]) throws -> String {
|
|
5
|
+
var encoding = ""
|
|
6
|
+
|
|
7
|
+
// Encode return type
|
|
8
|
+
guard let returnEncoding = typeToEncoding(returnType) else {
|
|
9
|
+
throw MacroExpansionErrorMessage("Unsupported return type: \(returnType)")
|
|
10
|
+
}
|
|
11
|
+
encoding += returnEncoding
|
|
12
|
+
|
|
13
|
+
// Add block marker
|
|
14
|
+
encoding += "@?"
|
|
15
|
+
|
|
16
|
+
// Encode parameters
|
|
17
|
+
for paramType in paramTypes {
|
|
18
|
+
guard let paramEncoding = typeToEncoding(paramType) else {
|
|
19
|
+
throw MacroExpansionErrorMessage("Unsupported parameter type: \(paramType)")
|
|
20
|
+
}
|
|
21
|
+
encoding += paramEncoding
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return encoding
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Maps Swift type to Objective-C type encoding character
|
|
28
|
+
internal func typeToEncoding(_ type: String) -> String? {
|
|
29
|
+
switch type {
|
|
30
|
+
case "Double":
|
|
31
|
+
return "d"
|
|
32
|
+
case "Int", "Int64":
|
|
33
|
+
return "q"
|
|
34
|
+
case "String":
|
|
35
|
+
return "@"
|
|
36
|
+
case "Bool":
|
|
37
|
+
return "B"
|
|
38
|
+
case "Void", "()":
|
|
39
|
+
return "v"
|
|
40
|
+
default:
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
struct MacroExpansionErrorMessage: Error, CustomStringConvertible {
|
|
46
|
+
let message: String
|
|
47
|
+
|
|
48
|
+
init(_ message: String) {
|
|
49
|
+
self.message = message
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var description: String {
|
|
53
|
+
return message
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import ExpoModulesScanner
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftCompilerPlugin
|
|
4
|
+
import SwiftSyntaxMacros
|
|
5
|
+
|
|
6
|
+
struct ExpoModulesMacrosPlugin: CompilerPlugin {
|
|
7
|
+
let providingMacros: [Macro.Type] = [
|
|
8
|
+
OptimizedFunctionAttachedMacro.self,
|
|
9
|
+
JSMacro.self,
|
|
10
|
+
EventMacro.self,
|
|
11
|
+
ExpoModuleMacro.self,
|
|
12
|
+
SharedObjectMacro.self,
|
|
13
|
+
RecordMacro.self,
|
|
14
|
+
UnionMacro.self,
|
|
15
|
+
ViewPropsMacro.self,
|
|
16
|
+
ExpoViewMacro.self,
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// The executable doubles as the scanner CLI. The compiler always launches a plugin executable
|
|
21
|
+
/// without arguments and speaks the plugin protocol over stdin, so any argument means a scanner
|
|
22
|
+
/// invocation (`ExpoModulesMacros scan-modules <path>...`); with none, this starts the plugin
|
|
23
|
+
/// server exactly as `@main` on the `CompilerPlugin` type would.
|
|
24
|
+
@main
|
|
25
|
+
enum EntryPoint {
|
|
26
|
+
static func main() throws {
|
|
27
|
+
let arguments = Array(CommandLine.arguments.dropFirst())
|
|
28
|
+
if arguments.isEmpty {
|
|
29
|
+
try ExpoModulesMacrosPlugin.main()
|
|
30
|
+
} else {
|
|
31
|
+
exit(ScannerCLI.run(arguments: arguments))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// Where a directly-bound closure gets the Swift value it calls into. This is one of the two
|
|
4
|
+
/// orthogonal axes of a binding; the other is the `Phase` (which JS object the member is installed
|
|
5
|
+
/// on). The two are independent: choosing the prototype phase does not by itself decide whether the
|
|
6
|
+
/// receiver is `self`, `_self`, or the metatype.
|
|
7
|
+
///
|
|
8
|
+
/// - A module is a singleton, so its bindings call `self` and ignore the JS `this`.
|
|
9
|
+
/// - A shared-object *instance* member has a distinct native instance per JS object, so it recovers
|
|
10
|
+
/// the typed receiver from `this`.
|
|
11
|
+
/// - A `static`/`class` member has no instance at all; it calls the Swift member on the metatype
|
|
12
|
+
/// (`Cache.open(…)`) and ignores `this` (which, on the static side, is the constructor).
|
|
13
|
+
internal enum Receiver {
|
|
14
|
+
/// The module singleton; the closure captures `self` strong.
|
|
15
|
+
case module
|
|
16
|
+
/// A shared object of the given concrete type; the closure captures nothing and recovers the receiver
|
|
17
|
+
/// from `this` per call.
|
|
18
|
+
case sharedObject(typeName: String)
|
|
19
|
+
/// A `static`/`class` member of the given concrete type; the closure captures nothing and calls the
|
|
20
|
+
/// Swift member on the type itself, ignoring `this`.
|
|
21
|
+
case staticMember(typeName: String)
|
|
22
|
+
|
|
23
|
+
/// The expression the body calls members on: `self` for a module, `_self` (bound by `unwrapStatement`)
|
|
24
|
+
/// for a shared-object instance, the type name for a static member. The leading underscore on `_self`
|
|
25
|
+
/// avoids colliding with a user member like `var owner`.
|
|
26
|
+
var callee: String {
|
|
27
|
+
switch self {
|
|
28
|
+
case .module:
|
|
29
|
+
return "self"
|
|
30
|
+
case .sharedObject:
|
|
31
|
+
return "_self"
|
|
32
|
+
case .staticMember(let typeName):
|
|
33
|
+
return typeName
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// The leading body line binding the receiver, or `nil` when nothing needs to be unwrapped (a module
|
|
38
|
+
/// reads `self` directly; a static member calls the type directly). For a shared-object instance,
|
|
39
|
+
/// `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign object or a type
|
|
40
|
+
/// mismatch.
|
|
41
|
+
///
|
|
42
|
+
/// The `this` object always comes from the borrowed `JavaScriptUnownedValue` (`asObject(in:)`): an
|
|
43
|
+
/// async binding unwraps in its synchronous decode phase, before the borrowed value's lifetime ends,
|
|
44
|
+
/// and only the recovered native instance crosses into the async body.
|
|
45
|
+
var unwrapStatement: String? {
|
|
46
|
+
switch self {
|
|
47
|
+
case .module, .staticMember:
|
|
48
|
+
return nil
|
|
49
|
+
case .sharedObject(let typeName):
|
|
50
|
+
return "let _self = try SharedObject.native(from: this.asObject(in: runtime), as: \(typeName).self)"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The capture-clause fragment (with a trailing space, or empty when nothing is captured). A module
|
|
55
|
+
/// captures `self` strong; a shared-object instance and a static member capture nothing.
|
|
56
|
+
var captureClause: String {
|
|
57
|
+
switch self {
|
|
58
|
+
case .module:
|
|
59
|
+
return "[self] "
|
|
60
|
+
case .sharedObject, .staticMember:
|
|
61
|
+
return ""
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Which JS object a set of bindings is installed on: the second orthogonal axis alongside `Receiver`.
|
|
67
|
+
/// It selects the decorator entry point's argument label and the local name the body binds members onto,
|
|
68
|
+
/// mirroring JS class semantics (a class has a constructor function whose `.prototype` carries instance
|
|
69
|
+
/// members).
|
|
70
|
+
/// The raw value is the argument label of the decorator entry point, which is also the local name the
|
|
71
|
+
/// body binds members onto.
|
|
72
|
+
internal enum Phase: String {
|
|
73
|
+
/// A concrete JS object: a singleton's own object (the module). Instance method; receiver `self`.
|
|
74
|
+
case object
|
|
75
|
+
/// The class constructor's `prototype`, carrying per-instance members. Static; receiver `_self`.
|
|
76
|
+
case prototype
|
|
77
|
+
/// The class constructor function itself, carrying `static`/`class` members. Static; receiver the type.
|
|
78
|
+
case constructor
|
|
79
|
+
}
|