expo-modules-macros 0.31.0 → 0.33.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/{swift.yml → macos.yml} +12 -3
- package/AGENTS.md +4 -2
- package/Package.swift +10 -1
- package/README.md +6 -6
- 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 +174 -20
- 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/EventMacro.swift +12 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +12 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +64 -30
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +124 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/RecordMacro.swift +68 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +79 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/UnionMacro.swift +101 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +574 -23
- package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +111 -2
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +28 -2
- package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
- package/build/types.d.ts +58 -3
- package/package.json +5 -5
|
@@ -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
|
}
|
|
@@ -40,6 +40,94 @@ internal func diagnoseJSNameCollisions(
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/// The JavaScript object that a set of `@JS` members binds onto, for `diagnoseReservedJSNames`.
|
|
44
|
+
internal enum JSBindingTarget {
|
|
45
|
+
/// The module object of an `@ExpoModule` class.
|
|
46
|
+
case module
|
|
47
|
+
/// The prototype of a shared object class, which holds the instance members.
|
|
48
|
+
case prototype
|
|
49
|
+
/// The constructor of a shared object class, which holds the static members.
|
|
50
|
+
case constructor
|
|
51
|
+
|
|
52
|
+
/// The names that JavaScript already gives a meaning on this object, with what binding a member
|
|
53
|
+
/// under one would break.
|
|
54
|
+
var reservedNames: [String: String] {
|
|
55
|
+
let proto = "setting '__proto__' on an object replaces its prototype instead of adding a member"
|
|
56
|
+
switch self {
|
|
57
|
+
case .module:
|
|
58
|
+
return ["__proto__": proto]
|
|
59
|
+
case .prototype:
|
|
60
|
+
return [
|
|
61
|
+
"__proto__": proto,
|
|
62
|
+
"constructor": "a member named 'constructor' replaces the prototype's reference to the class",
|
|
63
|
+
]
|
|
64
|
+
case .constructor:
|
|
65
|
+
return [
|
|
66
|
+
"__proto__": proto,
|
|
67
|
+
"prototype": "a static member named 'prototype' replaces the prototype that holds the instance members",
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Reports each `@JS` member whose JavaScript name JavaScript already gives a meaning on the object that
|
|
74
|
+
/// the member binds onto, such as `constructor` on a shared object's prototype.
|
|
75
|
+
internal func diagnoseReservedJSNames(
|
|
76
|
+
functions: [JSFunction],
|
|
77
|
+
properties: [JSProperty],
|
|
78
|
+
on target: JSBindingTarget,
|
|
79
|
+
in context: some MacroExpansionContext
|
|
80
|
+
) {
|
|
81
|
+
let members = functions.map(BoundMember.init(function:)) + properties.map(BoundMember.init(property:))
|
|
82
|
+
for member in members {
|
|
83
|
+
guard let reason = target.reservedNames[member.jsName] else {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
context.diagnose(
|
|
87
|
+
Diagnostic(
|
|
88
|
+
node: member.nameToken,
|
|
89
|
+
message: ReservedJSNameMessage(
|
|
90
|
+
"The JavaScript name '\(member.jsName)' can't be bound: \(reason). Give the \(member.kind) another name with \(member.renaming)."
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Reports each `@Event` property of a class body whose JavaScript event name an earlier `@Event`
|
|
98
|
+
/// property already uses. Both properties emit under that name, so a listener can't tell which one fired.
|
|
99
|
+
internal func diagnoseEventNameCollisions(in memberBlock: MemberBlockSyntax, in context: some MacroExpansionContext) {
|
|
100
|
+
var firstEvents: [String: TokenSyntax] = [:]
|
|
101
|
+
|
|
102
|
+
for member in memberBlock.members {
|
|
103
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
104
|
+
let attribute = varDecl.attributes.firstAttribute(named: "Event"),
|
|
105
|
+
let jsName = eventJSName(of: varDecl, attribute: attribute),
|
|
106
|
+
let nameToken = varDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier
|
|
107
|
+
else {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
guard let first = firstEvents[jsName] else {
|
|
111
|
+
firstEvents[jsName] = nameToken
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
context.diagnose(
|
|
115
|
+
Diagnostic(
|
|
116
|
+
node: nameToken,
|
|
117
|
+
message: EventNameCollisionMessage(
|
|
118
|
+
"The event name '\(jsName)' is already used by the earlier @Event '\(first.text)'. Both properties emit the same event, so a JavaScript listener can't tell them apart. Give one of them another name with @Event(\"...\")."
|
|
119
|
+
),
|
|
120
|
+
notes: [
|
|
121
|
+
Note(
|
|
122
|
+
node: Syntax(first),
|
|
123
|
+
message: JSNameCollisionNote("'\(jsName)' is first used here", id: "event-name-collision-first")
|
|
124
|
+
)
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
43
131
|
private struct BoundMember {
|
|
44
132
|
let jsName: String
|
|
45
133
|
/// How the message names the member, such as `@JS function`.
|
|
@@ -74,11 +162,32 @@ private struct JSNameCollisionMessage: DiagnosticMessage {
|
|
|
74
162
|
}
|
|
75
163
|
}
|
|
76
164
|
|
|
77
|
-
private struct
|
|
165
|
+
private struct ReservedJSNameMessage: DiagnosticMessage {
|
|
166
|
+
let message: String
|
|
167
|
+
let diagnosticID = MessageID(domain: "ExpoModulesMacros", id: "js-reserved-name")
|
|
168
|
+
let severity: DiagnosticSeverity = .error
|
|
169
|
+
|
|
170
|
+
init(_ message: String) {
|
|
171
|
+
self.message = message
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private struct EventNameCollisionMessage: DiagnosticMessage {
|
|
78
176
|
let message: String
|
|
79
|
-
let
|
|
177
|
+
let diagnosticID = MessageID(domain: "ExpoModulesMacros", id: "event-name-collision")
|
|
178
|
+
let severity: DiagnosticSeverity = .warning
|
|
80
179
|
|
|
81
180
|
init(_ message: String) {
|
|
82
181
|
self.message = message
|
|
83
182
|
}
|
|
84
183
|
}
|
|
184
|
+
|
|
185
|
+
private struct JSNameCollisionNote: NoteMessage {
|
|
186
|
+
let message: String
|
|
187
|
+
let noteID: MessageID
|
|
188
|
+
|
|
189
|
+
init(_ message: String, id: String = "js-name-collision-first") {
|
|
190
|
+
self.message = message
|
|
191
|
+
self.noteID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -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
|
}
|
|
@@ -159,6 +161,17 @@ internal struct SpecifiedType {
|
|
|
159
161
|
/// a fresh decoded value satisfies either way.
|
|
160
162
|
internal let ownershipSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
|
|
161
163
|
|
|
164
|
+
/// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
|
|
165
|
+
/// label would make it a tuple, which Swift rejects with a single element anyway.
|
|
166
|
+
internal func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
|
|
167
|
+
guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
168
|
+
only.firstName == nil
|
|
169
|
+
else {
|
|
170
|
+
return type
|
|
171
|
+
}
|
|
172
|
+
return withoutParentheses(only.type)
|
|
173
|
+
}
|
|
174
|
+
|
|
162
175
|
/// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
|
|
163
176
|
/// optional.
|
|
164
177
|
internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
@@ -349,6 +362,19 @@ internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
349
362
|
return false
|
|
350
363
|
}
|
|
351
364
|
|
|
365
|
+
/// The generic parameter clause of a `@JS` function or initializer, or `nil` when it has none. The
|
|
366
|
+
/// binding decodes each argument by its static type, so a generic member can't be bound: `@JS` reports
|
|
367
|
+
/// it, and `@ExpoModule` and `@SharedObject` leave it out.
|
|
368
|
+
internal func jsGenericParameterClause(of decl: DeclSyntaxProtocol) -> GenericParameterClauseSyntax? {
|
|
369
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
370
|
+
return funcDecl.genericParameterClause
|
|
371
|
+
}
|
|
372
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
373
|
+
return initDecl.genericParameterClause
|
|
374
|
+
}
|
|
375
|
+
return nil
|
|
376
|
+
}
|
|
377
|
+
|
|
352
378
|
/// Decides whether the macro should stamp `@JavaScriptActor` on a `@JS`-marked member.
|
|
353
379
|
/// The macro defers to the user when they've already chosen an isolation:
|
|
354
380
|
/// - the member opted out with `@JS(.concurrent)`
|
|
@@ -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.33.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.33.0",
|
|
30
|
+
"@expo/modules-macros-linux-x64": "0.33.0",
|
|
31
|
+
"@expo/modules-macros-win32-arm64": "0.33.0",
|
|
32
|
+
"@expo/modules-macros-win32-x64": "0.33.0"
|
|
33
33
|
}
|
|
34
34
|
}
|