expo-modules-macros 0.0.1 → 0.15.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 (46) hide show
  1. package/.github/resources/expo-modules-macros.svg +23 -0
  2. package/.github/workflows/publish.yml +122 -0
  3. package/.github/workflows/swift.yml +68 -0
  4. package/AGENTS.md +59 -0
  5. package/CLAUDE.md +1 -0
  6. package/LICENSE +21 -0
  7. package/README.md +119 -0
  8. package/apple/ExpoModulesMacros +0 -0
  9. package/apple/Package.resolved +15 -0
  10. package/apple/Package.swift +61 -0
  11. package/apple/PodTests/StubTest.swift +5 -0
  12. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
  13. package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
  14. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
  15. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
  16. package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
  17. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
  18. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
  19. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
  20. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
  21. package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
  22. package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
  23. package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
  24. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
  25. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
  26. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
  27. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +389 -0
  28. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
  29. package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
  30. package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
  31. package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
  32. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
  33. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
  34. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
  35. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
  36. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
  37. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
  38. package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
  39. package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
  40. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
  41. package/apple/build.js +138 -0
  42. package/build/index.d.ts +51 -0
  43. package/build/index.js +153 -0
  44. package/build/types.d.ts +264 -0
  45. package/build/types.js +15 -0
  46. package/package.json +22 -5
@@ -0,0 +1,284 @@
1
+ import SwiftSyntax
2
+
3
+ /// A boundary type parsed into a structured, JS-oriented tree, so the consumer walks a tagged tree
4
+ /// instead of re-parsing Swift type syntax. The single Swift type parser lives here. Encoded as a
5
+ /// `kind` discriminator plus per-kind fields and a `typeof` on every node, e.g.
6
+ /// `{ "kind": "array", "typeof": "object", "element": { "kind": "primitive", "name": "Int",
7
+ /// "typeof": "number" } }`. An unmodeled spelling becomes `.unknown` (verbatim text, no `typeof`),
8
+ /// never silently dropped.
9
+
10
+ /// The runtime category mirroring JavaScript's `typeof`. A coarse companion to `TypeNode.kind`: every
11
+ /// structured object kind (array, dictionary, promise, ref) reports `object`. `bigint`/`symbol` are
12
+ /// included for completeness; the scanner doesn't produce them.
13
+ enum JSType: String, Encodable {
14
+ case undefined
15
+ case object
16
+ case boolean
17
+ case number
18
+ case bigint
19
+ case string
20
+ case symbol
21
+ case function
22
+ }
23
+
24
+ /// Which of the scanned kinds declares the name a `.ref` points at.
25
+ ///
26
+ /// Resolution happens after the whole scan (`ExportedSurface.resolvingRefs()`), not while parsing a
27
+ /// type: a `TypeNode` is built from one `TypeSyntax` in isolation, long before the scanner knows what
28
+ /// else exists. A ref the scan can't place carries no `refKind` at all rather than a value meaning "none",
29
+ /// matching how the rest of the surface signals "nothing to say" (`returns` for `Void`, `rawType` for
30
+ /// a bare conformance). That is not an error: it covers a platform or built-in convertible (`CGPoint`,
31
+ /// `URL`) and a type from another module, which the consumer resolves against its own catalog.
32
+ enum RefKind: String, Encodable {
33
+ case record
34
+ case sharedObject
35
+ case `enum`
36
+ case union
37
+ }
38
+
39
+ indirect enum TypeNode: Equatable {
40
+ /// `Bool`, `Int`, `Double`, `String`: the types core fast-decodes. `name` is the Swift spelling;
41
+ /// `jsType` is `boolean`/`number`/`string`.
42
+ case primitive(name: String, jsType: JSType)
43
+
44
+ /// `T?` / `T!` / `Optional<T>`.
45
+ case optional(wrapped: TypeNode)
46
+
47
+ /// `[T]` / `Array<T>`.
48
+ case array(element: TypeNode)
49
+
50
+ /// `[K: V]` / `Dictionary<K, V>`.
51
+ case dictionary(key: TypeNode, value: TypeNode)
52
+
53
+ /// `Promise<T>`.
54
+ case promise(value: TypeNode)
55
+
56
+ /// A closure `(A, B) async throws -> R`. `returns` is `nil` for `Void`; `isAsync`/`isThrowing` carry
57
+ /// the effects (encoded `async`/`throws`).
58
+ case function(parameters: [TypeNode], returns: TypeNode?, isAsync: Bool, isThrowing: Bool)
59
+
60
+ /// Any other named type (record, shared object, enum, union, …). `name` is the possibly-qualified
61
+ /// spelling.
62
+ ///
63
+ /// `refKind` says which scanned kind declares that name, filled in after the scan by
64
+ /// `ExportedSurface.resolvingRefs()`; it is `nil` until then, and stays `nil` for a name the scan
65
+ /// never declared (a platform type like `CGPoint`, or one from another module).
66
+ ///
67
+ /// `jsTypeOverride` carries the category a resolved ref actually crosses as, when that isn't
68
+ /// `object`: a raw-value enum reaches JS as its raw value, so `Status: String` is a `string`. Only
69
+ /// resolution can know this, since the category comes from the *declaration's* `rawType`, which a
70
+ /// node parsed at a use site has never seen.
71
+ case ref(name: String, refKind: RefKind? = nil, jsTypeOverride: JSType? = nil)
72
+
73
+ /// A spelling the parser doesn't model (generic parameter, tuple, metatype, …), kept verbatim so
74
+ /// nothing is lost. Has no `typeof`.
75
+ case unknown(text: String)
76
+
77
+ /// The `typeof` category, or `nil` for `.unknown`. An optional reports its *present* value's
78
+ /// category; the absent (`undefined`) case is carried by the `.optional` wrapper itself.
79
+ var jsType: JSType? {
80
+ switch self {
81
+ case .primitive(_, let jsType):
82
+ return jsType
83
+ case .array, .dictionary, .promise:
84
+ return .object
85
+ case .ref(_, _, let jsTypeOverride):
86
+ // An unresolved ref, and every resolved one but an enum, is an object.
87
+ return jsTypeOverride ?? .object
88
+ case .function:
89
+ return .function
90
+ case .optional(let wrapped):
91
+ return wrapped.jsType
92
+ case .unknown:
93
+ return nil
94
+ }
95
+ }
96
+ }
97
+
98
+ extension TypeNode: Encodable {
99
+ private enum CodingKeys: String, CodingKey {
100
+ case refKind
101
+ case kind
102
+ case name
103
+ // The `typeof` category. Spelled `typeof` in JSON (what the consumer reads); the Swift property is
104
+ // `jsType` since it's the cleaner Swift name.
105
+ case jsType = "typeof"
106
+ case wrapped, element, key, value, parameters, returns, text
107
+ // A closure type's effects, matching `ExportedFunction`'s TS-keyword spellings.
108
+ case isAsync = "async"
109
+ case isThrowing = "throws"
110
+ }
111
+
112
+ func encode(to encoder: Encoder) throws {
113
+ var container = encoder.container(keyedBy: CodingKeys.self)
114
+ // Every node carries its `typeof` category (except `.unknown`, whose `jsType` is nil). Encoded up
115
+ // front so it sits beside `kind` on each node.
116
+ try container.encodeIfPresent(jsType, forKey: .jsType)
117
+ switch self {
118
+ case .primitive(let name, _):
119
+ try container.encode("primitive", forKey: .kind)
120
+ try container.encode(name, forKey: .name)
121
+ case .optional(let wrapped):
122
+ try container.encode("optional", forKey: .kind)
123
+ try container.encode(wrapped, forKey: .wrapped)
124
+ case .array(let element):
125
+ try container.encode("array", forKey: .kind)
126
+ try container.encode(element, forKey: .element)
127
+ case .dictionary(let key, let value):
128
+ try container.encode("dictionary", forKey: .kind)
129
+ try container.encode(key, forKey: .key)
130
+ try container.encode(value, forKey: .value)
131
+ case .promise(let value):
132
+ try container.encode("promise", forKey: .kind)
133
+ try container.encode(value, forKey: .value)
134
+ case .function(let parameters, let returns, let isAsync, let isThrowing):
135
+ try container.encode("function", forKey: .kind)
136
+ try container.encode(parameters, forKey: .parameters)
137
+ try container.encodeIfPresent(returns, forKey: .returns)
138
+ try container.encode(isAsync, forKey: .isAsync)
139
+ try container.encode(isThrowing, forKey: .isThrowing)
140
+ case .ref(let name, let refKind, _):
141
+ try container.encode("ref", forKey: .kind)
142
+ try container.encode(name, forKey: .name)
143
+ try container.encodeIfPresent(refKind, forKey: .refKind)
144
+ case .unknown(let text):
145
+ try container.encode("unknown", forKey: .kind)
146
+ try container.encode(text, forKey: .text)
147
+ }
148
+ }
149
+ }
150
+
151
+ /// The primitive Swift types with a dedicated JS mapping (the set core fast-decodes), each paired with
152
+ /// its JS primitive. A bare name here is a `.primitive`; anything else is a `.ref`.
153
+ private let primitiveJSTypes: [String: JSType] = [
154
+ "Bool": .boolean,
155
+ "Int": .number,
156
+ "Double": .number,
157
+ "String": .string,
158
+ ]
159
+
160
+ /// Parses a `TypeSyntax` into a `TypeNode`: the single place Swift type syntax is interpreted.
161
+ func typeNode(from type: TypeSyntax) -> TypeNode {
162
+ // `T?` sugar.
163
+ if let optional = type.as(OptionalTypeSyntax.self) {
164
+ return .optional(wrapped: typeNode(from: optional.wrappedType))
165
+ }
166
+ // `T!`, JS treats it the same as `T?`.
167
+ if let iuo = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
168
+ return .optional(wrapped: typeNode(from: iuo.wrappedType))
169
+ }
170
+ // `[T]` sugar.
171
+ if let arrayType = type.as(ArrayTypeSyntax.self) {
172
+ return .array(element: typeNode(from: arrayType.element))
173
+ }
174
+ // `[K: V]` sugar.
175
+ if let dictionaryType = type.as(DictionaryTypeSyntax.self) {
176
+ return .dictionary(key: typeNode(from: dictionaryType.key), value: typeNode(from: dictionaryType.value))
177
+ }
178
+ // `(A, B) -> R`.
179
+ if let functionType = type.as(FunctionTypeSyntax.self) {
180
+ return functionNode(from: functionType)
181
+ }
182
+ // `@escaping (…) -> …`, `@Sendable …`, etc.: strip the attributes and parse the underlying type.
183
+ if let attributed = type.as(AttributedTypeSyntax.self) {
184
+ return typeNode(from: attributed.baseType)
185
+ }
186
+ // A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
187
+ if let identifier = type.as(IdentifierTypeSyntax.self) {
188
+ return nominalNode(name: identifier.name.text, generics: identifier.genericArgumentClause)
189
+ }
190
+ // A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling.
191
+ if let member = type.as(MemberTypeSyntax.self) {
192
+ return .ref(name: member.trimmedDescription)
193
+ }
194
+ // Tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
195
+ return .unknown(text: type.trimmedDescription)
196
+ }
197
+
198
+ /// A nominal type's node. The standard generic wrappers normalize to their sugared node
199
+ /// (`Optional<T>`/`Array<T>`/`Dictionary<K,V>`/`Promise<T>`); a bare name is a primitive when known,
200
+ /// else a ref; any other generic is a ref under its full spelling.
201
+ private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -> TypeNode {
202
+ guard let generics else {
203
+ if let jsType = primitiveJSTypes[name] {
204
+ return .primitive(name: name, jsType: jsType)
205
+ }
206
+ return .ref(name: name)
207
+ }
208
+
209
+ let arguments = genericArguments(generics)
210
+ switch (name, arguments.count) {
211
+ case ("Optional", 1):
212
+ return .optional(wrapped: arguments[0])
213
+ case ("Array", 1):
214
+ return .array(element: arguments[0])
215
+ case ("Dictionary", 2):
216
+ return .dictionary(key: arguments[0], value: arguments[1])
217
+ case ("Promise", 1):
218
+ return .promise(value: arguments[0])
219
+ default:
220
+ // An unmodeled generic (`Set<Int>`, `Either<A, B>`, …). Keep the whole spelling as a ref so the
221
+ // name and its arguments survive for the generator to interpret or flag.
222
+ return .ref(name: "\(name)<\(arguments.map { $0.spelling }.joined(separator: ", "))>")
223
+ }
224
+ }
225
+
226
+ /// The argument types of a generic clause, each parsed into a node.
227
+ private func genericArguments(_ clause: GenericArgumentClauseSyntax) -> [TypeNode] {
228
+ clause.arguments.compactMap { argument in
229
+ guard case .type(let type) = argument.argument else {
230
+ return nil
231
+ }
232
+ return typeNode(from: type)
233
+ }
234
+ }
235
+
236
+ /// A closure node: each parameter parsed, the result (`Void`/`()` collapsed to `nil`, matching how a
237
+ /// function's own return is reported), and the closure's `async`/`throws` effects.
238
+ private func functionNode(from type: FunctionTypeSyntax) -> TypeNode {
239
+ let parameters = type.parameters.map { typeNode(from: $0.type) }
240
+ let returnType = type.returnClause.type
241
+ let returns = isVoidType(returnType) ? nil : typeNode(from: returnType)
242
+ let effects = type.effectSpecifiers
243
+ return .function(
244
+ parameters: parameters,
245
+ returns: returns,
246
+ isAsync: effects?.asyncSpecifier != nil,
247
+ isThrowing: effects?.throwsClause?.throwsSpecifier != nil
248
+ )
249
+ }
250
+
251
+ /// True when a return clause is absent or written `Void` / `()`, so the surface reports `nil`. Shared
252
+ /// by the function-return read and the closure-type parser.
253
+ func isVoidType(_ type: TypeSyntax?) -> Bool {
254
+ guard let type else {
255
+ return true
256
+ }
257
+ let text = type.trimmedDescription
258
+ return text == "Void" || text == "()"
259
+ }
260
+
261
+ extension TypeNode {
262
+ /// A best-effort source-like spelling, used only to re-compose an unmodeled generic ref's name.
263
+ /// Not a round-trip of arbitrary Swift; the structured node is the source of truth.
264
+ fileprivate var spelling: String {
265
+ switch self {
266
+ case .primitive(let name, _):
267
+ return name
268
+ case .ref(let name, _, _):
269
+ return name
270
+ case .optional(let wrapped):
271
+ return "\(wrapped.spelling)?"
272
+ case .array(let element):
273
+ return "[\(element.spelling)]"
274
+ case .dictionary(let key, let value):
275
+ return "[\(key.spelling): \(value.spelling)]"
276
+ case .promise(let value):
277
+ return "Promise<\(value.spelling)>"
278
+ case .function(let parameters, let returns, _, _):
279
+ return "(\(parameters.map { $0.spelling }.joined(separator: ", "))) -> \(returns?.spelling ?? "Void")"
280
+ case .unknown(let text):
281
+ return text
282
+ }
283
+ }
284
+ }
@@ -0,0 +1,161 @@
1
+ import Foundation
2
+ import SwiftParser
3
+
4
+ /// The scanner's public entry point. Argument parsing, subcommand dispatch, and usage text live in
5
+ /// the CLI target; this just runs a command and writes its JSON report to stdout.
6
+ ///
7
+ /// The detection model (`Detection`, `DetectionVisitor`, …) stays `internal`: tests reach it via
8
+ /// `@testable import`, and the CLI only needs these entries, so nothing else is exposed.
9
+ public enum Scanner {
10
+ /// Runs the `scan-modules` command over `paths`, prints the JSON report to stdout, and returns a
11
+ /// process exit code: `0` on success, `1` if encoding fails. (The deep `scan-exports` command has
12
+ /// its own `runExports` entry returning its own result type.)
13
+ ///
14
+ /// `defines` (the `--define` options) asserts conditional compilation flags; see `scanModules`
15
+ /// for how they and platforms shape each module's `platforms` list.
16
+ public static func runModules(paths: [String], defines: [String] = []) -> Int32 {
17
+ let result = scanModules(paths: paths, defines: Set(defines))
18
+
19
+ do {
20
+ let encoder = JSONEncoder()
21
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
22
+ let data = try encoder.encode(result)
23
+ FileHandle.standardOutput.write(data)
24
+ FileHandle.standardOutput.write(Data("\n".utf8))
25
+ return 0
26
+ } catch {
27
+ FileHandle.standardError.write(Data("error: failed to encode results: \(error)\n".utf8))
28
+ return 1
29
+ }
30
+ }
31
+ }
32
+
33
+ /// One module in the `scan-modules` output. Trimmed to what `expo-modules-autolinking` needs to
34
+ /// register a module: the Swift class name, the JS name it registers under, the platforms that
35
+ /// include it, and the file it's in. The richer fields the visitor captures (declaration kind, raw
36
+ /// macro arguments, line/column) are dropped here — they're redundant for this command (the macro
37
+ /// is always `@ExpoModule` on a class) and the deep `scan-exports` surface carries the richer
38
+ /// per-member detail instead.
39
+ struct ScannedModule: Codable, Equatable {
40
+ /// The Swift class name the module is declared as.
41
+ let name: String
42
+
43
+ /// The fully-resolved JS module name: the `@ExpoModule("Foo")` override when present, otherwise the
44
+ /// class name. Resolved here (rather than left `nil`) so it matches how the macro derives the name
45
+ /// and the consumer never has to apply the fallback itself.
46
+ let jsName: String
47
+
48
+ /// The class's spelled access modifier (`open`, `public`, `package`, `fileprivate`, `private`),
49
+ /// or `internal` when none is written. The generated modules provider references the class from
50
+ /// the app target, which requires `public`/`open`, so the consumer uses this to skip inaccessible
51
+ /// classes with a diagnostic instead of emitting a provider that fails to compile.
52
+ let accessLevel: String
53
+
54
+ /// The Apple OSes whose builds include this class, spelled as `os(...)` spells them and compared
55
+ /// case-sensitively. An unconditional module lists every OS. Empty means no build is known to
56
+ /// include it, because the enclosing conditions depend on flags not asserted with `--define` or
57
+ /// on conditions a static scan cannot answer (those are reported in `warnings`). The consumer
58
+ /// must not assume such a class exists.
59
+ let platforms: [String]
60
+
61
+ /// Source file the module was found in, relative to the path the scanner was invoked with.
62
+ let file: String
63
+ }
64
+
65
+ /// Version of the `scan-modules` output shape. Bumped on any breaking change to the envelope or to
66
+ /// `ScannedModule`, so `expo-modules-autolinking` can verify it understands the output before
67
+ /// trusting it (and fall back to config-declared modules when it doesn't). Version 2 added the
68
+ /// per-module `platforms` list and made the module list platform-agnostic.
69
+ let scanModulesSchemaVersion = 2
70
+
71
+ /// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
72
+ /// command's JSON output. (`scan-exports` returns its own `ScanExportsResult` shape; the two commands
73
+ /// serve different consumers and don't share an envelope.)
74
+ struct ScanModulesResult: Codable, Equatable {
75
+ let schemaVersion: Int
76
+ let modules: [ScannedModule]
77
+
78
+ /// Warnings for `#if` conditions the scan couldn't answer statically (see `ScanWarning`). Carried
79
+ /// in the report rather than on stderr so the consumer can attach them to its own output.
80
+ let warnings: [ScanWarning]
81
+
82
+ let stats: ScanStats
83
+ }
84
+
85
+ /// The Apple OSes a scan attributes modules to. `condition` is what `os(...)` matches; `reported`
86
+ /// is what the JSON carries. They are identical today, but stated separately so the output
87
+ /// contract cannot drift if the matcher's spelling changes.
88
+ private let platformUniverse: [(condition: String, reported: String)] = [
89
+ (condition: "iOS", reported: "iOS"),
90
+ (condition: "macOS", reported: "macOS"),
91
+ (condition: "tvOS", reported: "tvOS"),
92
+ (condition: "watchOS", reported: "watchOS"),
93
+ (condition: "visionOS", reported: "visionOS"),
94
+ ]
95
+
96
+ /// Scans the given paths for top-level `@ExpoModule` types and returns every module found in any
97
+ /// `#if` branch (in file then source order), each with the platforms whose builds include it, plus
98
+ /// the stats for the run — the `scan-modules` command. Kept separate from the public entry (and
99
+ /// `internal`) so tests can drive it without going through argv/stdout.
100
+ ///
101
+ /// Each file is parsed once and walked once per platform (plus once unconditionally to enumerate
102
+ /// every module): a module's `platforms` are the OSes whose evaluated walk reached it, given the
103
+ /// asserted `defines`. The scanner reports the facts; filtering to the platform being linked is the
104
+ /// consumer's call.
105
+ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResult {
106
+ var modules: [ScannedModule] = []
107
+ var warnings: [ScanWarning] = []
108
+ var seenWarnings = Set<ScanWarning>()
109
+
110
+ let stats = scanFiles(paths: paths, macros: [.expoModule]) { source, file in
111
+ let tree = Parser.parse(source: source)
112
+
113
+ // The unconditional walk enumerates every module in the file, in source order.
114
+ let allModules = DetectionVisitor(file: file, tree: tree, detectedMacros: [.expoModule], configuration: nil)
115
+ allModules.walk(tree)
116
+
117
+ // One evaluated walk per OS attributes each module to the platforms that include it. The walks
118
+ // are cheap relative to the parse, which is shared.
119
+ var platformsByDetection: [String: [String]] = [:]
120
+ for platform in platformUniverse {
121
+ let configuration = ScanBuildConfiguration(platform: platform.condition, defines: defines)
122
+ let visitor = DetectionVisitor(file: file, tree: tree, detectedMacros: [.expoModule], configuration: configuration)
123
+ visitor.walk(tree)
124
+ for detection in visitor.detections {
125
+ platformsByDetection[detectionKey(detection), default: []].append(platform.reported)
126
+ }
127
+ // The same unanswerable condition diagnoses identically in every per-platform walk; report
128
+ // it once.
129
+ for warning in visitor.warnings where seenWarnings.insert(warning).inserted {
130
+ warnings.append(warning)
131
+ }
132
+ }
133
+
134
+ for detection in allModules.detections {
135
+ // Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")` override, else
136
+ // the class name.
137
+ modules.append(
138
+ ScannedModule(
139
+ name: detection.name,
140
+ jsName: detection.jsName ?? detection.name,
141
+ accessLevel: detection.accessLevel,
142
+ platforms: platformsByDetection[detectionKey(detection)] ?? [],
143
+ file: detection.file
144
+ )
145
+ )
146
+ }
147
+ }
148
+
149
+ return ScanModulesResult(
150
+ schemaVersion: scanModulesSchemaVersion,
151
+ modules: modules,
152
+ warnings: warnings,
153
+ stats: stats
154
+ )
155
+ }
156
+
157
+ /// Identifies one declaration across the per-platform walks of the same tree: the source position
158
+ /// is unique within a file, and the name guards against any position ambiguity.
159
+ private func detectionKey(_ detection: Detection) -> String {
160
+ return "\(detection.line):\(detection.column):\(detection.name)"
161
+ }
package/apple/build.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require('node:child_process');
4
+ const fs = require('node:fs/promises');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { promisify } = require('node:util');
8
+
9
+ const execFile = promisify(childProcess.execFile);
10
+
11
+ /**
12
+ * Runs a command and resolves when it exits successfully.
13
+ */
14
+ function spawnAsync(command, args, options = {}) {
15
+ return new Promise((resolve, reject) => {
16
+ const child = childProcess.spawn(command, args, { stdio: 'inherit', ...options });
17
+ child.once('error', reject);
18
+ child.once('close', (code, signal) => {
19
+ if (code === 0) {
20
+ resolve();
21
+ } else {
22
+ reject(new Error(`\`${command} ${args.join(' ')}\` exited with ${signal ?? `code ${code}`}`));
23
+ }
24
+ });
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Builds the macro plugin for the given architecture and returns the path to the built binary.
30
+ * SwiftPM always builds macro tools for the host architecture, so the x86_64 slice
31
+ * is built by running the whole toolchain under Rosetta with `arch -${arch}`.
32
+ */
33
+ async function buildForArch(arch) {
34
+ const buildArgs = ['build', '-c', 'release'];
35
+ if (arch === os.machine()) {
36
+ await spawnAsync('swift', buildArgs, { cwd: __dirname });
37
+ } else {
38
+ await spawnAsync('arch', [`-${arch}`, 'swift', ...buildArgs], { cwd: __dirname });
39
+ }
40
+
41
+ const binPath = path.join(__dirname, `.build/${arch}-apple-macosx/release/ExpoModulesMacros-tool`);
42
+ try {
43
+ await fs.access(binPath);
44
+ } catch {
45
+ throw new Error(`Could not find the built ExpoModulesMacros-tool at ${binPath}`);
46
+ }
47
+ return binPath;
48
+ }
49
+
50
+ /**
51
+ * Checks whether the Swift toolchain actually runs for the given architecture by
52
+ * verifying the host target triple it reports, e.g. `Target: x86_64-apple-macosx26.0`
53
+ * under Rosetta. Exit status alone is not enough: if `arch` ever fell back to the
54
+ * native architecture, the probe would pass but the build would produce a wrong slice.
55
+ */
56
+ async function canRunSwiftForArch(arch) {
57
+ try {
58
+ const { stdout } = await execFile('arch', [`-${arch}`, 'swift', '--version']);
59
+ return stdout.includes(`${arch}-apple`);
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Checks whether the toolchain can build for the given architecture.
67
+ * Building x86_64 on Apple Silicon requires Rosetta, so when it is missing,
68
+ * try to install it before giving up.
69
+ */
70
+ async function canBuildForArch(arch) {
71
+ if (arch === os.machine()) {
72
+ return true;
73
+ }
74
+ if (await canRunSwiftForArch(arch)) {
75
+ return true;
76
+ }
77
+ if (arch === 'x86_64' && os.machine() === 'arm64') {
78
+ try {
79
+ console.log('Installing Rosetta to build the x86_64 slice...');
80
+ await spawnAsync('sudo', ['softwareupdate', '--install-rosetta', '--agree-to-license']);
81
+ return await canRunSwiftForArch(arch);
82
+ } catch {
83
+ // No sudo access or the install failed - fall through to the unsupported arch warning.
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * Asserts that the built binary contains a slice for each of the given architectures.
91
+ */
92
+ async function verifyArchs(binaryPath, archs) {
93
+ const { stdout } = await execFile('lipo', ['-archs', binaryPath]);
94
+ const builtArchs = stdout.trim().split(/\s+/);
95
+ for (const arch of archs) {
96
+ if (!builtArchs.includes(arch)) {
97
+ throw new Error(`The built binary at ${binaryPath} is missing the ${arch} slice`);
98
+ }
99
+ }
100
+ }
101
+
102
+ async function main() {
103
+ const outputPath = path.join(__dirname, 'ExpoModulesMacros');
104
+
105
+ const archs = [];
106
+ for (const arch of ['arm64', 'x86_64']) {
107
+ if (await canBuildForArch(arch)) {
108
+ archs.push(arch);
109
+ } else {
110
+ console.warn(`The Swift toolchain cannot run for ${arch} - the built binary will not support ${arch} Macs.`);
111
+ }
112
+ }
113
+ if (archs.length === 0) {
114
+ throw new Error('The Swift toolchain is not available for any supported architecture');
115
+ }
116
+
117
+ // Builds run sequentially as SwiftPM locks the shared .build directory.
118
+ const binPaths = [];
119
+ for (const arch of archs) {
120
+ binPaths.push(await buildForArch(arch));
121
+ }
122
+
123
+ await fs.rm(outputPath, { force: true });
124
+ if (binPaths.length > 1) {
125
+ await spawnAsync('lipo', ['-create', ...binPaths, '-output', outputPath]);
126
+ } else {
127
+ await fs.copyFile(binPaths[0], outputPath);
128
+ }
129
+
130
+ await spawnAsync('strip', [outputPath]);
131
+ await verifyArchs(outputPath, archs);
132
+ await spawnAsync('lipo', ['-info', outputPath]);
133
+ }
134
+
135
+ main().catch((error) => {
136
+ console.error('Build failed:', error.message);
137
+ process.exit(1);
138
+ });
@@ -0,0 +1,51 @@
1
+ import { ScanExportsResult, ScanModulesResult } from './types';
2
+ export * from './types';
3
+ /** Options accepted by `scanModules`, mirroring the CLI's `scan-modules` flags. */
4
+ export interface ScanModulesOptions {
5
+ /** Conditional compilation flags to treat as set, e.g. `['DEBUG']`. */
6
+ defines?: string[];
7
+ /** Overrides the scanner binary path. Defaults to the one shipped with this package. */
8
+ binaryPath?: string;
9
+ }
10
+ /** Options accepted by `scanExports`. The deep scan doesn't evaluate `#if`, so it takes no flags. */
11
+ export interface ScanExportsOptions {
12
+ /** Overrides the scanner binary path. Defaults to the one shipped with this package. */
13
+ binaryPath?: string;
14
+ }
15
+ /**
16
+ * Thrown when the scanner exits non-zero. Exit code 2 is a usage error (bad subcommand or flags) and
17
+ * 1 means the report couldn't be encoded; both put a message on stderr, carried here as `stderr`.
18
+ */
19
+ export declare class ScannerError extends Error {
20
+ readonly exitCode: number | null;
21
+ readonly stderr: string;
22
+ constructor(message: string, exitCode: number | null, stderr: string);
23
+ }
24
+ /** Thrown when the binary's output schema doesn't match what these types were written against. */
25
+ export declare class ScannerSchemaVersionError extends Error {
26
+ readonly command: string;
27
+ readonly found: number;
28
+ readonly expected: number;
29
+ constructor(command: string, found: number, expected: number);
30
+ }
31
+ /**
32
+ * Absolute path to the scanner binary shipped with this package. It doubles as the macro plugin
33
+ * executable, so it lives next to the Swift package rather than in a `bin` directory.
34
+ */
35
+ export declare function getScannerBinaryPath(): string;
36
+ /**
37
+ * Fast scan for top-level `@ExpoModule` types, for autolinking. Each path is a `.swift` file or a
38
+ * directory, scanned recursively.
39
+ *
40
+ * Conditions the scan can't answer statically come back in `warnings` rather than throwing, so the
41
+ * caller can surface them alongside its own diagnostics.
42
+ */
43
+ export declare function scanModules(paths: string[], options?: ScanModulesOptions): Promise<ScanModulesResult>;
44
+ /**
45
+ * Deep scan of the full JS-exported surface, for TypeScript type generation. Each path is a `.swift`
46
+ * file or a directory, scanned recursively.
47
+ *
48
+ * Unlike `scanModules`, this doesn't evaluate `#if` blocks, so it takes no define option:
49
+ * conditional declarations are reported as if their conditions held.
50
+ */
51
+ export declare function scanExports(paths: string[], options?: ScanExportsOptions): Promise<ScanExportsResult>;