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.
Files changed (28) hide show
  1. package/.github/workflows/{swift.yml → macos.yml} +12 -3
  2. package/AGENTS.md +4 -2
  3. package/Package.swift +10 -1
  4. package/README.md +6 -6
  5. package/apple/ExpoModulesMacros +0 -0
  6. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +124 -5
  7. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +2 -1
  8. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ResolveRefs.swift +76 -17
  9. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +15 -8
  10. package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +174 -20
  11. package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +14 -6
  12. package/apple/Sources/ExpoModulesCLI/Core/Detection.swift +6 -0
  13. package/apple/Sources/ExpoModulesCLI/Core/DetectionVisitor.swift +41 -2
  14. package/apple/Sources/ExpoModulesCLI/Core/SourceScan.swift +3 -2
  15. package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +12 -2
  16. package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +12 -0
  17. package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +64 -30
  18. package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +124 -0
  19. package/apple/Sources/ExpoModulesMacrosCore/Macros/RecordMacro.swift +68 -0
  20. package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +79 -1
  21. package/apple/Sources/ExpoModulesMacrosCore/Macros/UnionMacro.swift +101 -5
  22. package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +574 -23
  23. package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
  24. package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +111 -2
  25. package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +28 -2
  26. package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
  27. package/build/types.d.ts +58 -3
  28. 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 JSNameCollisionNote: NoteMessage {
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 noteID = MessageID(domain: "ExpoModulesMacros", id: "js-name-collision-first")
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>`. Used to
115
- /// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
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`, or a `@SharedObject`
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.31.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.31.0",
30
- "@expo/modules-macros-linux-x64": "0.31.0",
31
- "@expo/modules-macros-win32-arm64": "0.31.0",
32
- "@expo/modules-macros-win32-x64": "0.31.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
  }