expo-modules-macros 0.35.0 → 0.36.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/Package.swift +3 -0
- package/README.md +1 -1
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +39 -11
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +8 -6
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +17 -13
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +6 -5
- package/apple/Sources/ExpoModulesCLI/Core/DetectionVisitor.swift +40 -9
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +13 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +45 -0
- package/apple/Sources/ExpoModulesMacrosCore/Support/DecorateModuleBuilder.swift +45 -16
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSMemberKey.swift +125 -0
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +24 -14
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +51 -12
- package/build/types.d.ts +32 -7
- package/build/types.js +1 -1
- package/package.json +5 -5
package/Package.swift
CHANGED
|
@@ -30,6 +30,8 @@ let package = Package(
|
|
|
30
30
|
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
|
|
31
31
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
|
32
32
|
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
|
|
33
|
+
// For the value of a string literal (`representedLiteralValue`).
|
|
34
|
+
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
33
35
|
// For the plugin server, which resolves the macros by type name (see PluginServer.swift).
|
|
34
36
|
.product(name: "_SwiftCompilerPluginMessageHandling", package: "swift-syntax"),
|
|
35
37
|
],
|
|
@@ -47,6 +49,7 @@ let package = Package(
|
|
|
47
49
|
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
|
|
48
50
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
|
49
51
|
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
|
|
52
|
+
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
50
53
|
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
|
51
54
|
],
|
|
52
55
|
path: "apple/Sources/ExpoModulesMacros"
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ This package lists them as optional dependencies, and their `os`, `cpu` and (on
|
|
|
25
25
|
# Macros
|
|
26
26
|
|
|
27
27
|
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none. It adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet (see [Sendable conformance](#sendable-conformance)).
|
|
28
|
-
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. The class macro reads only the class body, so `@JS` on a member of an extension, or of a type that is neither a module nor a shared object, is an error. So is `@JS` on an initializer or a `static` member of a module, since JavaScript doesn't construct a module and gets only its instance. `@JS` on any other declaration, such as a subscript, is an error too. A `@JS` function or initializer can't be generic, since the binding converts each value by its static type. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. A name that JavaScript already gives a meaning on the object is an error too: `__proto__` on any object, `constructor` on a shared object's prototype, and `prototype` on its constructor. The macro also checks that every type crossing the boundary is convertible in the direction it travels. For a top-level declaration, a bad type is reported on the author's own declaration. In an `@ExpoModule` or `@SharedObject` class, the class macro makes the check, since only it sees the type aliases in the class body, so the error is reported on the class and names the member and its argument labels, such as `local function 'move(_:by:returning:)' requires that 'Point' conform to 'JavaScriptEncodable'`. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. Because of that, a `.concurrent` function and a function with `.withAsync` can't take a JavaScript value such as `JavaScriptValue` or `JavaScriptObject`, which is valid only on the JavaScript thread; the macro reports such a parameter. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol, the error points into the generated code, so mark the function `nonisolated`. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one. A synchronous function or an initializer can take the JavaScript runtime as its last parameter, `in runtime: borrowing JavaScriptRuntime`, for example to create a JavaScript value. The binding passes the runtime of the call, and JavaScript doesn't see the parameter. The runtime can be used only on the JavaScript thread, and an `async` function can resume on another thread after an `await`, so the macro reports the parameter on an `async` function or a function with `.withAsync`, and when it isn't the last parameter, isn't labeled `in`, isn't `borrowing` or is optional. The macro finds the parameter by its type as written, so write it as `JavaScriptRuntime`: a typealias or a subclass is decoded as a JavaScript argument instead.
|
|
28
|
+
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** and **`@JS(symbol:_:)`** on a member of a module or shared object. Marks a function, property or initializer for export. The class macro reads only the class body, so `@JS` on a member of an extension, or of a type that is neither a module nor a shared object, is an error. So is `@JS` on an initializer or a `static` member of a module, since JavaScript doesn't construct a module and gets only its instance. `@JS` on any other declaration, such as a subscript, is an error too. A `@JS` function or initializer can't be generic, since the binding converts each value by its static type. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. A name that JavaScript already gives a meaning on the object is an error too: `__proto__` on any object, `constructor` on a shared object's prototype, and `prototype` on its constructor. To bind a function or property under a symbol instead of a name, pass `symbol:`: a case of `JavaScriptSymbol.WellKnown`, such as `@JS(symbol: .iterator)` for `Symbol.iterator`, or a string literal, such as `@JS(symbol: "app.id")` for `Symbol.for("app.id")`. A function bound under a symbol is named after it in brackets, such as `[Symbol.iterator]`, as JavaScript names such a method. A function under `Symbol.hasInstance` or `Symbol.toStringTag` is defined as a data property first, since an assignment can't shadow what the prototype chain holds under these keys: a read-only `Symbol.hasInstance` on `Function.prototype`, and a `Symbol.toStringTag` accessor without a setter on the base `SharedObject` prototype. A symbol never collides with a name, and `symbol:` on an initializer is an error. In the `scan-exports` output, such a member has a `symbol` field instead of a `jsName`. The macro also checks that every type crossing the boundary is convertible in the direction it travels. For a top-level declaration, a bad type is reported on the author's own declaration. In an `@ExpoModule` or `@SharedObject` class, the class macro makes the check, since only it sees the type aliases in the class body, so the error is reported on the class and names the member and its argument labels, such as `local function 'move(_:by:returning:)' requires that 'Point' conform to 'JavaScriptEncodable'`. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. Because of that, a `.concurrent` function and a function with `.withAsync` can't take a JavaScript value such as `JavaScriptValue` or `JavaScriptObject`, which is valid only on the JavaScript thread; the macro reports such a parameter. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol, the error points into the generated code, so mark the function `nonisolated`. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one. A synchronous function or an initializer can take the JavaScript runtime as its last parameter, `in runtime: borrowing JavaScriptRuntime`, for example to create a JavaScript value. The binding passes the runtime of the call, and JavaScript doesn't see the parameter. The runtime can be used only on the JavaScript thread, and an `async` function can resume on another thread after an `await`, so the macro reports the parameter on an `async` function or a function with `.withAsync`, and when it isn't the last parameter, isn't labeled `in`, isn't `borrowing` or is optional. The macro finds the parameter by its type as written, so write it as `JavaScriptRuntime`: a typealias or a subclass is decoded as a JavaScript argument instead.
|
|
29
29
|
- **`@Event(_ name: String? = nil, _ options: EventOptions...)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. `@ExpoModule` and `@SharedObject` warn when two events in the class body have the same JS name. By default the event is scheduled on the JavaScript thread. The `.inline` option runs the listeners before the call returns when it is made on the JavaScript thread, and schedules the event otherwise; it never blocks. The `.blocking` option runs the listeners before the call returns on any thread, so a call from another thread blocks until the JavaScript thread runs them. An event property is callable from any thread with each option.
|
|
30
30
|
- **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A class without an inheritance clause is an error, with a Fix-It that adds `: SharedObject`. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses. A subclass of another `@SharedObject` class also gets the `@JS` members of that class, and its own members replace inherited members with the same JS name. A subclass that isn't `final` can't be constructed from JavaScript through the `@JS init` of its superclass, so give each subclass that JavaScript constructs its own `@JS init`. Like `@ExpoModule`, it adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet.
|
|
31
31
|
- **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. A record converts only from and to JavaScript values: its `from(dictionary:appContext:)` throws `RecordDictionaryConversionException`, so a record can't be created from a native `[String: Any]`. A `@Union` can be a record property. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional. The memberwise initializer sets every field, so a field that is a `let` with a default value is an error. It doesn't set a `private` or `fileprivate` stored property, so such a property needs a default value, unless it's an optional `var` or the type declares its own memberwise initializer.
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -72,8 +72,12 @@ struct ExportedFunction: Encodable, Equatable {
|
|
|
72
72
|
let name: String
|
|
73
73
|
|
|
74
74
|
/// The JS name it binds under: the `@JS("x")` override, else `name`. The second entry of a function
|
|
75
|
-
/// with `.withAsync("x")` binds under `x`.
|
|
76
|
-
let jsName: String
|
|
75
|
+
/// with `.withAsync("x")` binds under `x`. `nil` for a function bound under a symbol (see `symbol`).
|
|
76
|
+
let jsName: String?
|
|
77
|
+
|
|
78
|
+
/// The symbol it binds under with `@JS(symbol: ...)`, or `nil` when it binds under `jsName`.
|
|
79
|
+
/// Exactly one of `jsName` and `symbol` is set.
|
|
80
|
+
var symbol: ExportedSymbol? = nil
|
|
77
81
|
|
|
78
82
|
var parameters: [ExportedParameter]
|
|
79
83
|
|
|
@@ -96,20 +100,41 @@ struct ExportedFunction: Encodable, Equatable {
|
|
|
96
100
|
|
|
97
101
|
/// The flags are encoded under their TS-keyword spellings.
|
|
98
102
|
private enum CodingKeys: String, CodingKey {
|
|
99
|
-
case name, jsName, parameters, returns, accessLevel, platforms
|
|
103
|
+
case name, jsName, symbol, parameters, returns, accessLevel, platforms
|
|
100
104
|
case isAsync = "async"
|
|
101
105
|
case isThrowing = "throws"
|
|
102
106
|
case isStatic = "static"
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/// The symbol that a `@JS(symbol: ...)` member binds under.
|
|
111
|
+
struct ExportedSymbol: Encodable, Hashable {
|
|
112
|
+
enum Kind: String, Encodable, Hashable {
|
|
113
|
+
/// A well-known symbol, such as `Symbol.iterator`, from `@JS(symbol: .iterator)`.
|
|
114
|
+
case wellKnown
|
|
115
|
+
/// The symbol that the global registry holds for a key, `Symbol.for("app.id")`, from
|
|
116
|
+
/// `@JS(symbol: "app.id")`.
|
|
117
|
+
case registered
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let kind: Kind
|
|
121
|
+
|
|
122
|
+
/// The name of the well-known symbol, such as `iterator`, or the registry key.
|
|
123
|
+
let name: String
|
|
124
|
+
}
|
|
125
|
+
|
|
106
126
|
/// One `@JS var` on a module or shared object.
|
|
107
127
|
struct ExportedProperty: Encodable, Equatable {
|
|
108
128
|
/// The Swift declaration name.
|
|
109
129
|
let name: String
|
|
110
130
|
|
|
111
|
-
/// The JS name it binds under: the `@JS("x")` override, else `name`.
|
|
112
|
-
|
|
131
|
+
/// The JS name it binds under: the `@JS("x")` override, else `name`. `nil` for a property bound
|
|
132
|
+
/// under a symbol (see `symbol`).
|
|
133
|
+
let jsName: String?
|
|
134
|
+
|
|
135
|
+
/// The symbol it binds under with `@JS(symbol: ...)`, or `nil` when it binds under `jsName`.
|
|
136
|
+
/// Exactly one of `jsName` and `symbol` is set.
|
|
137
|
+
var symbol: ExportedSymbol? = nil
|
|
113
138
|
|
|
114
139
|
/// The value type, or `nil` when undeterminable syntactically (no annotation and no literal
|
|
115
140
|
/// default), the case the macro binds getter-only.
|
|
@@ -132,7 +157,7 @@ struct ExportedProperty: Encodable, Equatable {
|
|
|
132
157
|
let platforms: [ExportedPlatform]
|
|
133
158
|
|
|
134
159
|
private enum CodingKeys: String, CodingKey {
|
|
135
|
-
case name, jsName, type, defaultValue, accessLevel, platforms
|
|
160
|
+
case name, jsName, symbol, type, defaultValue, accessLevel, platforms
|
|
136
161
|
case isReadonly = "readonly"
|
|
137
162
|
case isStatic = "static"
|
|
138
163
|
}
|
|
@@ -140,7 +165,8 @@ struct ExportedProperty: Encodable, Equatable {
|
|
|
140
165
|
func encode(to encoder: Encoder) throws {
|
|
141
166
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
142
167
|
try container.encode(name, forKey: .name)
|
|
143
|
-
try container.
|
|
168
|
+
try container.encodeIfPresent(jsName, forKey: .jsName)
|
|
169
|
+
try container.encodeIfPresent(symbol, forKey: .symbol)
|
|
144
170
|
try container.encodeIfPresent(type, forKey: .type)
|
|
145
171
|
try container.encode(!isSettable, forKey: .isReadonly)
|
|
146
172
|
try container.encode(isStatic, forKey: .isStatic)
|
|
@@ -468,7 +494,8 @@ struct ExportedView: Encodable, Equatable {
|
|
|
468
494
|
/// The Swift class name. Only a view declared at file scope is reported, as in `scan-modules`.
|
|
469
495
|
let name: String
|
|
470
496
|
|
|
471
|
-
/// The name the view registers under
|
|
497
|
+
/// The name the view registers under: the `@ExpoView<CardProps>("Foo")` override when present,
|
|
498
|
+
/// otherwise the class name.
|
|
472
499
|
let jsName: String
|
|
473
500
|
|
|
474
501
|
/// The props type: the generic argument of `@ExpoView<CardProps>`, as written without backticks, or
|
|
@@ -606,9 +633,10 @@ enum ExportedEventDispatch: String, Encodable, Equatable {
|
|
|
606
633
|
/// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
|
|
607
634
|
/// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
|
|
608
635
|
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes; version 11
|
|
609
|
-
/// replaces the `sync` flag of an event with `dispatch
|
|
610
|
-
///
|
|
611
|
-
|
|
636
|
+
/// replaces the `sync` flag of an event with `dispatch`; version 12 adds the `symbol` of a function or
|
|
637
|
+
/// property bound with `@JS(symbol: ...)`, which then has no `jsName`. A new field that older consumers
|
|
638
|
+
/// can ignore, such as `views`, doesn't change the version.
|
|
639
|
+
let scanExportsSchemaVersion = 12
|
|
612
640
|
|
|
613
641
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
614
642
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
@@ -20,7 +20,7 @@ extension ExportedSurface {
|
|
|
20
20
|
/// `platforms`.
|
|
21
21
|
///
|
|
22
22
|
/// The members merge the way the macro binds them: the inherited ones first, then the class's own. An
|
|
23
|
-
/// own member replaces an inherited function or property with the same JS name, separately for static
|
|
23
|
+
/// own member replaces an inherited function or property with the same JS name or symbol, separately for static
|
|
24
24
|
/// and instance members. Events are kept from both: an event is a stored property that emits under its
|
|
25
25
|
/// JS name, so a subclass can't replace an inherited one, and both are sent. The `@JS init` isn't
|
|
26
26
|
/// inherited: core doesn't construct a subclass through the initializer of its superclass.
|
|
@@ -52,17 +52,17 @@ extension ExportedSurface {
|
|
|
52
52
|
var result = sharedObject
|
|
53
53
|
|
|
54
54
|
let ownMembers = Set(
|
|
55
|
-
sharedObject.functions.map { MemberKey(jsName: $0.jsName, isStatic: $0.isStatic) }
|
|
56
|
-
+ sharedObject.properties.map { MemberKey(jsName: $0.jsName, isStatic: $0.isStatic) }
|
|
55
|
+
sharedObject.functions.map { MemberKey(jsName: $0.jsName, symbol: $0.symbol, isStatic: $0.isStatic) }
|
|
56
|
+
+ sharedObject.properties.map { MemberKey(jsName: $0.jsName, symbol: $0.symbol, isStatic: $0.isStatic) }
|
|
57
57
|
)
|
|
58
58
|
result.functions =
|
|
59
59
|
parents.flatMap(\.functions).filter {
|
|
60
|
-
!ownMembers.contains(MemberKey(jsName: $0.jsName, isStatic: $0.isStatic))
|
|
60
|
+
!ownMembers.contains(MemberKey(jsName: $0.jsName, symbol: $0.symbol, isStatic: $0.isStatic))
|
|
61
61
|
}
|
|
62
62
|
+ sharedObject.functions
|
|
63
63
|
result.properties =
|
|
64
64
|
parents.flatMap(\.properties).filter {
|
|
65
|
-
!ownMembers.contains(MemberKey(jsName: $0.jsName, isStatic: $0.isStatic))
|
|
65
|
+
!ownMembers.contains(MemberKey(jsName: $0.jsName, symbol: $0.symbol, isStatic: $0.isStatic))
|
|
66
66
|
}
|
|
67
67
|
+ sharedObject.properties
|
|
68
68
|
result.events = parents.flatMap(\.events) + sharedObject.events
|
|
@@ -91,7 +91,9 @@ extension ExportedSurface {
|
|
|
91
91
|
|
|
92
92
|
/// A member's slot on the JavaScript side: static members live on the constructor and instance members
|
|
93
93
|
/// on the prototype, so the two don't replace each other.
|
|
94
|
+
/// The key a member binds under: its JS name or its symbol, one of which is set.
|
|
94
95
|
private struct MemberKey: Hashable {
|
|
95
|
-
let jsName: String
|
|
96
|
+
let jsName: String?
|
|
97
|
+
let symbol: ExportedSymbol?
|
|
96
98
|
let isStatic: Bool
|
|
97
99
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import SwiftParser
|
|
1
2
|
import SwiftSyntax
|
|
2
3
|
|
|
3
4
|
/// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
@@ -222,7 +223,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
222
223
|
views.append(
|
|
223
224
|
ExportedView(
|
|
224
225
|
name: qualified,
|
|
225
|
-
jsName: name,
|
|
226
|
+
jsName: stringArgument(of: attribute) ?? name,
|
|
226
227
|
propsType: String(propsType.trimmedDescription.filter { $0 != "`" }),
|
|
227
228
|
propsTypeLookupName: spellingWithoutGenericArguments(of: propsType),
|
|
228
229
|
accessLevel: accessLevel,
|
|
@@ -366,10 +367,11 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
366
367
|
let isAsync = effects?.asyncSpecifier != nil
|
|
367
368
|
let exportedParameters = parameters(of: funcDecl.signature.parameterClause)
|
|
368
369
|
let returns = isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) }
|
|
369
|
-
let makeFunction = { (jsName: String
|
|
370
|
+
let makeFunction = { (jsName: String?, symbol: ExportedSymbol?, isAsync: Bool) in
|
|
370
371
|
ExportedFunction(
|
|
371
372
|
name: funcDecl.name.identifierName,
|
|
372
373
|
jsName: jsName,
|
|
374
|
+
symbol: symbol,
|
|
373
375
|
parameters: exportedParameters,
|
|
374
376
|
returns: returns,
|
|
375
377
|
isAsync: isAsync,
|
|
@@ -379,12 +381,15 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
379
381
|
platforms: platforms
|
|
380
382
|
)
|
|
381
383
|
}
|
|
382
|
-
let
|
|
383
|
-
|
|
384
|
+
let symbol = symbolArgument(of: attribute)
|
|
385
|
+
let jsName = symbol == nil ? stringArgument(of: attribute) ?? funcDecl.name.identifierName : nil
|
|
386
|
+
let function = makeFunction(jsName, symbol, isAsync)
|
|
387
|
+
// The macro binds no variant for an `async` function: it reports the option there instead. The
|
|
388
|
+
// variant binds under its own name, also for a function bound under a symbol.
|
|
384
389
|
guard !isAsync, let variantName = asyncVariantName(of: attribute) else {
|
|
385
390
|
return [function]
|
|
386
391
|
}
|
|
387
|
-
return [function, makeFunction(variantName, true)]
|
|
392
|
+
return [function, makeFunction(variantName, nil, true)]
|
|
388
393
|
}
|
|
389
394
|
|
|
390
395
|
/// Builds the `ExportedProperty` entries for a `@JS var`/`let`. One declaration can introduce several
|
|
@@ -399,6 +404,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
399
404
|
// A `lazy var` is read-only from JS, like a `let`: the macro binds it getter-only.
|
|
400
405
|
let isLazy = varDecl.modifiers.contains { $0.name.tokenKind == .keyword(.lazy) }
|
|
401
406
|
let isStatic = isTypeLevel(varDecl.modifiers)
|
|
407
|
+
let symbol = symbolArgument(of: attribute)
|
|
402
408
|
let override = stringArgument(of: attribute)
|
|
403
409
|
var result: [ExportedProperty] = []
|
|
404
410
|
|
|
@@ -411,7 +417,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
411
417
|
result.append(
|
|
412
418
|
ExportedProperty(
|
|
413
419
|
name: name,
|
|
414
|
-
jsName: override ?? name,
|
|
420
|
+
jsName: symbol == nil ? override ?? name : nil,
|
|
421
|
+
symbol: symbol,
|
|
415
422
|
type: type,
|
|
416
423
|
isSettable: !isLazy && isSettable(binding: binding, isLet: isLet),
|
|
417
424
|
isStatic: isStatic,
|
|
@@ -895,8 +902,8 @@ private func hasEventOption(_ attribute: AttributeSyntax, named option: String)
|
|
|
895
902
|
|
|
896
903
|
/// The name in a `@JS` attribute's `.withAsync("name")` option, in the arguments or inside an array
|
|
897
904
|
/// literal (`@JS("load", [.withAsync("loadAsync")])`), or `nil` when the option is absent or its
|
|
898
|
-
/// argument isn't a
|
|
899
|
-
/// macros.
|
|
905
|
+
/// argument isn't a string literal without interpolation. The name is the value of the literal.
|
|
906
|
+
/// Mirrors `asyncVariantOption` and `asyncVariantName` in the macros.
|
|
900
907
|
private func asyncVariantName(of attribute: AttributeSyntax) -> String? {
|
|
901
908
|
guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
902
909
|
return nil
|
|
@@ -916,14 +923,11 @@ private func asyncVariantName(of attribute: AttributeSyntax) -> String? {
|
|
|
916
923
|
}
|
|
917
924
|
guard call.arguments.count == 1,
|
|
918
925
|
let argument = call.arguments.first,
|
|
919
|
-
argument.label == nil
|
|
920
|
-
let literal = argument.expression.as(StringLiteralExprSyntax.self),
|
|
921
|
-
literal.segments.count == 1,
|
|
922
|
-
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
926
|
+
argument.label == nil
|
|
923
927
|
else {
|
|
924
928
|
return nil
|
|
925
929
|
}
|
|
926
|
-
return
|
|
930
|
+
return argument.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue
|
|
927
931
|
}
|
|
928
932
|
return nil
|
|
929
933
|
}
|
|
@@ -44,9 +44,10 @@ struct ScannedClass: Codable, Equatable {
|
|
|
44
44
|
/// The Swift class name.
|
|
45
45
|
let name: String
|
|
46
46
|
|
|
47
|
-
/// The fully-resolved JS name: the `@ExpoModule("Foo")
|
|
48
|
-
///
|
|
49
|
-
/// macro derives the name and the consumer never has
|
|
47
|
+
/// The fully-resolved JS name: the `@ExpoModule("Foo")`, `@SharedObject("Foo")` or
|
|
48
|
+
/// `@ExpoView<CardProps>("Foo")` override when present, otherwise the class name. Resolved here
|
|
49
|
+
/// (rather than left `nil`) so it matches how the macro derives the name and the consumer never has
|
|
50
|
+
/// to apply the fallback itself.
|
|
50
51
|
let jsName: String
|
|
51
52
|
|
|
52
53
|
/// The class's spelled access modifier (`open`, `public`, `package`, `fileprivate`, `private`),
|
|
@@ -155,8 +156,8 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
155
156
|
|
|
156
157
|
// The macros reject anything but a class, so a struct with one of them is never registered.
|
|
157
158
|
for detection in allModules.detections where detection.declarationKind == "class" {
|
|
158
|
-
// Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")
|
|
159
|
-
// `@SharedObject("Foo")` override, else the class name.
|
|
159
|
+
// Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")`,
|
|
160
|
+
// `@SharedObject("Foo")` or `@ExpoView<CardProps>("Foo")` override, else the class name.
|
|
160
161
|
let scanned = ScannedClass(
|
|
161
162
|
name: detection.name,
|
|
162
163
|
jsName: detection.jsName ?? detection.name,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import SwiftDiagnostics
|
|
2
2
|
import SwiftIfConfig
|
|
3
|
+
import SwiftParser
|
|
3
4
|
import SwiftSyntax
|
|
4
5
|
|
|
5
6
|
/// Walks a parsed source file and records top-level declarations carrying `@ExpoModule`, `@JS`,
|
|
@@ -206,19 +207,49 @@ private func arguments(of attribute: AttributeSyntax) -> [MacroArgument] {
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
/// The
|
|
210
|
-
///
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
/// The spellings of the type of the well-known symbols that a `symbol:` argument can name before the
|
|
211
|
+
/// case, as in the macros.
|
|
212
|
+
private let wellKnownSymbolTypeNames: Set<String> = [
|
|
213
|
+
"JavaScriptSymbol.WellKnown",
|
|
214
|
+
"ExpoModulesJSI.JavaScriptSymbol.WellKnown",
|
|
215
|
+
"ExpoModulesCore.JavaScriptSymbol.WellKnown",
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
/// The `symbol:` argument of `@JS`, the same as the macros read it: a case of
|
|
219
|
+
/// `JavaScriptSymbol.WellKnown` names a well-known symbol (`@JS(symbol: .iterator)`), and a string
|
|
220
|
+
/// literal without interpolation a registry key (`@JS(symbol: "app.id")`), with its escape sequences
|
|
221
|
+
/// decoded. `nil` when the attribute has none, or when the argument has another shape, such as a
|
|
222
|
+
/// constant `Keys.id`, which `@JS` reports.
|
|
223
|
+
func symbolArgument(of attribute: AttributeSyntax) -> ExportedSymbol? {
|
|
224
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
225
|
+
let argument = args.first(where: { $0.label?.text == "symbol" })
|
|
226
|
+
else {
|
|
227
|
+
return nil
|
|
228
|
+
}
|
|
229
|
+
if let member = argument.expression.as(MemberAccessExprSyntax.self) {
|
|
230
|
+
if let base = member.base, !wellKnownSymbolTypeNames.contains(base.trimmedDescription) {
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
return ExportedSymbol(kind: .wellKnown, name: member.declName.baseName.identifierName)
|
|
234
|
+
}
|
|
235
|
+
if let value = argument.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue {
|
|
236
|
+
return ExportedSymbol(kind: .registered, name: value)
|
|
237
|
+
}
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// The value of the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork",
|
|
242
|
+
/// with its escape sequences decoded, so a raw string and an escape read as JavaScript sees the name.
|
|
243
|
+
/// Returns `nil` when there's no argument or it isn't an unlabeled string literal without
|
|
244
|
+
/// interpolation. (Same shape the `jsNameArgument` helper reads inside the macros.) Shared with the
|
|
245
|
+
/// `scan-exports` surface visitor, which reads the same JS-name override off
|
|
246
|
+
/// `@JS`/`@ExpoModule`/`@SharedObject`.
|
|
213
247
|
func stringArgument(of attribute: AttributeSyntax) -> String? {
|
|
214
248
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
215
249
|
let first = args.first,
|
|
216
|
-
first.label == nil
|
|
217
|
-
let str = first.expression.as(StringLiteralExprSyntax.self),
|
|
218
|
-
let segment = str.segments.first?.as(StringSegmentSyntax.self),
|
|
219
|
-
str.segments.count == 1
|
|
250
|
+
first.label == nil
|
|
220
251
|
else {
|
|
221
252
|
return nil
|
|
222
253
|
}
|
|
223
|
-
return
|
|
254
|
+
return first.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue
|
|
224
255
|
}
|
|
@@ -26,6 +26,11 @@ import SwiftSyntaxMacros
|
|
|
26
26
|
/// public typealias Props = CardProps
|
|
27
27
|
/// extension CardView: AnyNativeView {}
|
|
28
28
|
///
|
|
29
|
+
/// The view registers under its class name. Pass `@ExpoView<CardProps>("FancyCard")` to override it,
|
|
30
|
+
/// and the expansion also gets a `_jsName` static that core reads in place of the class name:
|
|
31
|
+
///
|
|
32
|
+
/// public nonisolated static let _jsName = "FancyCard"
|
|
33
|
+
///
|
|
29
34
|
/// There is **no view definition**: no `ViewDefinition`, no `View(…) { }` builder, no `Props(_:)` or
|
|
30
35
|
/// `Events(_:)` element. Core reaches the view through the `AnyNativeView` conformance: it reads the
|
|
31
36
|
/// props type from the `Props` typealias, which satisfies the protocol's associated type, and the
|
|
@@ -83,11 +88,18 @@ public struct ExpoViewMacro: MemberMacro, ExtensionMacro {
|
|
|
83
88
|
)
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
var emitted: [DeclSyntax] = [
|
|
87
92
|
"""
|
|
88
93
|
public typealias Props = \(raw: propsType)
|
|
89
94
|
"""
|
|
90
95
|
]
|
|
96
|
+
|
|
97
|
+
// `AnyNativeView` defaults `_jsName` to the class name, so it's emitted only for an override.
|
|
98
|
+
// `nonisolated`, because the protocol is `@MainActor` and core reads the name off the main actor.
|
|
99
|
+
if let jsName = jsNameArgument(of: node) {
|
|
100
|
+
emitted.append("public nonisolated static let _jsName = \"\(raw: jsName)\"")
|
|
101
|
+
}
|
|
102
|
+
return emitted
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
public static func expansion(
|
|
@@ -25,6 +25,12 @@ import SwiftSyntaxMacros
|
|
|
25
25
|
///
|
|
26
26
|
/// @JS
|
|
27
27
|
/// var status: String { "ok" }
|
|
28
|
+
///
|
|
29
|
+
/// @JS(symbol: .iterator)
|
|
30
|
+
/// func makeIterator() -> JavaScriptObject { ... }
|
|
31
|
+
///
|
|
32
|
+
/// @JS(symbol: "app.id")
|
|
33
|
+
/// var id: String { ... }
|
|
28
34
|
public struct JSMacro: PeerMacro {
|
|
29
35
|
public static func expansion(
|
|
30
36
|
of node: AttributeSyntax,
|
|
@@ -52,6 +58,7 @@ public struct JSMacro: PeerMacro {
|
|
|
52
58
|
diagnoseAsyncVariantOption(of: node, on: declaration, in: context)
|
|
53
59
|
diagnoseJavaScriptValuesOffThread(of: node, on: declaration, in: context)
|
|
54
60
|
diagnoseRuntimeParameter(of: node, on: declaration, in: context)
|
|
61
|
+
diagnoseSymbolArgument(of: node, on: declaration, in: context)
|
|
55
62
|
|
|
56
63
|
// In an `@ExpoModule` or `@SharedObject` class, the class macro emits the assertion instead: it sees
|
|
57
64
|
// the type aliases declared in the class body, which this macro can't (see `TupleTypeAliases`).
|
|
@@ -200,6 +207,44 @@ private func removeAttributeFixIts(_ node: AttributeSyntax, before keyword: Toke
|
|
|
200
207
|
]
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
/// Emits the diagnostics for the `symbol:` argument of `@JS`, which binds the member under a symbol
|
|
211
|
+
/// (see `JSMemberKey`). The argument must be a case of `JavaScriptSymbol.WellKnown` or a string literal
|
|
212
|
+
/// that the macro can read, and an initializer can't take it, since a constructor has no key.
|
|
213
|
+
private func diagnoseSymbolArgument(
|
|
214
|
+
of node: AttributeSyntax,
|
|
215
|
+
on declaration: some DeclSyntaxProtocol,
|
|
216
|
+
in context: some MacroExpansionContext
|
|
217
|
+
) {
|
|
218
|
+
guard let argument = symbolArgument(of: node) else {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if declaration.is(InitializerDeclSyntax.self) {
|
|
222
|
+
context.diagnose(
|
|
223
|
+
Diagnostic(
|
|
224
|
+
node: argument,
|
|
225
|
+
message: JSDiagnosticMessage(
|
|
226
|
+
"'symbol:' applies only to a @JS function or property: JavaScript constructs a shared object with 'new', so an initializer isn't bound under a key.",
|
|
227
|
+
id: "js-symbol-on-initializer",
|
|
228
|
+
severity: .error
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
if symbolKey(of: node) == nil {
|
|
235
|
+
context.diagnose(
|
|
236
|
+
Diagnostic(
|
|
237
|
+
node: argument.expression,
|
|
238
|
+
message: JSDiagnosticMessage(
|
|
239
|
+
"'symbol:' takes a well-known symbol, such as '.iterator', or the registry key of 'Symbol.for' as a string literal, such as \"app.id\". The macro reads the argument from the source, so it can't be a variable, a constant or an interpolated string.",
|
|
240
|
+
id: "js-symbol-argument",
|
|
241
|
+
severity: .error
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
203
248
|
/// Emits the diagnostic for `@JS(.concurrent)` on a member that can't take it. The option maps to
|
|
204
249
|
/// Swift's `@concurrent`, which requires an `async` function: a synchronous member has nowhere to
|
|
205
250
|
/// suspend, and a property or initializer can't be async at all. Diagnosing here points at the
|
|
@@ -21,9 +21,13 @@ import SwiftSyntax
|
|
|
21
21
|
/// A synchronous function can take the JavaScript runtime as its last parameter,
|
|
22
22
|
/// `in runtime: borrowing JavaScriptRuntime`. JavaScript doesn't see it: the binding passes the runtime
|
|
23
23
|
/// of the call.
|
|
24
|
+
///
|
|
25
|
+
/// A function marked `@JS(symbol: ...)` binds under a symbol instead of a string name (see
|
|
26
|
+
/// `JSMemberKey`).
|
|
24
27
|
internal struct JSFunction {
|
|
25
28
|
let swiftName: String
|
|
26
|
-
|
|
29
|
+
/// The key that the function binds under.
|
|
30
|
+
private(set) var key: JSMemberKey
|
|
27
31
|
/// The parameters that take JavaScript arguments, without the runtime parameter.
|
|
28
32
|
let parameters: [FunctionParameterSyntax]
|
|
29
33
|
/// The label of the parameter that takes the JavaScript runtime, or `nil` when the function has none.
|
|
@@ -44,7 +48,7 @@ internal struct JSFunction {
|
|
|
44
48
|
|
|
45
49
|
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
46
50
|
self.swiftName = funcDecl.name.text
|
|
47
|
-
self.
|
|
51
|
+
self.key = JSMemberKey(attribute: attribute, defaultName: withoutBackticks(funcDecl.name.text))
|
|
48
52
|
self.nameToken = funcDecl.name
|
|
49
53
|
let declaredParameters = funcDecl.signature.parameterClause.parameters
|
|
50
54
|
self.parameters = declaredParameters.filter { !isRuntimeParameter($0) }
|
|
@@ -68,7 +72,7 @@ internal struct JSFunction {
|
|
|
68
72
|
return nil
|
|
69
73
|
}
|
|
70
74
|
var variant = self
|
|
71
|
-
variant.
|
|
75
|
+
variant.key = .name(asyncVariantName.name)
|
|
72
76
|
variant.nameToken = asyncVariantName.token
|
|
73
77
|
variant.isAsync = true
|
|
74
78
|
variant.isAsyncVariant = true
|
|
@@ -112,7 +116,7 @@ internal struct JSFunction {
|
|
|
112
116
|
lines.append(
|
|
113
117
|
"""
|
|
114
118
|
guard arguments.count == \(maximum) else {
|
|
115
|
-
throw Exceptions.ArgumentsRangeMismatch((functionName:
|
|
119
|
+
throw Exceptions.ArgumentsRangeMismatch((functionName: \(key.functionNameLiteral), received: arguments.count, required: \(required), maximum: \(maximum)))
|
|
116
120
|
}
|
|
117
121
|
"""
|
|
118
122
|
)
|
|
@@ -120,7 +124,7 @@ internal struct JSFunction {
|
|
|
120
124
|
lines.append(
|
|
121
125
|
"""
|
|
122
126
|
guard arguments.count >= \(required) && arguments.count <= \(maximum) else {
|
|
123
|
-
throw Exceptions.ArgumentsRangeMismatch((functionName:
|
|
127
|
+
throw Exceptions.ArgumentsRangeMismatch((functionName: \(key.functionNameLiteral), received: arguments.count, required: \(required), maximum: \(maximum)))
|
|
124
128
|
}
|
|
125
129
|
"""
|
|
126
130
|
)
|
|
@@ -337,6 +341,21 @@ internal struct JSFunction {
|
|
|
337
341
|
/// the host-function closure is what keeps the native callable alive for as long as JS can invoke
|
|
338
342
|
/// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
|
|
339
343
|
/// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
|
|
344
|
+
///
|
|
345
|
+
/// A function bound under a symbol gets the symbol from the runtime first, so the statement is
|
|
346
|
+
/// marked `try`.
|
|
347
|
+
/// Whether the body of a synchronous binding uses the runtime: to decode an argument, to unwrap a shared
|
|
348
|
+
/// object from `this`, or to pass it to a function that takes the runtime.
|
|
349
|
+
private func bodyUsesRuntime(receiver: Receiver) -> Bool {
|
|
350
|
+
if runtimeLabel != nil || !parameters.isEmpty {
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
if case .sharedObject = receiver {
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
|
|
340
359
|
func decorateStatements(object: String, receiver: Receiver) -> String {
|
|
341
360
|
// Every `@JS` binding hands `this` in as a borrowed `JavaScriptUnownedValue` instead of
|
|
342
361
|
// allocating an owning `JavaScriptValue` and forming its `weak`-runtime reference on every call,
|
|
@@ -350,19 +369,21 @@ internal struct JSFunction {
|
|
|
350
369
|
// returns nothing binds through the `Void` overload (`JavaScriptRuntime.VoidFunctionClosure`)
|
|
351
370
|
// instead, which leaves JS's result as `undefined`. A call through it is faster than through a
|
|
352
371
|
// result-slot closure that writes nothing. The closure declares `-> Void` because a body of several
|
|
353
|
-
// statements without a `return` is otherwise ambiguous between the overloads.
|
|
354
|
-
// the runtime
|
|
355
|
-
//
|
|
372
|
+
// statements without a `return` is otherwise ambiguous between the overloads. The `Void` overload
|
|
373
|
+
// doesn't get the runtime, so it's only for a body that doesn't use it: a function that decodes
|
|
374
|
+
// arguments, unwraps a shared object or takes the runtime binds through the result-slot overload even
|
|
375
|
+
// when it returns nothing. Capturing the decorator's runtime instead would make the runtime own a
|
|
376
|
+
// closure that retains it, so a runtime that owns its engine would never be freed.
|
|
356
377
|
let captures = receiver.captureClause
|
|
357
378
|
let parameters =
|
|
358
379
|
isAsync
|
|
359
380
|
? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
360
|
-
: returnType == nil &&
|
|
381
|
+
: returnType == nil && !bodyUsesRuntime(receiver: receiver)
|
|
361
382
|
? "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) -> Void"
|
|
362
383
|
: resultSlotClosureParameters
|
|
363
384
|
|
|
364
385
|
return """
|
|
365
|
-
\(object).setProperty(
|
|
386
|
+
\(key.tryPrefix)\(object).setProperty(\(key.expression)) { \(captures)\(parameters) in
|
|
366
387
|
\(bodyStatements(receiver: receiver, indent: " "))
|
|
367
388
|
}
|
|
368
389
|
"""
|
|
@@ -389,9 +410,13 @@ internal struct JSFunction {
|
|
|
389
410
|
/// so each shared object instance stores its own value. `defineProperty` leaves a frozen object
|
|
390
411
|
/// unchanged, so there the property stays an accessor. A `this` that isn't an object doesn't fail the read
|
|
391
412
|
/// either: the getter still returns the value.
|
|
413
|
+
///
|
|
414
|
+
/// A property marked `@JS(symbol: ...)` binds under a symbol instead of a string name (see
|
|
415
|
+
/// `JSMemberKey`).
|
|
392
416
|
internal struct JSProperty {
|
|
393
417
|
let swiftName: String
|
|
394
|
-
|
|
418
|
+
/// The key that the property binds under.
|
|
419
|
+
let key: JSMemberKey
|
|
395
420
|
/// The property's value type as written, or `nil` when it couldn't be inferred (no annotation and
|
|
396
421
|
/// no literal default). When `nil` the getter still works (the encode infers from `self.<name>`)
|
|
397
422
|
/// but the setter uses an untyped closure parameter.
|
|
@@ -451,7 +476,8 @@ internal struct JSProperty {
|
|
|
451
476
|
getBody = "result.emplace(\(encode))"
|
|
452
477
|
}
|
|
453
478
|
} else {
|
|
454
|
-
let define =
|
|
479
|
+
let define =
|
|
480
|
+
"\(key.tryPrefix)this.getObject(in: runtime).defineProperty(\(key.expression), value: value, options: [.enumerable])"
|
|
455
481
|
// A module getter ignores `this` otherwise, so it can be anything, and only an object can hold the
|
|
456
482
|
// value. A shared object's unwrap has already checked that `this` is an object.
|
|
457
483
|
let defineStatement =
|
|
@@ -476,7 +502,7 @@ internal struct JSProperty {
|
|
|
476
502
|
)
|
|
477
503
|
}
|
|
478
504
|
|
|
479
|
-
lines.append("\(object).defineProperty(\
|
|
505
|
+
lines.append("\(key.tryPrefix)\(object).defineProperty(\(key.expression), descriptor: \(descriptorName))")
|
|
480
506
|
|
|
481
507
|
return
|
|
482
508
|
lines
|
|
@@ -543,7 +569,10 @@ private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversio
|
|
|
543
569
|
///
|
|
544
570
|
/// `configurableProperties` makes the property accessors configurable. `replacesInherited` first
|
|
545
571
|
/// redefines the name of each function as a writable data property: the object may already hold an
|
|
546
|
-
/// inherited accessor with that name, which an assignment would call instead of replacing.
|
|
572
|
+
/// inherited accessor with that name, which an assignment would call instead of replacing. A function
|
|
573
|
+
/// bound under a symbol that an object up the prototype chain holds as a read-only property or as an
|
|
574
|
+
/// accessor without a setter is redefined first too, since an assignment can't shadow such a property
|
|
575
|
+
/// (see `JSMemberKey.isInheritedReadOnly`).
|
|
547
576
|
private func decorateBody(
|
|
548
577
|
functions: [JSFunction],
|
|
549
578
|
properties: [JSProperty],
|
|
@@ -554,11 +583,11 @@ private func decorateBody(
|
|
|
554
583
|
) -> String {
|
|
555
584
|
let functionBody = functions.map { function in
|
|
556
585
|
let binding = function.decorateStatements(object: object, receiver: receiver)
|
|
557
|
-
guard replacesInherited else {
|
|
586
|
+
guard replacesInherited || function.key.isInheritedReadOnly else {
|
|
558
587
|
return binding
|
|
559
588
|
}
|
|
560
589
|
let redefine =
|
|
561
|
-
" \(object).defineProperty(\
|
|
590
|
+
" \(function.key.tryPrefix)\(object).defineProperty(\(function.key.expression), descriptor: .init(configurable: true, enumerable: true, writable: true))"
|
|
562
591
|
return redefine + "\n" + binding
|
|
563
592
|
}
|
|
564
593
|
let propertyBody = properties.map {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import SwiftParser
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
|
|
4
|
+
/// The JavaScript property key that a `@JS` function or property binds under: a string name, from
|
|
5
|
+
/// `@JS("name")` or the Swift name, or a symbol, from `@JS(symbol: ...)`.
|
|
6
|
+
///
|
|
7
|
+
/// - `@JS(symbol: .iterator)` binds under a well-known symbol, `Symbol.iterator`. The case name is
|
|
8
|
+
/// a case of `JavaScriptSymbol.WellKnown`, which the macro declaration type-checks.
|
|
9
|
+
/// - `@JS(symbol: "app.id")` binds under the symbol that the global registry holds for the key,
|
|
10
|
+
/// `Symbol.for("app.id")`.
|
|
11
|
+
internal enum JSMemberKey: Hashable {
|
|
12
|
+
/// The name in the form of `literalContent(of:)`, which the generated code puts between quotes.
|
|
13
|
+
case name(String)
|
|
14
|
+
/// The name of a case of `JavaScriptSymbol.WellKnown`, such as `iterator`.
|
|
15
|
+
case wellKnownSymbol(String)
|
|
16
|
+
/// The registry key, the value of the string literal with its escape sequences decoded.
|
|
17
|
+
case registeredSymbol(String)
|
|
18
|
+
|
|
19
|
+
/// The key of a `@JS` member: the `symbol:` argument of the attribute when it has one, else the
|
|
20
|
+
/// string name of the attribute, else `defaultName`.
|
|
21
|
+
init(attribute: AttributeSyntax, defaultName: String) {
|
|
22
|
+
self = symbolKey(of: attribute) ?? .name(jsNameArgument(of: attribute) ?? defaultName)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var isSymbol: Bool {
|
|
26
|
+
if case .name = self {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// True for a well-known symbol that an object up the prototype chain of a binding target holds in a
|
|
33
|
+
/// way that an assignment can't shadow:
|
|
34
|
+
/// - `Symbol.hasInstance`, a read-only property of `Function.prototype`, which a shared object
|
|
35
|
+
/// constructor inherits;
|
|
36
|
+
/// - `Symbol.toStringTag`, an accessor without a setter on the prototype of the base `SharedObject`
|
|
37
|
+
/// class, which core defines.
|
|
38
|
+
///
|
|
39
|
+
/// A function under such a key is defined with `defineProperty` before it is bound.
|
|
40
|
+
var isInheritedReadOnly: Bool {
|
|
41
|
+
guard case .wellKnownSymbol(let name) = self else {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
return name == "hasInstance" || name == "toStringTag"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// How diagnostics name the key, as JavaScript code would write it: `name`, `Symbol.iterator` or
|
|
48
|
+
/// `Symbol.for("app.id")`.
|
|
49
|
+
var description: String {
|
|
50
|
+
switch self {
|
|
51
|
+
case .name(let name):
|
|
52
|
+
return name
|
|
53
|
+
case .wellKnownSymbol(let name):
|
|
54
|
+
return "Symbol.\(name)"
|
|
55
|
+
case .registeredSymbol(let key):
|
|
56
|
+
return "Symbol.for(\(swiftStringLiteral(key)))"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// The string literal of the `name` of a function bound under the key, as JavaScript gives it to a
|
|
61
|
+
/// method: the string name, or the description of the symbol in brackets, such as
|
|
62
|
+
/// `[Symbol.iterator]` or `[app.id]`. It names the function in an argument count error.
|
|
63
|
+
var functionNameLiteral: String {
|
|
64
|
+
switch self {
|
|
65
|
+
case .name(let name):
|
|
66
|
+
return "\"\(name)\""
|
|
67
|
+
case .wellKnownSymbol(let name):
|
|
68
|
+
return "\"[Symbol.\(name)]\""
|
|
69
|
+
case .registeredSymbol(let key):
|
|
70
|
+
return swiftStringLiteral("[\(key)]")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// The expression that the generated code passes as the key: a string literal, or a throwing call
|
|
75
|
+
/// that gets the symbol from the runtime in scope (see `tryPrefix`). A well-known symbol is cached
|
|
76
|
+
/// in the runtime after its first use.
|
|
77
|
+
var expression: String {
|
|
78
|
+
switch self {
|
|
79
|
+
case .name(let name):
|
|
80
|
+
return "\"\(name)\""
|
|
81
|
+
case .wellKnownSymbol(let name):
|
|
82
|
+
return "JavaScriptSymbol.wellKnown(runtime, .\(name))"
|
|
83
|
+
case .registeredSymbol(let key):
|
|
84
|
+
return "JavaScriptSymbol.for(runtime, \(swiftStringLiteral(key)))"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// The `try ` that a statement passing `expression` needs, or an empty string for a string key.
|
|
89
|
+
var tryPrefix: String {
|
|
90
|
+
return isSymbol ? "try " : ""
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// The spellings of the type of the well-known symbols that a `symbol:` argument can name before the
|
|
95
|
+
/// case. Any other base, such as a constant `Keys.iterator` of type `String`, isn't a well-known
|
|
96
|
+
/// symbol.
|
|
97
|
+
private let wellKnownSymbolTypeNames: Set<String> = [
|
|
98
|
+
"JavaScriptSymbol.WellKnown",
|
|
99
|
+
"ExpoModulesJSI.JavaScriptSymbol.WellKnown",
|
|
100
|
+
"ExpoModulesCore.JavaScriptSymbol.WellKnown",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
/// The key of a `symbol:` argument, or `nil` when the attribute has none or the argument is neither
|
|
104
|
+
/// a case of `JavaScriptSymbol.WellKnown` (`.iterator`, `JavaScriptSymbol.WellKnown.iterator`) nor a
|
|
105
|
+
/// string literal without interpolation, which `@JS` reports.
|
|
106
|
+
internal func symbolKey(of attribute: AttributeSyntax) -> JSMemberKey? {
|
|
107
|
+
guard let argument = symbolArgument(of: attribute) else {
|
|
108
|
+
return nil
|
|
109
|
+
}
|
|
110
|
+
if let member = argument.expression.as(MemberAccessExprSyntax.self) {
|
|
111
|
+
if let base = member.base, !wellKnownSymbolTypeNames.contains(base.trimmedDescription) {
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
return .wellKnownSymbol(withoutBackticks(member.declName.baseName.text))
|
|
115
|
+
}
|
|
116
|
+
if let value = argument.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue {
|
|
117
|
+
return .registeredSymbol(value)
|
|
118
|
+
}
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// The `symbol:` argument of an attribute, or `nil` when it has none.
|
|
123
|
+
internal func symbolArgument(of attribute: AttributeSyntax) -> LabeledExprSyntax? {
|
|
124
|
+
return attribute.arguments?.as(LabeledExprListSyntax.self)?.first { $0.label?.text == "symbol" }
|
|
125
|
+
}
|
|
@@ -2,8 +2,8 @@ import SwiftDiagnostics
|
|
|
2
2
|
import SwiftSyntax
|
|
3
3
|
import SwiftSyntaxMacros
|
|
4
4
|
|
|
5
|
-
/// Reports each `@JS` member whose JavaScript name an earlier `@JS` member already binds on the
|
|
6
|
-
/// JavaScript object. Pass the members of one object only: a shared object's static members bind onto
|
|
5
|
+
/// Reports each `@JS` member whose JavaScript name or symbol an earlier `@JS` member already binds on the
|
|
6
|
+
/// same JavaScript object. A symbol never collides with a string name. Pass the members of one object only: a shared object's static members bind onto
|
|
7
7
|
/// the constructor and its instance members onto the prototype, so the two sets are checked separately.
|
|
8
8
|
internal func diagnoseJSNameCollisions(
|
|
9
9
|
functions: [JSFunction],
|
|
@@ -14,11 +14,11 @@ internal func diagnoseJSNameCollisions(
|
|
|
14
14
|
// name, JavaScript would reach only one. Members are compared in source order, so the error lands on
|
|
15
15
|
// every member after the first, whichever kind each one is.
|
|
16
16
|
let members = functions.map(BoundMember.init(function:)) + properties.map(BoundMember.init(property:))
|
|
17
|
-
var firstMembers: [
|
|
17
|
+
var firstMembers: [JSMemberKey: BoundMember] = [:]
|
|
18
18
|
|
|
19
19
|
for member in members.sorted(by: { $0.nameToken.position < $1.nameToken.position }) {
|
|
20
|
-
guard let first = firstMembers[member.
|
|
21
|
-
firstMembers[member.
|
|
20
|
+
guard let first = firstMembers[member.key] else {
|
|
21
|
+
firstMembers[member.key] = member
|
|
22
22
|
continue
|
|
23
23
|
}
|
|
24
24
|
let renaming =
|
|
@@ -27,12 +27,12 @@ internal func diagnoseJSNameCollisions(
|
|
|
27
27
|
Diagnostic(
|
|
28
28
|
node: member.nameToken,
|
|
29
29
|
message: JSNameCollisionMessage(
|
|
30
|
-
"The JavaScript
|
|
30
|
+
"The JavaScript \(member.keyKind) '\(member.key.description)' is already bound by an earlier \(first.kind). A JavaScript object holds one member per \(member.keyKind), so JavaScript could reach only one of them. Give one of them another \(member.keyKind) with \(renaming)."
|
|
31
31
|
),
|
|
32
32
|
notes: [
|
|
33
33
|
Note(
|
|
34
34
|
node: Syntax(first.nameToken),
|
|
35
|
-
message: JSNameCollisionNote("'\(member.
|
|
35
|
+
message: JSNameCollisionNote("'\(member.key.description)' is first bound here")
|
|
36
36
|
)
|
|
37
37
|
]
|
|
38
38
|
)
|
|
@@ -80,14 +80,15 @@ internal func diagnoseReservedJSNames(
|
|
|
80
80
|
) {
|
|
81
81
|
let members = functions.map(BoundMember.init(function:)) + properties.map(BoundMember.init(property:))
|
|
82
82
|
for member in members {
|
|
83
|
-
|
|
83
|
+
// A symbol key never means anything else on the object.
|
|
84
|
+
guard case .name(let jsName) = member.key, let reason = target.reservedNames[jsName] else {
|
|
84
85
|
continue
|
|
85
86
|
}
|
|
86
87
|
context.diagnose(
|
|
87
88
|
Diagnostic(
|
|
88
89
|
node: member.nameToken,
|
|
89
90
|
message: ReservedJSNameMessage(
|
|
90
|
-
"The JavaScript name '\(
|
|
91
|
+
"The JavaScript name '\(jsName)' can't be bound: \(reason). Give the \(member.kind) another name with \(member.renaming)."
|
|
91
92
|
)
|
|
92
93
|
)
|
|
93
94
|
)
|
|
@@ -129,7 +130,7 @@ internal func diagnoseEventNameCollisions(in memberBlock: MemberBlockSyntax, in
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
private struct BoundMember {
|
|
132
|
-
let
|
|
133
|
+
let key: JSMemberKey
|
|
133
134
|
/// How the message names the member, such as `@JS function`.
|
|
134
135
|
let kind: String
|
|
135
136
|
/// The spelling that gives the member another JavaScript name.
|
|
@@ -138,18 +139,27 @@ private struct BoundMember {
|
|
|
138
139
|
|
|
139
140
|
init(function: JSFunction) {
|
|
140
141
|
// A `.withAsync` variant takes its JavaScript name from the option, not from `@JS("...")`.
|
|
141
|
-
self.
|
|
142
|
+
self.key = function.key
|
|
142
143
|
self.kind = function.isAsyncVariant ? "'.withAsync' variant" : "@JS function"
|
|
143
|
-
self.renaming = function.isAsyncVariant ? ".withAsync(\"...\")" :
|
|
144
|
+
self.renaming = function.isAsyncVariant ? ".withAsync(\"...\")" : Self.renaming(of: function.key)
|
|
144
145
|
self.nameToken = function.nameToken
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
init(property: JSProperty) {
|
|
148
|
-
self.
|
|
149
|
+
self.key = property.key
|
|
149
150
|
self.kind = "@JS property"
|
|
150
|
-
self.renaming =
|
|
151
|
+
self.renaming = Self.renaming(of: property.key)
|
|
151
152
|
self.nameToken = property.nameToken
|
|
152
153
|
}
|
|
154
|
+
|
|
155
|
+
/// How the message names the key: `name` or `symbol`.
|
|
156
|
+
var keyKind: String {
|
|
157
|
+
return key.isSymbol ? "symbol" : "name"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static func renaming(of key: JSMemberKey) -> String {
|
|
161
|
+
return key.isSymbol ? "@JS(symbol: ...)" : "@JS(\"...\")"
|
|
162
|
+
}
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
private struct JSNameCollisionMessage: DiagnosticMessage {
|
|
@@ -1,28 +1,68 @@
|
|
|
1
|
+
import SwiftParser
|
|
1
2
|
import SwiftSyntax
|
|
2
3
|
|
|
3
4
|
/// Reads the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork".
|
|
4
|
-
/// Returns nil if the attribute has no arguments or the first argument is not
|
|
5
|
+
/// Returns nil if the attribute has no arguments or the first argument is not an unlabeled string
|
|
6
|
+
/// literal without interpolation, such as the `symbol:` argument of `@JS(symbol: "app.id")`. The
|
|
7
|
+
/// name is in the form of `literalContent(of:)`.
|
|
5
8
|
internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
6
9
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
7
10
|
let first = args.first,
|
|
8
|
-
|
|
9
|
-
let
|
|
11
|
+
first.label == nil,
|
|
12
|
+
let literal = first.expression.as(StringLiteralExprSyntax.self)
|
|
10
13
|
else {
|
|
11
14
|
return nil
|
|
12
15
|
}
|
|
13
|
-
return
|
|
16
|
+
return literalContent(of: literal)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// The value of a string literal as the content of a plain Swift string literal: the value with its
|
|
20
|
+
/// escape sequences decoded, and escaped again by `swiftStringLiteral(_:)` without the quotes. So
|
|
21
|
+
/// the generated code can put it between quotes whatever the literal is: a raw string (`#"a"b"#`)
|
|
22
|
+
/// reads as `a\"b`, and literals with the same value (`"id"`, `"\u{69}d"`) read the same. `nil` for
|
|
23
|
+
/// a literal with interpolation.
|
|
24
|
+
internal func literalContent(of literal: StringLiteralExprSyntax) -> String? {
|
|
25
|
+
guard let value = literal.representedLiteralValue else {
|
|
26
|
+
return nil
|
|
27
|
+
}
|
|
28
|
+
return String(swiftStringLiteral(value).dropFirst().dropLast())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// A Swift string literal whose value is `value`. A quote, a backslash and the control characters are
|
|
32
|
+
/// escaped, so any value gives a valid literal.
|
|
33
|
+
internal func swiftStringLiteral(_ value: String) -> String {
|
|
34
|
+
var literal = "\""
|
|
35
|
+
for scalar in value.unicodeScalars {
|
|
36
|
+
switch scalar {
|
|
37
|
+
case "\"":
|
|
38
|
+
literal += "\\\""
|
|
39
|
+
case "\\":
|
|
40
|
+
literal += "\\\\"
|
|
41
|
+
case "\n":
|
|
42
|
+
literal += "\\n"
|
|
43
|
+
case "\r":
|
|
44
|
+
literal += "\\r"
|
|
45
|
+
case "\t":
|
|
46
|
+
literal += "\\t"
|
|
47
|
+
case _ where scalar.value < 0x20 || scalar.value == 0x7F:
|
|
48
|
+
literal += "\\u{\(String(scalar.value, radix: 16))}"
|
|
49
|
+
default:
|
|
50
|
+
literal.unicodeScalars.append(scalar)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return literal + "\""
|
|
14
54
|
}
|
|
15
55
|
|
|
16
56
|
/// True if the attribute lists the given option member, e.g. `@JS(.concurrent)`, `@Event(.inline)` or
|
|
17
57
|
/// `@Event("name", EventOptions.inline)` -> true for the matching name. Options are written as
|
|
18
58
|
/// member-access expressions, one per variadic argument. Array literals are scanned too, though the
|
|
19
59
|
/// variadic declarations in `expo-modules-core` don't accept them. The leading string literal, when
|
|
20
|
-
/// present, is the JS name and is skipped
|
|
60
|
+
/// present, is the JS name and is skipped, and so is the `symbol:` argument of `@JS`.
|
|
21
61
|
internal func hasOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
22
62
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
23
63
|
return false
|
|
24
64
|
}
|
|
25
|
-
for arg in args {
|
|
65
|
+
for arg in args where arg.label == nil {
|
|
26
66
|
if namesOption(arg.expression, option) {
|
|
27
67
|
return true
|
|
28
68
|
}
|
|
@@ -88,18 +128,18 @@ private func asyncOptionCall(_ expression: ExprSyntax) -> FunctionCallExprSyntax
|
|
|
88
128
|
|
|
89
129
|
/// The JavaScript name a `.withAsync("name")` option gives the promise-returning variant, with the
|
|
90
130
|
/// token of its string literal, where a name collision is reported. `nil` when the argument isn't a
|
|
91
|
-
///
|
|
131
|
+
/// string literal without interpolation, which `@JS` reports. The name is in the form of
|
|
132
|
+
/// `literalContent(of:)`.
|
|
92
133
|
internal func asyncVariantName(of option: FunctionCallExprSyntax) -> (name: String, token: TokenSyntax)? {
|
|
93
134
|
guard option.arguments.count == 1,
|
|
94
135
|
let argument = option.arguments.first,
|
|
95
136
|
argument.label == nil,
|
|
96
137
|
let literal = argument.expression.as(StringLiteralExprSyntax.self),
|
|
97
|
-
|
|
98
|
-
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
138
|
+
let name = literalContent(of: literal)
|
|
99
139
|
else {
|
|
100
140
|
return nil
|
|
101
141
|
}
|
|
102
|
-
return (
|
|
142
|
+
return (name, literal.segments.first?.as(StringSegmentSyntax.self)?.content ?? literal.openingQuote)
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
/// True if the `@JS`-marked declaration lists `.withAsync("name")`. Such a function is left
|
|
@@ -563,7 +603,6 @@ internal func collectProperties(
|
|
|
563
603
|
varDecl: VariableDeclSyntax,
|
|
564
604
|
attribute: AttributeSyntax
|
|
565
605
|
) -> [JSProperty] {
|
|
566
|
-
let jsNameOverride = jsNameArgument(of: attribute)
|
|
567
606
|
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
568
607
|
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
569
608
|
let isLazy = isLazy(varDecl.modifiers)
|
|
@@ -580,7 +619,7 @@ internal func collectProperties(
|
|
|
580
619
|
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
581
620
|
return JSProperty(
|
|
582
621
|
swiftName: swiftName,
|
|
583
|
-
|
|
622
|
+
key: JSMemberKey(attribute: attribute, defaultName: withoutBackticks(swiftName)),
|
|
584
623
|
valueType: valueType,
|
|
585
624
|
// A literal-inferred type never mentions `Any`, so only an explicit annotation can be free-form.
|
|
586
625
|
freeForm: binding.typeAnnotation.flatMap { freeFormConversion(of: $0.type) },
|
package/build/types.d.ts
CHANGED
|
@@ -177,6 +177,17 @@ export interface ExportedParameter {
|
|
|
177
177
|
/** The default value as Swift source text (`1`, `"x"`, `.fast`), absent when none is written. */
|
|
178
178
|
defaultValue?: string;
|
|
179
179
|
}
|
|
180
|
+
/** The symbol that a `@JS(symbol: ...)` member binds under. */
|
|
181
|
+
export interface ExportedSymbol {
|
|
182
|
+
/**
|
|
183
|
+
* `wellKnown` for a well-known symbol such as `Symbol.iterator`, from `@JS(symbol: .iterator)`;
|
|
184
|
+
* `registered` for the symbol that the global registry holds for a key, `Symbol.for("app.id")`, from
|
|
185
|
+
* `@JS(symbol: "app.id")`.
|
|
186
|
+
*/
|
|
187
|
+
kind: 'wellKnown' | 'registered';
|
|
188
|
+
/** The name of the well-known symbol, such as `iterator`, or the registry key. */
|
|
189
|
+
name: string;
|
|
190
|
+
}
|
|
180
191
|
/** One `@JS func` on a module or shared object. */
|
|
181
192
|
export interface ExportedFunction extends ExportedAccess, ExportedAvailability {
|
|
182
193
|
/**
|
|
@@ -185,9 +196,14 @@ export interface ExportedFunction extends ExportedAccess, ExportedAvailability {
|
|
|
185
196
|
name: string;
|
|
186
197
|
/**
|
|
187
198
|
* The JS name it binds under: the `@JS("x")` override, else `name`. The second entry of a function
|
|
188
|
-
* with `.withAsync("x")` binds under `x`.
|
|
199
|
+
* with `.withAsync("x")` binds under `x`. Absent for a function bound under a symbol (see `symbol`).
|
|
189
200
|
*/
|
|
190
|
-
jsName
|
|
201
|
+
jsName?: string;
|
|
202
|
+
/**
|
|
203
|
+
* The symbol it binds under with `@JS(symbol: ...)`, absent when it binds under `jsName`. Exactly one
|
|
204
|
+
* of `jsName` and `symbol` is present.
|
|
205
|
+
*/
|
|
206
|
+
symbol?: ExportedSymbol;
|
|
191
207
|
parameters: ExportedParameter[];
|
|
192
208
|
/** Absent for a `Void` return. */
|
|
193
209
|
returns?: TypeNode;
|
|
@@ -198,7 +214,16 @@ export interface ExportedFunction extends ExportedAccess, ExportedAvailability {
|
|
|
198
214
|
/** One `@JS var` on a module or shared object. */
|
|
199
215
|
export interface ExportedProperty extends ExportedAccess, ExportedAvailability {
|
|
200
216
|
name: string;
|
|
201
|
-
|
|
217
|
+
/**
|
|
218
|
+
* The JS name it binds under: the `@JS("x")` override, else `name`. Absent for a property bound
|
|
219
|
+
* under a symbol (see `symbol`).
|
|
220
|
+
*/
|
|
221
|
+
jsName?: string;
|
|
222
|
+
/**
|
|
223
|
+
* The symbol it binds under with `@JS(symbol: ...)`, absent when it binds under `jsName`. Exactly one
|
|
224
|
+
* of `jsName` and `symbol` is present.
|
|
225
|
+
*/
|
|
226
|
+
symbol?: ExportedSymbol;
|
|
202
227
|
/** Absent when the type isn't determinable syntactically; the macro binds it getter-only. */
|
|
203
228
|
type?: TypeNode;
|
|
204
229
|
readonly: boolean;
|
|
@@ -401,7 +426,7 @@ export interface ExportedViewEvent extends ExportedAccess, ExportedAvailability
|
|
|
401
426
|
export interface ExportedView extends ExportedAccess, ExportedAvailability {
|
|
402
427
|
/** The Swift class name. Only a view declared at file scope is reported. */
|
|
403
428
|
name: string;
|
|
404
|
-
/** The name the view registers under
|
|
429
|
+
/** The name the view registers under: the `@ExpoView<CardProps>("Foo")` override, else the class name. */
|
|
405
430
|
jsName: string;
|
|
406
431
|
/**
|
|
407
432
|
* The props type: the generic argument of `@ExpoView<CardProps>`, as written without backticks
|
|
@@ -451,8 +476,8 @@ export interface ScanStats {
|
|
|
451
476
|
export interface ScannedClass {
|
|
452
477
|
name: string;
|
|
453
478
|
/**
|
|
454
|
-
* The resolved JS name: the `@ExpoModule("Foo")
|
|
455
|
-
*
|
|
479
|
+
* The resolved JS name: the `@ExpoModule("Foo")`, `@SharedObject("Foo")` or
|
|
480
|
+
* `@ExpoView<CardProps>("Foo")` override, else the class name.
|
|
456
481
|
*/
|
|
457
482
|
jsName: string;
|
|
458
483
|
/**
|
|
@@ -535,5 +560,5 @@ export interface ExpandResult {
|
|
|
535
560
|
* as a clear error instead of silently misread fields.
|
|
536
561
|
*/
|
|
537
562
|
export declare const SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
|
|
538
|
-
export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION =
|
|
563
|
+
export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 12;
|
|
539
564
|
export declare const SUPPORTED_EXPAND_SCHEMA_VERSION = 1;
|
package/build/types.js
CHANGED
|
@@ -12,5 +12,5 @@ exports.SUPPORTED_EXPAND_SCHEMA_VERSION = exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_
|
|
|
12
12
|
* as a clear error instead of silently misread fields.
|
|
13
13
|
*/
|
|
14
14
|
exports.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
|
|
15
|
-
exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION =
|
|
15
|
+
exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 12;
|
|
16
16
|
exports.SUPPORTED_EXPAND_SCHEMA_VERSION = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Swift macro plugin for Expo modules",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typescript": "^5.4.0"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"@expo/modules-macros-linux-arm64": "0.
|
|
30
|
-
"@expo/modules-macros-linux-x64": "0.
|
|
31
|
-
"@expo/modules-macros-win32-arm64": "0.
|
|
32
|
-
"@expo/modules-macros-win32-x64": "0.
|
|
29
|
+
"@expo/modules-macros-linux-arm64": "0.36.0",
|
|
30
|
+
"@expo/modules-macros-linux-x64": "0.36.0",
|
|
31
|
+
"@expo/modules-macros-win32-arm64": "0.36.0",
|
|
32
|
+
"@expo/modules-macros-win32-x64": "0.36.0"
|
|
33
33
|
}
|
|
34
34
|
}
|