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.
@@ -13,26 +13,12 @@ internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
13
13
  return segment.content.text
14
14
  }
15
15
 
16
- /// Reads a labeled boolean-literal argument of an attribute, e.g. `@Event(sync: true)` -> true.
17
- /// Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
18
- internal func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool? {
19
- guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
20
- return nil
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 hasJSOption(attribute, named: "concurrent")
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 `hasJSOption`, it scans
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: true)` property. A sync event dispatches inline on the
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 boolArgument(of: attribute, label: "sync") == true
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: true)`, dispatching inline on the JS thread instead of scheduling. */
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
- /** One module in the `scan-modules` output. */
395
- export interface ScannedModule {
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
- /** The resolved JS name: the `@ExpoModule("Foo")` override, else the class name. */
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 module lists every OS. Empty means no build is known to include it, so don't
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
- modules: ScannedModule[];
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.29.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.29.0",
30
- "@expo/modules-macros-linux-x64": "0.29.0",
31
- "@expo/modules-macros-win32-arm64": "0.29.0",
32
- "@expo/modules-macros-win32-x64": "0.29.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
  }