expo-modules-macros 0.29.0 → 0.30.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 +2 -2
- package/README.md +35 -4
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/CLI.swift +46 -9
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +37 -1
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/MacroExpander.swift +117 -26
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +20 -10
- package/apple/Sources/ExpoModulesCLI/Commands/Fix/RunFix.swift +228 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +1 -1
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +8 -10
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +48 -34
- package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +3 -3
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +8 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +54 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +9 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +36 -6
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +11 -25
- package/apple/Sources/ExpoModulesMacrosCore/Support/PublicAccess.swift +105 -0
- package/build/types.d.ts +15 -6
- package/package.json +5 -5
|
@@ -13,26 +13,12 @@ internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
|
13
13
|
return segment.content.text
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
for arg in args where arg.label?.text == label {
|
|
23
|
-
guard let literal = arg.expression.as(BooleanLiteralExprSyntax.self) else {
|
|
24
|
-
return nil
|
|
25
|
-
}
|
|
26
|
-
return literal.literal.tokenKind == .keyword(.true)
|
|
27
|
-
}
|
|
28
|
-
return nil
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/// True if the attribute lists the given `JSOptions` member, e.g. `@JS(.concurrent)` or
|
|
32
|
-
/// `@JS("name", [.concurrent])` -> true for "concurrent". Options are written as member-access
|
|
33
|
-
/// expressions (`.concurrent`), optionally inside an array literal when more than one is combined, so
|
|
34
|
-
/// both spellings are scanned. The leading string literal, when present, is the JS name and is skipped.
|
|
35
|
-
internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
16
|
+
/// True if the attribute lists the given option member, e.g. `@JS(.concurrent)`, `@Event(.sync)` or
|
|
17
|
+
/// `@Event("name", EventOptions.sync)` -> true for the matching name. Options are written as
|
|
18
|
+
/// member-access expressions, one per variadic argument. Array literals are scanned too, though the
|
|
19
|
+
/// variadic declarations in `expo-modules-core` don't accept them. The leading string literal, when
|
|
20
|
+
/// present, is the JS name and is skipped.
|
|
21
|
+
internal func hasOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
36
22
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
37
23
|
return false
|
|
38
24
|
}
|
|
@@ -49,7 +35,7 @@ internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) ->
|
|
|
49
35
|
return false
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
/// True when the expression is the member access `.<option>` (or `JSOptions.<option>`).
|
|
38
|
+
/// True when the expression is the member access `.<option>` (or `JSOptions.<option>`, `EventOptions.<option>`).
|
|
53
39
|
private func namesOption(_ expression: ExprSyntax, _ option: String) -> Bool {
|
|
54
40
|
guard let member = expression.as(MemberAccessExprSyntax.self) else {
|
|
55
41
|
return false
|
|
@@ -64,7 +50,7 @@ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
64
50
|
guard let attribute = memberAttributes(of: decl).firstAttribute(named: "JS") else {
|
|
65
51
|
return false
|
|
66
52
|
}
|
|
67
|
-
return
|
|
53
|
+
return hasOption(attribute, named: "concurrent")
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
/// The `.withAsync("name")` option of a `@JS` attribute, e.g. `@JS(.withAsync("loadAsync"))` or
|
|
@@ -74,7 +60,7 @@ internal func asyncVariantOption(of attribute: AttributeSyntax) -> FunctionCallE
|
|
|
74
60
|
return asyncVariantOptions(of: attribute).first
|
|
75
61
|
}
|
|
76
62
|
|
|
77
|
-
/// Every `.withAsync(...)` option of a `@JS` attribute, in source order. Like `
|
|
63
|
+
/// Every `.withAsync(...)` option of a `@JS` attribute, in source order. Like `hasOption`, it scans
|
|
78
64
|
/// the arguments and the elements of an array literal.
|
|
79
65
|
internal func asyncVariantOptions(of attribute: AttributeSyntax) -> [FunctionCallExprSyntax] {
|
|
80
66
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
@@ -290,7 +276,7 @@ internal func hasDefaultValue(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
|
290
276
|
return parameter.defaultValue != nil
|
|
291
277
|
}
|
|
292
278
|
|
|
293
|
-
/// True if the declaration is a `@Event(sync
|
|
279
|
+
/// True if the declaration is a `@Event(.sync)` property. A sync event dispatches inline on the
|
|
294
280
|
/// JS thread instead of scheduling, so `@ExpoModule`/`@SharedObject` stamp it with `@JavaScriptActor`,
|
|
295
281
|
/// making "must be called on the JS thread" a compile-time guarantee at the call site. Async events
|
|
296
282
|
/// (the default) are deliberately not stamped: their `emit` schedules onto the JS thread itself, so
|
|
@@ -301,7 +287,7 @@ internal func isSyncEventMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
301
287
|
else {
|
|
302
288
|
return false
|
|
303
289
|
}
|
|
304
|
-
return
|
|
290
|
+
return hasOption(attribute, named: "sync")
|
|
305
291
|
}
|
|
306
292
|
|
|
307
293
|
/// Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/// Warns when a class that autolinking exports isn't `public` or `open`. Autolinking generates the modules
|
|
6
|
+
/// provider in the app target, so it can reference only public classes of other modules. The warning has a
|
|
7
|
+
/// fix-it that makes the class `public`. A local class is skipped, because it can't have an access modifier.
|
|
8
|
+
internal func diagnoseNonPublicClass(
|
|
9
|
+
_ classDecl: ClassDeclSyntax,
|
|
10
|
+
macroName: String,
|
|
11
|
+
reason: String,
|
|
12
|
+
idPrefix: String,
|
|
13
|
+
in context: some MacroExpansionContext
|
|
14
|
+
) {
|
|
15
|
+
guard !isLocal(context: context) else {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
let accessModifier = classDecl.modifiers.first { accessLevelKeywords.contains($0.name.tokenKind) }
|
|
19
|
+
if let accessModifier,
|
|
20
|
+
[.keyword(.public), .keyword(.open)].contains(accessModifier.name.tokenKind)
|
|
21
|
+
{
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
let name = classDecl.name.text
|
|
25
|
+
context.diagnose(
|
|
26
|
+
Diagnostic(
|
|
27
|
+
node: accessModifier.map(Syntax.init) ?? Syntax(classDecl.classKeyword),
|
|
28
|
+
message: PublicAccessDiagnosticMessage(
|
|
29
|
+
"\(macroName) class '\(name)' should be public or open, so that the modules provider that autolinking generates in the app target can \(reason)",
|
|
30
|
+
id: "\(idPrefix)-class-not-public"
|
|
31
|
+
),
|
|
32
|
+
fixIts: [makePublicFixIt(for: classDecl, replacing: accessModifier, idPrefix: idPrefix)]
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private let accessLevelKeywords: [TokenKind] = [
|
|
38
|
+
.keyword(.open), .keyword(.public), .keyword(.package), .keyword(.internal), .keyword(.fileprivate),
|
|
39
|
+
.keyword(.private),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/// True when the declaration is inside a function, closure or another scope that isn't a type or an extension.
|
|
43
|
+
private func isLocal(context: some MacroExpansionContext) -> Bool {
|
|
44
|
+
return context.lexicalContext.contains { node in
|
|
45
|
+
return !node.is(ClassDeclSyntax.self) && !node.is(StructDeclSyntax.self) && !node.is(EnumDeclSyntax.self)
|
|
46
|
+
&& !node.is(ActorDeclSyntax.self) && !node.is(ExtensionDeclSyntax.self)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The fix-it for a class that isn't public: replace its access modifier with `public`, or add `public` before
|
|
51
|
+
/// its other modifiers.
|
|
52
|
+
private func makePublicFixIt(
|
|
53
|
+
for classDecl: ClassDeclSyntax,
|
|
54
|
+
replacing accessModifier: DeclModifierSyntax?,
|
|
55
|
+
idPrefix: String
|
|
56
|
+
) -> FixIt {
|
|
57
|
+
let message = PublicAccessFixItMessage("Make the class 'public'", id: "\(idPrefix)-class-insert-public")
|
|
58
|
+
if let accessModifier {
|
|
59
|
+
let newModifier = accessModifier.with(\.name, accessModifier.name.with(\.tokenKind, .keyword(.public)))
|
|
60
|
+
return FixIt(message: message, changes: [.replace(oldNode: Syntax(accessModifier), newNode: Syntax(newModifier))])
|
|
61
|
+
}
|
|
62
|
+
// The new modifier takes over the leading trivia of the first modifier or of `class` (the line break after the
|
|
63
|
+
// attribute), so it lands where the declaration started.
|
|
64
|
+
let newDecl: ClassDeclSyntax
|
|
65
|
+
if let firstModifier = classDecl.modifiers.first {
|
|
66
|
+
let publicModifier = DeclModifierSyntax(
|
|
67
|
+
name: .keyword(.public, leadingTrivia: firstModifier.leadingTrivia, trailingTrivia: .space)
|
|
68
|
+
)
|
|
69
|
+
var modifiers = classDecl.modifiers
|
|
70
|
+
modifiers[modifiers.startIndex] = firstModifier.with(\.leadingTrivia, [])
|
|
71
|
+
modifiers.insert(publicModifier, at: modifiers.startIndex)
|
|
72
|
+
newDecl = classDecl.with(\.modifiers, modifiers)
|
|
73
|
+
} else {
|
|
74
|
+
let classKeyword = classDecl.classKeyword
|
|
75
|
+
let publicModifier = DeclModifierSyntax(
|
|
76
|
+
name: .keyword(.public, leadingTrivia: classKeyword.leadingTrivia, trailingTrivia: .space)
|
|
77
|
+
)
|
|
78
|
+
newDecl =
|
|
79
|
+
classDecl
|
|
80
|
+
.with(\.modifiers, [publicModifier])
|
|
81
|
+
.with(\.classKeyword, classKeyword.with(\.leadingTrivia, []))
|
|
82
|
+
}
|
|
83
|
+
return FixIt(message: message, changes: [.replace(oldNode: Syntax(classDecl), newNode: Syntax(newDecl))])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private struct PublicAccessDiagnosticMessage: DiagnosticMessage {
|
|
87
|
+
let message: String
|
|
88
|
+
let diagnosticID: MessageID
|
|
89
|
+
let severity: DiagnosticSeverity = .warning
|
|
90
|
+
|
|
91
|
+
init(_ message: String, id: String) {
|
|
92
|
+
self.message = message
|
|
93
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private struct PublicAccessFixItMessage: FixItMessage {
|
|
98
|
+
let message: String
|
|
99
|
+
let fixItID: MessageID
|
|
100
|
+
|
|
101
|
+
init(_ message: String, id: String) {
|
|
102
|
+
self.message = message
|
|
103
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
104
|
+
}
|
|
105
|
+
}
|
package/build/types.d.ts
CHANGED
|
@@ -260,7 +260,7 @@ export interface ExportedEvent extends ExportedAccess, ExportedAvailability {
|
|
|
260
260
|
jsName: string;
|
|
261
261
|
/** The payload type, absent for a no-payload `() -> Void` event. */
|
|
262
262
|
payload?: TypeNode;
|
|
263
|
-
/** `@Event(sync
|
|
263
|
+
/** `@Event(.sync)`, dispatching inline on the JS thread instead of scheduling. */
|
|
264
264
|
sync: boolean;
|
|
265
265
|
}
|
|
266
266
|
/** The `@JS init` of a shared object: the constructor JS calls with `new`. */
|
|
@@ -391,10 +391,16 @@ export interface ScanStats {
|
|
|
391
391
|
/** Wall-clock duration of the scan, in milliseconds. */
|
|
392
392
|
durationMs: number;
|
|
393
393
|
}
|
|
394
|
-
/**
|
|
395
|
-
|
|
394
|
+
/**
|
|
395
|
+
* One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, or a `@SharedObject`
|
|
396
|
+
* class in `sharedObjects`.
|
|
397
|
+
*/
|
|
398
|
+
export interface ScannedClass {
|
|
396
399
|
name: string;
|
|
397
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* The resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override, else the class
|
|
402
|
+
* name.
|
|
403
|
+
*/
|
|
398
404
|
jsName: string;
|
|
399
405
|
/**
|
|
400
406
|
* The spelled access level, or `internal` when none is written. The generated provider references
|
|
@@ -403,7 +409,7 @@ export interface ScannedModule {
|
|
|
403
409
|
accessLevel: string;
|
|
404
410
|
/**
|
|
405
411
|
* The OSes whose builds include this class, resolved from the enclosing `#if` conditions. An
|
|
406
|
-
* unconditional
|
|
412
|
+
* unconditional class lists every OS. Empty means no build is known to include it, so don't
|
|
407
413
|
* register it.
|
|
408
414
|
*/
|
|
409
415
|
platforms: ScannedPlatform[];
|
|
@@ -412,7 +418,10 @@ export interface ScannedModule {
|
|
|
412
418
|
/** The `scan-modules` result. */
|
|
413
419
|
export interface ScanModulesResult {
|
|
414
420
|
schemaVersion: number;
|
|
415
|
-
|
|
421
|
+
/** The top-level `@ExpoModule` classes. */
|
|
422
|
+
modules: ScannedClass[];
|
|
423
|
+
/** The top-level `@SharedObject` classes, with the same platform rules as `modules`. */
|
|
424
|
+
sharedObjects: ScannedClass[];
|
|
416
425
|
warnings: ScanWarning[];
|
|
417
426
|
stats: ScanStats;
|
|
418
427
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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.30.0",
|
|
30
|
+
"@expo/modules-macros-linux-x64": "0.30.0",
|
|
31
|
+
"@expo/modules-macros-win32-arm64": "0.30.0",
|
|
32
|
+
"@expo/modules-macros-win32-x64": "0.30.0"
|
|
33
33
|
}
|
|
34
34
|
}
|