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.
- package/.github/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +389 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- 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
|
+
});
|
package/build/index.d.ts
ADDED
|
@@ -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>;
|