expo-modules-macros 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/linux.yml +3 -7
- package/.github/workflows/publish.yml +2 -3
- package/.github/workflows/swift.yml +7 -13
- package/.github/workflows/windows.yml +3 -10
- package/AGENTS.md +11 -8
- package/Package.swift +110 -0
- package/README.md +56 -8
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/CLI.swift +47 -10
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +37 -1
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesCLI/Commands}/Expand/MacroExpander.swift +118 -27
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +20 -10
- package/apple/Sources/ExpoModulesCLI/Commands/Fix/RunFix.swift +228 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +1 -1
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +8 -10
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +48 -34
- package/apple/Sources/ExpoModulesMacros/main.swift +6 -0
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/EventMacro.swift +3 -3
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModuleMacro.swift +8 -1
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/JSMacro.swift +54 -1
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/SharedObjectMacro.swift +9 -2
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ViewPropsMacro.swift +36 -6
- package/apple/Sources/ExpoModulesMacrosCore/PluginServer.swift +40 -0
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/ProvidedMacros.swift +5 -5
- package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/MacroHelpers.swift +12 -26
- package/apple/Sources/ExpoModulesMacrosCore/Support/PublicAccess.swift +105 -0
- package/apple/Sources/ExpoModulesMacrosTool/main.swift +27 -0
- package/build/types.d.ts +15 -6
- package/package.json +5 -5
- package/apple/Package.swift +0 -66
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +0 -36
- /package/{apple/Package.resolved → Package.resolved} +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModulesOptimizedMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoViewMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/RecordMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/UnionMacro.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/ClosureArguments.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/DecorateModuleBuilder.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/FreeFormTypes.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSConstructor.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/JSNameCollisions.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/OptimizedFunctionHelpers.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/Receiver.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/SendableConformance.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TupleTypes.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TypeAliases.swift +0 -0
- /package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/TypeConformanceAssertion.swift +0 -0
|
@@ -34,19 +34,19 @@ public enum Scanner {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/// One
|
|
38
|
-
///
|
|
39
|
-
///
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
/// per-member detail instead.
|
|
43
|
-
struct
|
|
44
|
-
/// The Swift class name
|
|
37
|
+
/// One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, or a `@SharedObject`
|
|
38
|
+
/// class in `sharedObjects`. Trimmed to what `expo-modules-autolinking` needs to register the class:
|
|
39
|
+
/// the Swift class name, the JS name it registers under, the platforms that include it, and the file
|
|
40
|
+
/// it's in. The richer fields the visitor captures (declaration kind, raw macro arguments,
|
|
41
|
+
/// line/column) are dropped here, because this command doesn't need them (the macro is always on a
|
|
42
|
+
/// class) and the deep `scan-exports` surface carries the richer per-member detail instead.
|
|
43
|
+
struct ScannedClass: Codable, Equatable {
|
|
44
|
+
/// The Swift class name.
|
|
45
45
|
let name: String
|
|
46
46
|
|
|
47
|
-
/// The fully-resolved JS
|
|
48
|
-
/// class name. Resolved here (rather than left `nil`) so it matches how the
|
|
49
|
-
/// and the consumer never has to apply the fallback itself.
|
|
47
|
+
/// The fully-resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override when
|
|
48
|
+
/// present, otherwise the class name. Resolved here (rather than left `nil`) so it matches how the
|
|
49
|
+
/// macro derives the name and the consumer never has to apply the fallback itself.
|
|
50
50
|
let jsName: String
|
|
51
51
|
|
|
52
52
|
/// The class's spelled access modifier (`open`, `public`, `package`, `fileprivate`, `private`),
|
|
@@ -56,20 +56,21 @@ struct ScannedModule: Codable, Equatable {
|
|
|
56
56
|
let accessLevel: String
|
|
57
57
|
|
|
58
58
|
/// The Apple OSes whose builds include this class, spelled as `os(...)` spells them and compared
|
|
59
|
-
/// case-sensitively. An unconditional
|
|
59
|
+
/// case-sensitively. An unconditional class lists every OS. Empty means no build is known to
|
|
60
60
|
/// include it, because the enclosing conditions depend on flags not asserted with `--define` or
|
|
61
61
|
/// on conditions a static scan cannot answer (those are reported in `warnings`). The consumer
|
|
62
62
|
/// must not assume such a class exists.
|
|
63
63
|
let platforms: [String]
|
|
64
64
|
|
|
65
|
-
/// Source file the
|
|
65
|
+
/// Source file the class was found in, relative to the path the scanner was invoked with.
|
|
66
66
|
let file: String
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/// Version of the `scan-modules` output shape. Bumped on any breaking change to the envelope or to
|
|
70
|
-
/// `
|
|
70
|
+
/// `ScannedClass`, so `expo-modules-autolinking` can verify it understands the output before
|
|
71
71
|
/// trusting it (and fall back to config-declared modules when it doesn't). Version 2 added the
|
|
72
|
-
/// per-module `platforms` list and made the module list platform-agnostic.
|
|
72
|
+
/// per-module `platforms` list and made the module list platform-agnostic. A new field that older
|
|
73
|
+
/// consumers can ignore, such as `sharedObjects`, doesn't change the version.
|
|
73
74
|
let scanModulesSchemaVersion = 2
|
|
74
75
|
|
|
75
76
|
/// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
|
|
@@ -77,7 +78,11 @@ let scanModulesSchemaVersion = 2
|
|
|
77
78
|
/// serve different consumers and don't share an envelope.)
|
|
78
79
|
struct ScanModulesResult: Codable, Equatable {
|
|
79
80
|
let schemaVersion: Int
|
|
80
|
-
let modules: [
|
|
81
|
+
let modules: [ScannedClass]
|
|
82
|
+
|
|
83
|
+
/// The top-level `@SharedObject` classes, in the same order and with the same platform rules as
|
|
84
|
+
/// `modules`.
|
|
85
|
+
let sharedObjects: [ScannedClass]
|
|
81
86
|
|
|
82
87
|
/// Warnings for `#if` conditions the scan couldn't answer statically (see `ScanWarning`). Carried
|
|
83
88
|
/// in the report rather than on stderr so the consumer can attach them to its own output.
|
|
@@ -97,25 +102,28 @@ private let platformUniverse: [(condition: String, reported: String)] = [
|
|
|
97
102
|
(condition: "visionOS", reported: "visionOS"),
|
|
98
103
|
]
|
|
99
104
|
|
|
100
|
-
/// Scans the given paths for top-level `@ExpoModule` types and returns every
|
|
105
|
+
/// Scans the given paths for top-level `@ExpoModule` and `@SharedObject` types and returns every one
|
|
106
|
+
/// found in any
|
|
101
107
|
/// `#if` branch (in file then source order), each with the platforms whose builds include it, plus
|
|
102
108
|
/// the stats for the run — the `scan-modules` command. Kept separate from the public entry (and
|
|
103
109
|
/// `internal`) so tests can drive it without going through argv/stdout.
|
|
104
110
|
///
|
|
105
111
|
/// Each file is parsed once and walked once per platform (plus once unconditionally to enumerate
|
|
106
|
-
/// every
|
|
112
|
+
/// every type): a type's `platforms` are the OSes whose evaluated walk reached it, given the
|
|
107
113
|
/// asserted `defines`. The scanner reports the facts; filtering to the platform being linked is the
|
|
108
114
|
/// consumer's call.
|
|
109
115
|
func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResult {
|
|
110
|
-
var modules: [
|
|
116
|
+
var modules: [ScannedClass] = []
|
|
117
|
+
var sharedObjects: [ScannedClass] = []
|
|
111
118
|
var warnings: [ScanWarning] = []
|
|
112
119
|
var seenWarnings = Set<ScanWarning>()
|
|
113
120
|
|
|
114
|
-
let
|
|
121
|
+
let macros: Set<DetectedMacro> = [.expoModule, .sharedObject]
|
|
122
|
+
let stats = scanFiles(paths: paths, macros: macros) { source, file in
|
|
115
123
|
let tree = Parser.parse(source: source)
|
|
116
124
|
|
|
117
|
-
// The unconditional walk enumerates every
|
|
118
|
-
let allModules = DetectionVisitor(file: file, tree: tree, detectedMacros:
|
|
125
|
+
// The unconditional walk enumerates every type in the file, in source order.
|
|
126
|
+
let allModules = DetectionVisitor(file: file, tree: tree, detectedMacros: macros, configuration: nil)
|
|
119
127
|
allModules.walk(tree)
|
|
120
128
|
|
|
121
129
|
// One evaluated walk per OS attributes each module to the platforms that include it. The walks
|
|
@@ -126,7 +134,7 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
126
134
|
let visitor = DetectionVisitor(
|
|
127
135
|
file: file,
|
|
128
136
|
tree: tree,
|
|
129
|
-
detectedMacros:
|
|
137
|
+
detectedMacros: macros,
|
|
130
138
|
configuration: configuration
|
|
131
139
|
)
|
|
132
140
|
visitor.walk(tree)
|
|
@@ -140,24 +148,30 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// the
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
151
|
+
// The macros reject anything but a class, so a struct with one of them is never registered.
|
|
152
|
+
for detection in allModules.detections where detection.declarationKind == "class" {
|
|
153
|
+
// Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")` or
|
|
154
|
+
// `@SharedObject("Foo")` override, else the class name.
|
|
155
|
+
let scanned = ScannedClass(
|
|
156
|
+
name: detection.name,
|
|
157
|
+
jsName: detection.jsName ?? detection.name,
|
|
158
|
+
accessLevel: detection.accessLevel,
|
|
159
|
+
platforms: platformsByDetection[detectionKey(detection)] ?? [],
|
|
160
|
+
file: detection.file
|
|
154
161
|
)
|
|
162
|
+
switch detection.macro {
|
|
163
|
+
case .sharedObject:
|
|
164
|
+
sharedObjects.append(scanned)
|
|
165
|
+
default:
|
|
166
|
+
modules.append(scanned)
|
|
167
|
+
}
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
return ScanModulesResult(
|
|
159
172
|
schemaVersion: scanModulesSchemaVersion,
|
|
160
173
|
modules: modules,
|
|
174
|
+
sharedObjects: sharedObjects,
|
|
161
175
|
warnings: warnings,
|
|
162
176
|
stats: stats
|
|
163
177
|
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import ExpoModulesMacrosCore
|
|
2
|
+
|
|
3
|
+
// The plugin executable that SwiftPM builds for the packages that depend on the
|
|
4
|
+
// `ExpoModulesMacrosPlugin` product. It only serves the macros, so it doesn't link the CLI; the
|
|
5
|
+
// binary that this package ships is `ExpoModulesMacrosTool`, which does both.
|
|
6
|
+
try runPluginServer()
|
|
@@ -32,7 +32,7 @@ import SwiftSyntaxMacros
|
|
|
32
32
|
/// also self-contained: `@ExpoModule`/`@SharedObject` neither collect `@Event` members nor register
|
|
33
33
|
/// their names anywhere.
|
|
34
34
|
///
|
|
35
|
-
/// `@Event(sync
|
|
35
|
+
/// `@Event(.sync)` opts into **synchronous dispatch**: the closure calls `emitSync` (inline
|
|
36
36
|
/// conversion + dispatch, no scheduling) instead of `emit`, and `@ExpoModule`/`@SharedObject` stamp
|
|
37
37
|
/// the member `@JavaScriptActor` so the compiler forces the call site onto the JS thread, the
|
|
38
38
|
/// inverse of the async default. The isolation is on the property access, so it guards the inline
|
|
@@ -127,7 +127,7 @@ private struct EventMember {
|
|
|
127
127
|
let jsName: String
|
|
128
128
|
/// The payload type as written, or `nil` for a no-payload `() -> Void` event.
|
|
129
129
|
let payloadType: String?
|
|
130
|
-
/// Whether the event dispatches synchronously (`@Event(sync
|
|
130
|
+
/// Whether the event dispatches synchronously (`@Event(.sync)`) via `emitSync`.
|
|
131
131
|
let isSync: Bool
|
|
132
132
|
|
|
133
133
|
var hasPayload: Bool {
|
|
@@ -212,7 +212,7 @@ private func validatedEvent(
|
|
|
212
212
|
swiftName: swiftName,
|
|
213
213
|
jsName: jsNameArgument(of: node) ?? defaultEventName(for: withoutBackticks(swiftName)),
|
|
214
214
|
payloadType: functionType.parameters.first?.type.trimmedDescription,
|
|
215
|
-
isSync:
|
|
215
|
+
isSync: hasOption(node, named: "sync")
|
|
216
216
|
)
|
|
217
217
|
}
|
|
218
218
|
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ExpoModuleMacro.swift
RENAMED
|
@@ -38,6 +38,13 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
38
38
|
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
39
39
|
throw MacroExpansionErrorMessage("@ExpoModule can only be applied to a class")
|
|
40
40
|
}
|
|
41
|
+
diagnoseNonPublicClass(
|
|
42
|
+
classDecl,
|
|
43
|
+
macroName: "@ExpoModule",
|
|
44
|
+
reason: "register it",
|
|
45
|
+
idPrefix: "expomodule",
|
|
46
|
+
in: context
|
|
47
|
+
)
|
|
41
48
|
|
|
42
49
|
// The module name is fully resolved here (the explicit `@ExpoModule("…")` argument, else the
|
|
43
50
|
// class name) and emitted as a non-optional `_jsName` static below — so the
|
|
@@ -160,7 +167,7 @@ extension ExpoModuleMacro: MemberAttributeMacro {
|
|
|
160
167
|
// checked at compile time. Skipped when the member already chose an isolation
|
|
161
168
|
// (`nonisolated` or another global actor), or when `.concurrent` or `.withAsync("name")` runs
|
|
162
169
|
// it on the concurrent pool. See `shouldStampJavaScriptActor`.
|
|
163
|
-
// `@Event(sync
|
|
170
|
+
// `@Event(.sync)` members get the stamp too: a sync event dispatches inline, so the
|
|
164
171
|
// isolation forces its call site onto the JS thread. Async events (the default) are
|
|
165
172
|
// deliberately left unstamped — their `emit` schedules onto the JS thread itself.
|
|
166
173
|
if memberHasJSAttribute(member) || isSyncEventMember(member),
|
|
@@ -36,6 +36,7 @@ public struct JSMacro: PeerMacro {
|
|
|
36
36
|
diagnoseClosureTypes(in: declaration, in: context)
|
|
37
37
|
diagnoseConcurrentOption(of: node, on: declaration, in: context)
|
|
38
38
|
diagnoseAsyncVariantOption(of: node, on: declaration, in: context)
|
|
39
|
+
diagnoseJavaScriptValuesOffThread(of: node, on: declaration, in: context)
|
|
39
40
|
|
|
40
41
|
// In an `@ExpoModule` or `@SharedObject` class, the class macro emits the assertion instead: it sees
|
|
41
42
|
// the type aliases declared in the class body, which this macro can't (see `TupleTypeAliases`).
|
|
@@ -83,7 +84,7 @@ private func diagnoseConcurrentOption(
|
|
|
83
84
|
on declaration: some DeclSyntaxProtocol,
|
|
84
85
|
in context: some MacroExpansionContext
|
|
85
86
|
) {
|
|
86
|
-
guard
|
|
87
|
+
guard hasOption(node, named: "concurrent") else {
|
|
87
88
|
return
|
|
88
89
|
}
|
|
89
90
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
@@ -248,6 +249,58 @@ private func insertAsyncFixIt(for funcDecl: FunctionDeclSyntax) -> FixIt {
|
|
|
248
249
|
/// `consuming`, `sending`) are fine: the binding passes the decoded value under whichever convention
|
|
249
250
|
/// the parameter asks for. The specifier is left off the decode and the assertion (see
|
|
250
251
|
/// `SpecifiedType`); a parameter reported here is also left out of the assertion.
|
|
252
|
+
/// The JSI types that own an engine value and release it wherever they are dropped. Such a value is
|
|
253
|
+
/// valid only on the JavaScript thread. `JavaScriptPromise` releases its value on the JavaScript
|
|
254
|
+
/// thread, and `JavaScriptRef` and `JavaScriptCallback` exist to carry a value across threads, so they
|
|
255
|
+
/// aren't listed.
|
|
256
|
+
private let javaScriptThreadBoundTypes: Set<String> = [
|
|
257
|
+
"JavaScriptValue", "JavaScriptObject", "JavaScriptFunction", "JavaScriptArray", "JavaScriptArrayBuffer",
|
|
258
|
+
"JavaScriptTypedArray", "JavaScriptBigInt", "JavaScriptWeakObject",
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
/// Emits the diagnostic for a JSI value parameter of a `@JS(.concurrent)` function or of the variant
|
|
262
|
+
/// that `@JS(.withAsync("name"))` adds. Both run the body on the concurrent thread pool, never on the
|
|
263
|
+
/// JavaScript thread, so the body can't use the value, and the value is released off the JavaScript
|
|
264
|
+
/// thread too.
|
|
265
|
+
private func diagnoseJavaScriptValuesOffThread(
|
|
266
|
+
of node: AttributeSyntax,
|
|
267
|
+
on declaration: some DeclSyntaxProtocol,
|
|
268
|
+
in context: some MacroExpansionContext
|
|
269
|
+
) {
|
|
270
|
+
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
let option: String
|
|
274
|
+
let reason: String
|
|
275
|
+
if hasOption(node, named: "concurrent") {
|
|
276
|
+
option = ".concurrent"
|
|
277
|
+
reason = "'.concurrent' runs the body on the concurrent thread pool"
|
|
278
|
+
} else if asyncVariantOption(of: node) != nil {
|
|
279
|
+
option = ".withAsync"
|
|
280
|
+
reason = "the variant that '.withAsync' adds runs the function on the concurrent thread pool"
|
|
281
|
+
} else {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
for parameter in funcDecl.signature.parameterClause.parameters {
|
|
285
|
+
let type = SpecifiedType(parameter.type).type
|
|
286
|
+
guard let name = baseIdentifier(of: optionalWrappedType(type) ?? type), javaScriptThreadBoundTypes.contains(name)
|
|
287
|
+
else {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
let parameterName = (parameter.secondName ?? parameter.firstName).text
|
|
291
|
+
context.diagnose(
|
|
292
|
+
Diagnostic(
|
|
293
|
+
node: parameter.type,
|
|
294
|
+
message: JSDiagnosticMessage(
|
|
295
|
+
"'\(parameterName)' can't be a '\(name)' in a '\(option)' function: \(reason), and a JavaScript value is valid only on the JavaScript thread. Take a native type that the value decodes to, such as a @Record or [String: Any], or remove '\(option)'.",
|
|
296
|
+
id: "js-javascript-value-off-thread",
|
|
297
|
+
severity: .error
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
251
304
|
private func diagnoseParameterSpecifiers(
|
|
252
305
|
in declaration: some DeclSyntaxProtocol,
|
|
253
306
|
in context: some MacroExpansionContext
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/SharedObjectMacro.swift
RENAMED
|
@@ -8,7 +8,7 @@ import SwiftSyntaxMacros
|
|
|
8
8
|
/// is emitted as a `_jsName` override.
|
|
9
9
|
///
|
|
10
10
|
/// @SharedObject
|
|
11
|
-
/// final class Cache: SharedObject {
|
|
11
|
+
/// public final class Cache: SharedObject {
|
|
12
12
|
/// @JS
|
|
13
13
|
/// init(name: String) { self.name = name }
|
|
14
14
|
///
|
|
@@ -34,6 +34,13 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
34
34
|
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
35
35
|
throw MacroExpansionErrorMessage("@SharedObject can only be applied to a class")
|
|
36
36
|
}
|
|
37
|
+
diagnoseNonPublicClass(
|
|
38
|
+
classDecl,
|
|
39
|
+
macroName: "@SharedObject",
|
|
40
|
+
reason: "export it",
|
|
41
|
+
idPrefix: "sharedobject",
|
|
42
|
+
in: context
|
|
43
|
+
)
|
|
37
44
|
|
|
38
45
|
// The superclass isn't checked here: the macro sees only this declaration, so it can't tell an
|
|
39
46
|
// indirect subclass (`class Thumbnail: ImageRef`) from an unrelated class. The compiler reports a
|
|
@@ -164,7 +171,7 @@ extension SharedObjectMacro: MemberAttributeMacro {
|
|
|
164
171
|
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
165
172
|
in context: some MacroExpansionContext
|
|
166
173
|
) throws -> [AttributeSyntax] {
|
|
167
|
-
// `@Event(sync
|
|
174
|
+
// `@Event(.sync)` members are stamped alongside `@JS` ones: a sync event dispatches
|
|
168
175
|
// inline, so the isolation forces its call site onto the JS thread. Async events (the
|
|
169
176
|
// default) stay unstamped — their `emit` schedules onto the JS thread itself.
|
|
170
177
|
guard memberHasJSAttribute(member) || isSyncEventMember(member) else {
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Macros/ViewPropsMacro.swift
RENAMED
|
@@ -139,23 +139,37 @@ private func fieldAttributeDiagnostic(
|
|
|
139
139
|
for attribute: AttributeSyntax,
|
|
140
140
|
on varDecl: VariableDeclSyntax
|
|
141
141
|
) -> Diagnostic {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
var
|
|
145
|
-
|
|
142
|
+
// The token after the attribute takes over the trivia of the attribute: the line break,
|
|
143
|
+
// indentation and comments before it, and the comments after it. Its own leading whitespace is
|
|
144
|
+
// left out unless a comment comes before it, so `@Field var size` and `@Field` on its own line both
|
|
145
|
+
// become `var size` on the line of the attribute, and `@Field // note` keeps the line break after
|
|
146
|
+
// the comment.
|
|
147
|
+
var newDecl = varDecl
|
|
148
|
+
if let next = attribute.lastToken(viewMode: .sourceAccurate)?.nextToken(viewMode: .sourceAccurate) {
|
|
149
|
+
let comments = Trivia(pieces: attribute.trailingTrivia.pieces.drop(while: \.isWhitespace))
|
|
150
|
+
let nextTrivia =
|
|
151
|
+
comments.isEmpty ? Trivia(pieces: next.leadingTrivia.pieces.drop(while: \.isWhitespace)) : next.leadingTrivia
|
|
152
|
+
newDecl =
|
|
153
|
+
LeadingTriviaRewriter(token: next.id, trivia: attribute.leadingTrivia + comments + nextTrivia)
|
|
154
|
+
.rewrite(varDecl).cast(VariableDeclSyntax.self)
|
|
155
|
+
}
|
|
156
|
+
// Then remove the attribute with its trivia, so it doesn't leave a blank line behind. The rewritten
|
|
157
|
+
// declaration is a new tree, so the attribute is found by its place in the list.
|
|
158
|
+
if let index = varDecl.attributes.firstIndex(where: { element in
|
|
146
159
|
guard case .attribute(let candidate) = element else {
|
|
147
160
|
return false
|
|
148
161
|
}
|
|
149
162
|
return candidate == attribute
|
|
150
163
|
}) {
|
|
151
|
-
attributes.
|
|
164
|
+
let offset = varDecl.attributes.distance(from: varDecl.attributes.startIndex, to: index)
|
|
165
|
+
newDecl.attributes.remove(at: newDecl.attributes.index(newDecl.attributes.startIndex, offsetBy: offset))
|
|
152
166
|
}
|
|
153
167
|
let fixIt = FixIt(
|
|
154
168
|
message: ViewPropsFixItMessage("Remove the '@Field' attribute", id: "viewprops-remove-field"),
|
|
155
169
|
changes: [
|
|
156
170
|
.replace(
|
|
157
171
|
oldNode: Syntax(varDecl),
|
|
158
|
-
newNode: Syntax(
|
|
172
|
+
newNode: Syntax(newDecl)
|
|
159
173
|
)
|
|
160
174
|
]
|
|
161
175
|
)
|
|
@@ -166,6 +180,22 @@ private func fieldAttributeDiagnostic(
|
|
|
166
180
|
return Diagnostic(node: attribute, message: message, fixIts: [fixIt])
|
|
167
181
|
}
|
|
168
182
|
|
|
183
|
+
/// Replaces the leading trivia of one token.
|
|
184
|
+
private final class LeadingTriviaRewriter: SyntaxRewriter {
|
|
185
|
+
private let token: SyntaxIdentifier
|
|
186
|
+
private let trivia: Trivia
|
|
187
|
+
|
|
188
|
+
init(token: SyntaxIdentifier, trivia: Trivia) {
|
|
189
|
+
self.token = token
|
|
190
|
+
self.trivia = trivia
|
|
191
|
+
super.init(viewMode: .sourceAccurate)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override func visit(_ token: TokenSyntax) -> TokenSyntax {
|
|
195
|
+
token.id == self.token ? token.with(\.leadingTrivia, trivia) : token
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
169
199
|
/// The error for an optional event prop, attached to the declared type and carrying a fix-it that
|
|
170
200
|
/// unwraps it. Events are registered once at view creation from a static name list, so there is no
|
|
171
201
|
/// shape that could express "sometimes emitted".
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@_spi(PluginMessage) import SwiftCompilerPluginMessageHandling
|
|
2
|
+
import SwiftSyntaxMacros
|
|
3
|
+
|
|
4
|
+
/// The module name that `expo-modules-core` passes to `#externalMacro`, and the name of the macro
|
|
5
|
+
/// target that SwiftPM loads as the plugin.
|
|
6
|
+
private let pluginModuleName = "ExpoModulesMacros"
|
|
7
|
+
|
|
8
|
+
/// Runs the compiler plugin server on stdin and stdout until the compiler closes the connection.
|
|
9
|
+
///
|
|
10
|
+
/// `CompilerPlugin.main()` finds a macro by comparing `String(reflecting:)` of each type with
|
|
11
|
+
/// `<module>.<type>` from `#externalMacro`. The macro types live in this module, not in a module
|
|
12
|
+
/// named `ExpoModulesMacros`, so both executables that serve the plugin (the SwiftPM macro target and
|
|
13
|
+
/// the shipped tool) use this provider, which matches the type name under the plugin module name.
|
|
14
|
+
public func runPluginServer() throws {
|
|
15
|
+
let connection = try StandardIOMessageConnection()
|
|
16
|
+
let listener = CompilerPluginMessageListener(connection: connection, provider: ProvidedMacroProvider())
|
|
17
|
+
try listener.main()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private struct ProvidedMacroProvider: PluginProvider {
|
|
21
|
+
let macros: [String: Macro.Type] = Dictionary(
|
|
22
|
+
uniqueKeysWithValues: ProvidedMacro.all.map { (String(describing: $0.type), $0.type) }
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
func resolveMacro(moduleName: String, typeName: String) throws -> Macro.Type {
|
|
26
|
+
guard moduleName == pluginModuleName, let type = macros[typeName] else {
|
|
27
|
+
throw UnknownMacroError(moduleName: moduleName, typeName: typeName)
|
|
28
|
+
}
|
|
29
|
+
return type
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private struct UnknownMacroError: Error, CustomStringConvertible {
|
|
34
|
+
let moduleName: String
|
|
35
|
+
let typeName: String
|
|
36
|
+
|
|
37
|
+
var description: String {
|
|
38
|
+
"macro implementation type '\(moduleName).\(typeName)' could not be found in the Expo Modules macro plugin"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -3,15 +3,15 @@ import SwiftSyntaxMacros
|
|
|
3
3
|
|
|
4
4
|
/// A macro that the plugin provides, with the attribute name and the extension conformances of its
|
|
5
5
|
/// declaration in `expo-modules-core`.
|
|
6
|
-
struct ProvidedMacro {
|
|
7
|
-
let name: String
|
|
8
|
-
let type: Macro.Type
|
|
9
|
-
var conformances: [TypeSyntax] = []
|
|
6
|
+
public struct ProvidedMacro {
|
|
7
|
+
public let name: String
|
|
8
|
+
public let type: Macro.Type
|
|
9
|
+
public var conformances: [TypeSyntax] = []
|
|
10
10
|
|
|
11
11
|
/// Every macro of the plugin. The compiler gets the types from `providingMacros`, and `expand`
|
|
12
12
|
/// matches the attributes by name and passes the same conformances as the compiler, so both read
|
|
13
13
|
/// this list.
|
|
14
|
-
static var all: [ProvidedMacro] {
|
|
14
|
+
public static var all: [ProvidedMacro] {
|
|
15
15
|
[
|
|
16
16
|
ProvidedMacro(name: "OptimizedFunction", type: OptimizedFunctionAttachedMacro.self),
|
|
17
17
|
ProvidedMacro(name: "JS", type: JSMacro.self),
|
package/apple/Sources/{ExpoModulesMacros → ExpoModulesMacrosCore}/Support/MacroHelpers.swift
RENAMED
|
@@ -13,26 +13,12 @@ internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
|
13
13
|
return segment.content.text
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
for arg in args where arg.label?.text == label {
|
|
23
|
-
guard let literal = arg.expression.as(BooleanLiteralExprSyntax.self) else {
|
|
24
|
-
return nil
|
|
25
|
-
}
|
|
26
|
-
return literal.literal.tokenKind == .keyword(.true)
|
|
27
|
-
}
|
|
28
|
-
return nil
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/// True if the attribute lists the given `JSOptions` member, e.g. `@JS(.concurrent)` or
|
|
32
|
-
/// `@JS("name", [.concurrent])` -> true for "concurrent". Options are written as member-access
|
|
33
|
-
/// expressions (`.concurrent`), optionally inside an array literal when more than one is combined, so
|
|
34
|
-
/// both spellings are scanned. The leading string literal, when present, is the JS name and is skipped.
|
|
35
|
-
internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
16
|
+
/// True if the attribute lists the given option member, e.g. `@JS(.concurrent)`, `@Event(.sync)` or
|
|
17
|
+
/// `@Event("name", EventOptions.sync)` -> true for the matching name. Options are written as
|
|
18
|
+
/// member-access expressions, one per variadic argument. Array literals are scanned too, though the
|
|
19
|
+
/// variadic declarations in `expo-modules-core` don't accept them. The leading string literal, when
|
|
20
|
+
/// present, is the JS name and is skipped.
|
|
21
|
+
internal func hasOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
36
22
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
37
23
|
return false
|
|
38
24
|
}
|
|
@@ -49,7 +35,7 @@ internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) ->
|
|
|
49
35
|
return false
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
/// True when the expression is the member access `.<option>` (or `JSOptions.<option>`).
|
|
38
|
+
/// True when the expression is the member access `.<option>` (or `JSOptions.<option>`, `EventOptions.<option>`).
|
|
53
39
|
private func namesOption(_ expression: ExprSyntax, _ option: String) -> Bool {
|
|
54
40
|
guard let member = expression.as(MemberAccessExprSyntax.self) else {
|
|
55
41
|
return false
|
|
@@ -64,7 +50,7 @@ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
64
50
|
guard let attribute = memberAttributes(of: decl).firstAttribute(named: "JS") else {
|
|
65
51
|
return false
|
|
66
52
|
}
|
|
67
|
-
return
|
|
53
|
+
return hasOption(attribute, named: "concurrent")
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
/// The `.withAsync("name")` option of a `@JS` attribute, e.g. `@JS(.withAsync("loadAsync"))` or
|
|
@@ -74,7 +60,7 @@ internal func asyncVariantOption(of attribute: AttributeSyntax) -> FunctionCallE
|
|
|
74
60
|
return asyncVariantOptions(of: attribute).first
|
|
75
61
|
}
|
|
76
62
|
|
|
77
|
-
/// Every `.withAsync(...)` option of a `@JS` attribute, in source order. Like `
|
|
63
|
+
/// Every `.withAsync(...)` option of a `@JS` attribute, in source order. Like `hasOption`, it scans
|
|
78
64
|
/// the arguments and the elements of an array literal.
|
|
79
65
|
internal func asyncVariantOptions(of attribute: AttributeSyntax) -> [FunctionCallExprSyntax] {
|
|
80
66
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
@@ -290,7 +276,7 @@ internal func hasDefaultValue(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
|
290
276
|
return parameter.defaultValue != nil
|
|
291
277
|
}
|
|
292
278
|
|
|
293
|
-
/// True if the declaration is a `@Event(sync
|
|
279
|
+
/// True if the declaration is a `@Event(.sync)` property. A sync event dispatches inline on the
|
|
294
280
|
/// JS thread instead of scheduling, so `@ExpoModule`/`@SharedObject` stamp it with `@JavaScriptActor`,
|
|
295
281
|
/// making "must be called on the JS thread" a compile-time guarantee at the call site. Async events
|
|
296
282
|
/// (the default) are deliberately not stamped: their `emit` schedules onto the JS thread itself, so
|
|
@@ -301,7 +287,7 @@ internal func isSyncEventMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
301
287
|
else {
|
|
302
288
|
return false
|
|
303
289
|
}
|
|
304
|
-
return
|
|
290
|
+
return hasOption(attribute, named: "sync")
|
|
305
291
|
}
|
|
306
292
|
|
|
307
293
|
/// Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
|
|
@@ -524,7 +510,7 @@ internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of value
|
|
|
524
510
|
/// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
|
|
525
511
|
/// backticks in every identifier position, but a name JavaScript sees (a JS member name, an event
|
|
526
512
|
/// name, a record key) is the bare name.
|
|
527
|
-
|
|
513
|
+
public func withoutBackticks(_ name: String) -> String {
|
|
528
514
|
var name = Substring(name)
|
|
529
515
|
while name.first == "`" {
|
|
530
516
|
name.removeFirst()
|
|
@@ -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
|
+
}
|