expo-modules-macros 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/{swift.yml → macos.yml} +12 -3
- package/AGENTS.md +4 -2
- package/Package.swift +10 -1
- package/README.md +6 -6
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +124 -5
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +2 -1
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ResolveRefs.swift +76 -17
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +15 -8
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +174 -20
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +14 -6
- package/apple/Sources/ExpoModulesCLI/Core/Detection.swift +6 -0
- package/apple/Sources/ExpoModulesCLI/Core/DetectionVisitor.swift +41 -2
- package/apple/Sources/ExpoModulesCLI/Core/SourceScan.swift +3 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +12 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +12 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +64 -30
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +124 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/RecordMacro.swift +68 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +79 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/UnionMacro.swift +101 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +574 -23
- package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +111 -2
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +28 -2
- package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
- package/build/types.d.ts +58 -3
- package/package.json +5 -5
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: macOS
|
|
2
|
+
|
|
3
|
+
# Builds and tests the package on macOS, and checks the universal release tool there.
|
|
2
4
|
|
|
3
5
|
on:
|
|
4
6
|
push:
|
|
@@ -12,8 +14,8 @@ concurrency:
|
|
|
12
14
|
cancel-in-progress: true
|
|
13
15
|
|
|
14
16
|
jobs:
|
|
15
|
-
|
|
16
|
-
name: Build & test
|
|
17
|
+
macos:
|
|
18
|
+
name: Build & test (macOS)
|
|
17
19
|
# The package declares swift-tools-version 6.2, which requires Xcode 26+.
|
|
18
20
|
runs-on: macos-26
|
|
19
21
|
steps:
|
|
@@ -63,3 +65,10 @@ jobs:
|
|
|
63
65
|
run: |
|
|
64
66
|
pnpm install
|
|
65
67
|
pnpm run typecheck
|
|
68
|
+
|
|
69
|
+
- name: Scan through the TypeScript wrapper
|
|
70
|
+
# The wrapper must find the release tool in `apple/`, and `scanExports` must report the
|
|
71
|
+
# surface in `scripts/check-scan-exports.json`, which the Windows and Linux workflows also check.
|
|
72
|
+
run: |
|
|
73
|
+
pnpm run build:ts
|
|
74
|
+
node scripts/check-scan.js .
|
package/AGENTS.md
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
## Layout
|
|
6
6
|
|
|
7
|
-
- `Package.swift`: the SwiftPM package, at the root so that its identity is `expo-modules-macros` (an `apple/` manifest would have the identity `apple`, which conflicts with other packages that keep theirs in `apple/`). The targets name their paths under `apple/`. The `ExpoModulesMacrosPlugin` library product vends the macro target to Swift packages, and its name must differ from the target's. The test targets are declared only when `apple/Tests` exists, because the published npm package leaves it out.
|
|
7
|
+
- `Package.swift`: the SwiftPM package, at the root so that its identity is `expo-modules-macros` (an `apple/` manifest would have the identity `apple`, which conflicts with other packages that keep theirs in `apple/`). The targets name their paths under `apple/`. The `ExpoModulesMacrosPlugin` library product vends the macro target to Swift packages through a target of the same name, and its name must differ from the macro target's. The test targets are declared only when `apple/Tests` exists, because the published npm package leaves it out.
|
|
8
8
|
- `apple/Sources/ExpoModulesMacrosCore`: the macro implementations, in a library target so that both executables below can link them. `Macros/` holds one file per macro, and `Support/` the code that several macros share. `ProvidedMacros.swift` lists every macro with its attribute name and conformances in `ProvidedMacro.all`. `PluginServer.swift` runs the compiler plugin protocol with a provider that resolves the macros of `ProvidedMacro.all` by type name, because the default `CompilerPlugin` lookup expects them in a module named `ExpoModulesMacros`. It uses the `@_spi(PluginMessage)` API of swift-syntax.
|
|
9
9
|
- `apple/Sources/ExpoModulesMacros`: the `.macro` target that the `ExpoModulesMacrosPlugin` product vends to Swift packages. It only runs the plugin server, so Swift packages don't compile the CLI.
|
|
10
|
+
- `apple/Sources/ExpoModulesMacrosPlugin`: an empty target that the `ExpoModulesMacrosPlugin` product holds in place of the macro target, which it depends on. Xcode fails an iOS build when the product holds the macro target directly (see `Package.swift`).
|
|
10
11
|
- `apple/Sources/ExpoModulesMacrosTool`: the executable that ships as `apple/ExpoModulesMacros` and in the Windows and Linux packages. `main.swift` runs the plugin server without arguments and sends any invocation with arguments to the CLI.
|
|
11
12
|
- `apple/Sources/ExpoModulesCLI`: the library behind the command-line tool. `CLI.swift` parses the arguments, `Commands/` holds one directory per subcommand (`ScanModules/` for autolinking, `ScanExports/` for type generation, `Expand/` for `expand`, with `MacroExpander.swift`, which runs the macros in process, and `Fix/` for `fix`, which applies the Fix-Its that `expand` reports), and `Core/` holds the shared parsing, `#if` evaluation and output.
|
|
12
13
|
- `apple/Sources/ExpoModulesOptimized`: declarations for the `@OptimizedFunction` macro.
|
|
@@ -31,7 +32,7 @@ pnpm run swift:format # format the Swift sources in place
|
|
|
31
32
|
pnpm run swift:lint # check the formatting, as CI does
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
CI runs on macOS (`.github/workflows/
|
|
35
|
+
CI runs on macOS (`.github/workflows/macos.yml`), on Windows x64 and arm64 (`.github/workflows/windows.yml`), and on Linux x64 and arm64 (`.github/workflows/linux.yml`). On macOS it runs the release build, checks both binary slices, runs `swift test` and `pnpm run typecheck`, then scans through the TypeScript wrapper with `scripts/check-scan.js`. On Windows it builds and tests the package, builds the release binary, checks that it starts without the Swift runtime, packs it into its `platforms/` package (checking the files and the `os`/`cpu` fields), installs that package, and scans through the TypeScript wrapper with `scripts/check-scan.js`, which must find the executable there. On Linux it does the same in the `swift:6.4.0-rhel-ubi9` image, and also checks that no Swift library is linked dynamically, the newest glibc symbol version that the binary needs, and that the package keeps the executable bit. On every platform, `check-scan.js` also requires `scanExports` to report the surface in `scripts/check-scan-exports.json` for its fixture, so the generated types are the same on each OS. The Swift Format workflow (`.github/workflows/swift-format.yml`) builds the pinned swift-format version and runs `pnpm run swift:lint`. Code in `Sources` and `Tests` must not assume Apple platforms or POSIX paths.
|
|
35
36
|
|
|
36
37
|
## Windows
|
|
37
38
|
|
|
@@ -57,6 +58,7 @@ CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.gi
|
|
|
57
58
|
## Changing macros
|
|
58
59
|
|
|
59
60
|
- The scanner re-implements the macros' naming and member rules on the syntax tree; it does not call the macro code. When a macro changes which members it binds or how it names them (the `@JS` name override, the `on` prefix `@Event` strips, which properties `@Record` treats as fields), change `scan-exports` to match, or generated TypeScript types drift from the runtime.
|
|
61
|
+
- When the `scan-exports` output changes on purpose, update `scripts/check-scan-exports.json`: run `swift build`, then `node scripts/check-scan.js . --update .build/debug/ExpoModulesMacrosTool`, and review the diff. CI compares it with the output on macOS, Windows and Linux.
|
|
60
62
|
- The scanner only parses files that pass the pre-filter, `MacroPrefilter` in `ExpoModulesCLI/Core/SourceScan.swift`. Each command builds it from the `macros` and `conformances` arguments of its `scanFiles` call. A new attribute or conformance the scanner must detect has to be added to those arguments (an attribute also needs a `DetectedMacro` case), or files that use it are skipped without an error.
|
|
61
63
|
- A new macro needs: the implementation in `apple/Sources/ExpoModulesMacrosCore/Macros`, an entry in `ProvidedMacro.all` in `ProvidedMacros.swift` (with the attribute name and the extension conformances of its declaration), an expansion test file, the `#externalMacro` declaration in `expo-modules-core`, and an entry in the macro list in `README.md`.
|
|
62
64
|
|
package/Package.swift
CHANGED
|
@@ -14,7 +14,7 @@ let package = Package(
|
|
|
14
14
|
// `#externalMacro(module: "ExpoModulesMacros", ...)` depends on this product, and the targets that
|
|
15
15
|
// depend on it get the plugin too. CocoaPods builds keep loading the prebuilt binary. The name
|
|
16
16
|
// differs from the macro target's, because SwiftPM already has a product of that name for it.
|
|
17
|
-
.library(name: "ExpoModulesMacrosPlugin", targets: ["
|
|
17
|
+
.library(name: "ExpoModulesMacrosPlugin", targets: ["ExpoModulesMacrosPlugin"]),
|
|
18
18
|
.executable(name: "ExpoModulesMacrosTool", targets: ["ExpoModulesMacrosTool"]),
|
|
19
19
|
],
|
|
20
20
|
dependencies: [
|
|
@@ -51,6 +51,15 @@ let package = Package(
|
|
|
51
51
|
],
|
|
52
52
|
path: "apple/Sources/ExpoModulesMacros"
|
|
53
53
|
),
|
|
54
|
+
// The target of the `ExpoModulesMacrosPlugin` product. The product must not hold the macro target
|
|
55
|
+
// itself: Xcode then passes the module map of `ExpoModulesMacrosCore`, which it builds for the
|
|
56
|
+
// host only, to the iOS target that depends on the product, and the build fails with
|
|
57
|
+
// `ExpoModulesMacrosCore.modulemap not found`.
|
|
58
|
+
.target(
|
|
59
|
+
name: "ExpoModulesMacrosPlugin",
|
|
60
|
+
dependencies: ["ExpoModulesMacros"],
|
|
61
|
+
path: "apple/Sources/ExpoModulesMacrosPlugin"
|
|
62
|
+
),
|
|
54
63
|
// The shipped binary, `apple/ExpoModulesMacros` and the Windows and Linux binaries. It is both the
|
|
55
64
|
// compiler plugin and the scanner CLI: the compiler launches it without arguments and speaks the
|
|
56
65
|
// plugin protocol over stdin, while an invocation with arguments dispatches into `ScannerCLI`
|
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ 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. `@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. The macro also checks that every type crossing the boundary is convertible in the direction it travels.
|
|
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. 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
|
-
- **`@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 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
|
-
- **`@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.
|
|
32
|
-
- **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first.
|
|
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.
|
|
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
|
+
- **`@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
|
+
- **`@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.
|
|
32
|
+
- **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first. The macro warns about a case that an earlier case always decodes first: any case after a `JavaScriptValue` case, and a case with a payload that decodes only numbers, such as `Float` or `Int32`, after a `Double`, `Float` or `CGFloat` case.
|
|
33
33
|
|
|
34
34
|
The members and conformances that `@Record` and `@Union` generate are `nonisolated`, so they also work in a module that builds with a default isolation, such as `-default-isolation MainActor`. The stored properties of a record still take that isolation. A class record, or a struct record with a property whose type isn't `Sendable`, then needs `nonisolated` on its declaration, for example `@Record nonisolated final class Options`.
|
|
35
35
|
|
|
@@ -180,7 +180,7 @@ to `OTHER_SWIFT_FLAGS` for `ExpoModulesCore`, every pod that depends on it, and
|
|
|
180
180
|
|
|
181
181
|
## Swift packages
|
|
182
182
|
|
|
183
|
-
The package manifest is `Package.swift` at the root of this package, so a Swift package can depend on it by its path in `node_modules` or by the URL of this repository. Its identity is then `expo-modules-macros`. The `ExpoModulesMacrosPlugin` library product
|
|
183
|
+
The package manifest is `Package.swift` at the root of this package, so a Swift package can depend on it by its path in `node_modules` or by the URL of this repository. Its identity is then `expo-modules-macros`. The `ExpoModulesMacrosPlugin` library product gives the `ExpoModulesMacros` macro target to the targets that depend on it:
|
|
184
184
|
|
|
185
185
|
```swift
|
|
186
186
|
dependencies: [
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/// The deep-scan surface: the full JS-exported shape of every `@ExpoModule`, `@SharedObject`,
|
|
2
|
-
/// `@Record` type, with the per-member detail a
|
|
3
|
-
/// (like the macros): each boundary type becomes a
|
|
4
|
-
/// tagged tree rather than re-parsing Swift type
|
|
1
|
+
/// The deep-scan surface: the full JS-exported shape of every `@ExpoModule`, `@SharedObject`,
|
|
2
|
+
/// `@Record`, `@Union`, and `@ExpoView` type and every `Enumerable` enum, with the per-member detail a
|
|
3
|
+
/// TypeScript type generator needs. Read syntactically (like the macros): each boundary type becomes a
|
|
4
|
+
/// structured `TypeNode` tree so the consumer walks a tagged tree rather than re-parsing Swift type
|
|
5
|
+
/// syntax.
|
|
5
6
|
///
|
|
6
7
|
/// Every declaration and member carries the `platforms` that build it, and the ones that can be
|
|
7
8
|
/// written with an access modifier carry the `accessLevel` they spell. A declaration no platform
|
|
@@ -460,6 +461,122 @@ struct ExportedUnion: Encodable, Equatable {
|
|
|
460
461
|
let file: String
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
/// An `@ExpoView` class with the props and events of the `@ViewProps` struct in its generic argument.
|
|
465
|
+
/// It is reported on its own rather than under a module, because core reaches it through its
|
|
466
|
+
/// `AnyNativeView` conformance.
|
|
467
|
+
struct ExportedView: Encodable, Equatable {
|
|
468
|
+
/// The Swift class name. Only a view declared at file scope is reported, as in `scan-modules`.
|
|
469
|
+
let name: String
|
|
470
|
+
|
|
471
|
+
/// The name the view registers under. `@ExpoView` takes no name, so it is always the class name.
|
|
472
|
+
let jsName: String
|
|
473
|
+
|
|
474
|
+
/// The props type: the generic argument of `@ExpoView<CardProps>`, as written without backticks, or
|
|
475
|
+
/// the qualified name of the scanned `@ViewProps` struct it names (`CardView.Props`). A class without
|
|
476
|
+
/// the generic argument is not reported, because the macro rejects it.
|
|
477
|
+
var propsType: String
|
|
478
|
+
|
|
479
|
+
/// The name `propsType` is looked up under: its spelling without generic arguments, or `nil` when it
|
|
480
|
+
/// isn't a plain or qualified type name. Not encoded.
|
|
481
|
+
var propsTypeLookupName: String? = nil
|
|
482
|
+
|
|
483
|
+
/// The value props of the `@ViewProps` struct, or `nil` when the scan declares no struct named
|
|
484
|
+
/// `propsType` on a platform of the view. A struct shared by several views is copied into each of
|
|
485
|
+
/// them, and a struct declared in several `#if` branches contributes the props of each declaration
|
|
486
|
+
/// whose platforms overlap the view's.
|
|
487
|
+
var props: [ExportedViewProp]? = nil
|
|
488
|
+
|
|
489
|
+
/// The events of the `@ViewProps` struct, or `nil` when `props` is `nil`.
|
|
490
|
+
var events: [ExportedViewEvent]? = nil
|
|
491
|
+
|
|
492
|
+
/// The spelled access modifier, or `nil` when none is written.
|
|
493
|
+
let accessLevel: String?
|
|
494
|
+
|
|
495
|
+
let platforms: [ExportedPlatform]
|
|
496
|
+
|
|
497
|
+
let file: String
|
|
498
|
+
|
|
499
|
+
private enum CodingKeys: String, CodingKey {
|
|
500
|
+
case name, jsName, propsType, props, events, accessLevel, platforms, file
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// One value prop of a `@ViewProps` struct: a stored property that isn't function-typed. Like
|
|
505
|
+
/// `ExportedRecordProperty`, but with no `required`: core resets a prop that React leaves out to its
|
|
506
|
+
/// default, so JS can always omit one.
|
|
507
|
+
struct ExportedViewProp: Encodable, Equatable {
|
|
508
|
+
let name: String
|
|
509
|
+
|
|
510
|
+
/// The annotated type, or the type of a literal initial value. A property with neither is not
|
|
511
|
+
/// reported, because the macro rejects it.
|
|
512
|
+
var type: TypeNode
|
|
513
|
+
|
|
514
|
+
/// The initial value as source text, or `nil` when none is written.
|
|
515
|
+
let defaultValue: String?
|
|
516
|
+
|
|
517
|
+
/// The spelled access modifier, or `nil` when none is written. Never `private` or `fileprivate`,
|
|
518
|
+
/// since those exclude a property from the props.
|
|
519
|
+
let accessLevel: String?
|
|
520
|
+
|
|
521
|
+
let platforms: [ExportedPlatform]
|
|
522
|
+
|
|
523
|
+
/// Optional-typed, read from `type`, so every spelling of an optional counts (`T?`, `T!`,
|
|
524
|
+
/// `Optional<T>`, `(T?)`). Encoded as `optional`.
|
|
525
|
+
var isOptional: Bool {
|
|
526
|
+
if case .optional = type {
|
|
527
|
+
return true
|
|
528
|
+
}
|
|
529
|
+
return false
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private enum CodingKeys: String, CodingKey {
|
|
533
|
+
case name, type, defaultValue, accessLevel, platforms
|
|
534
|
+
case isOptional = "optional"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
func encode(to encoder: Encoder) throws {
|
|
538
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
539
|
+
try container.encode(name, forKey: .name)
|
|
540
|
+
try container.encode(type, forKey: .type)
|
|
541
|
+
try container.encode(isOptional, forKey: .isOptional)
|
|
542
|
+
try container.encodeIfPresent(defaultValue, forKey: .defaultValue)
|
|
543
|
+
try container.encodeIfPresent(accessLevel, forKey: .accessLevel)
|
|
544
|
+
try container.encode(platforms, forKey: .platforms)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/// One event of a `@ViewProps` struct: a function-typed stored property. Its name is the React prop
|
|
549
|
+
/// name as written, with no `on` prefix stripped, so it carries no `jsName`.
|
|
550
|
+
struct ExportedViewEvent: Encodable, Equatable {
|
|
551
|
+
/// The Swift property name, e.g. `onTap`.
|
|
552
|
+
let name: String
|
|
553
|
+
|
|
554
|
+
/// The type of the function's single parameter, or `nil` when it takes none (`() -> Void`) or more
|
|
555
|
+
/// than one.
|
|
556
|
+
var payload: TypeNode?
|
|
557
|
+
|
|
558
|
+
/// The spelled access modifier, or `nil` when none is written.
|
|
559
|
+
let accessLevel: String?
|
|
560
|
+
|
|
561
|
+
let platforms: [ExportedPlatform]
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/// A `@ViewProps` struct the scan found. Not encoded: `ExportedSurface.joiningViewProps(_:)` copies its
|
|
565
|
+
/// props and events into each view that names it, and a struct that no view names is not reported.
|
|
566
|
+
struct ViewPropsDeclaration: Equatable {
|
|
567
|
+
/// The Swift struct name, qualified with the types it is nested in (`CardView.Props`).
|
|
568
|
+
let name: String
|
|
569
|
+
|
|
570
|
+
/// The value props, in declaration order.
|
|
571
|
+
let props: [ExportedViewProp]
|
|
572
|
+
|
|
573
|
+
/// The events, in declaration order.
|
|
574
|
+
let events: [ExportedViewEvent]
|
|
575
|
+
|
|
576
|
+
/// The platforms that build the struct, to match it with the views that build on them.
|
|
577
|
+
let platforms: [ExportedPlatform]
|
|
578
|
+
}
|
|
579
|
+
|
|
463
580
|
/// The exported types grouped by kind, nested under `exports` in the result so the surface is one
|
|
464
581
|
/// self-contained object separate from `stats`.
|
|
465
582
|
struct ExportedSurface: Encodable, Equatable {
|
|
@@ -468,6 +585,7 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
468
585
|
let records: [ExportedRecord]
|
|
469
586
|
let enums: [ExportedEnum]
|
|
470
587
|
let unions: [ExportedUnion]
|
|
588
|
+
let views: [ExportedView]
|
|
471
589
|
}
|
|
472
590
|
|
|
473
591
|
/// How an `@Event` is dispatched to JavaScript, from its option. Mirrors `EventDispatch` in the macros.
|
|
@@ -488,7 +606,8 @@ enum ExportedEventDispatch: String, Encodable, Equatable {
|
|
|
488
606
|
/// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
|
|
489
607
|
/// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
|
|
490
608
|
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes; version 11
|
|
491
|
-
/// replaces the `sync` flag of an event with `dispatch`.
|
|
609
|
+
/// replaces the `sync` flag of an event with `dispatch`. A new field that older consumers can ignore,
|
|
610
|
+
/// such as `views`, doesn't change the version.
|
|
492
611
|
let scanExportsSchemaVersion = 11
|
|
493
612
|
|
|
494
613
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
@@ -74,26 +74,32 @@ struct RefIndex {
|
|
|
74
74
|
/// dropped one at a time (`Media.Status` -> `Status`): a use site may qualify a name with a module
|
|
75
75
|
/// the scan doesn't see, which is how the conformance and raw-type checks already match names.
|
|
76
76
|
func lookup(_ name: String, in scope: String) -> Entry? {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
return scopedLookup(name, in: scope, entries: entries)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// The entry that a use of `name` written inside the type `scope` names, from `entries` keyed by
|
|
82
|
+
/// qualified name. Swift's lookup rules, as `RefIndex.lookup(_:in:)` documents them.
|
|
83
|
+
private func scopedLookup<Entry>(_ name: String, in scope: String, entries: [String: Entry]) -> Entry? {
|
|
84
|
+
let name = name.hasPrefix("Self.") ? String(name.dropFirst("Self.".count)) : name
|
|
85
|
+
var components = scope.split(separator: ".").map(String.init)
|
|
86
|
+
while true {
|
|
87
|
+
if let entry = entries[(components + [name]).joined(separator: ".")] {
|
|
88
|
+
return entry
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if components.isEmpty {
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
components.removeLast()
|
|
94
|
+
}
|
|
95
|
+
var suffix = name.split(separator: ".").map(String.init).dropFirst()
|
|
96
|
+
while !suffix.isEmpty {
|
|
97
|
+
if let entry = entries[suffix.joined(separator: ".")] {
|
|
98
|
+
return entry
|
|
94
99
|
}
|
|
95
|
-
|
|
100
|
+
suffix = suffix.dropFirst()
|
|
96
101
|
}
|
|
102
|
+
return nil
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
/// The JS category a raw-value enum crosses as: its raw value's. A bare `Enumerable` conformance with
|
|
@@ -214,6 +220,59 @@ extension ExportedSurface {
|
|
|
214
220
|
return resolvedMember
|
|
215
221
|
}
|
|
216
222
|
return resolved
|
|
223
|
+
},
|
|
224
|
+
// The props and events are written in the `@ViewProps` struct, so their refs are looked up from
|
|
225
|
+
// it. `joiningViewProps(_:)` runs first and sets `propsType` to the struct's qualified name.
|
|
226
|
+
views: views.map { view in
|
|
227
|
+
var resolved = view
|
|
228
|
+
resolved.props = view.props?.map { prop in
|
|
229
|
+
var resolvedProp = prop
|
|
230
|
+
resolvedProp.type = prop.type.resolvingRefs(using: index, in: view.propsType)
|
|
231
|
+
return resolvedProp
|
|
232
|
+
}
|
|
233
|
+
resolved.events = view.events?.map { event in
|
|
234
|
+
var resolvedEvent = event
|
|
235
|
+
resolvedEvent.payload = event.payload?.resolvingRefs(using: index, in: view.propsType)
|
|
236
|
+
return resolvedEvent
|
|
237
|
+
}
|
|
238
|
+
return resolved
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// This surface with the props and events of each view's `@ViewProps` struct copied into the view.
|
|
244
|
+
/// The struct is looked up from the view like a ref, by its name without generic arguments, so it
|
|
245
|
+
/// may be declared in another file or nested (`CardView.Props`). A found struct gives the view its
|
|
246
|
+
/// qualified name as `propsType`. A view whose struct the scan doesn't declare keeps `props` and
|
|
247
|
+
/// `events` at `nil`, and `propsType` as written.
|
|
248
|
+
///
|
|
249
|
+
/// A struct declared more than once, in `#if` branches, contributes the props and events of each
|
|
250
|
+
/// declaration whose platforms overlap the platforms of the view, as a superclass does in
|
|
251
|
+
/// `mergingInheritedMembers()`. Each prop and event keeps its own `platforms`. A view that builds on
|
|
252
|
+
/// none of those platforms is treated as one whose struct the scan doesn't declare.
|
|
253
|
+
func joiningViewProps(_ declarations: [ViewPropsDeclaration]) -> ExportedSurface {
|
|
254
|
+
let entries = Dictionary(grouping: declarations, by: \.name)
|
|
255
|
+
return ExportedSurface(
|
|
256
|
+
modules: modules,
|
|
257
|
+
sharedObjects: sharedObjects,
|
|
258
|
+
records: records,
|
|
259
|
+
enums: enums,
|
|
260
|
+
unions: unions,
|
|
261
|
+
views: views.map { view in
|
|
262
|
+
let platforms = Set(view.platforms.map(\.name))
|
|
263
|
+
guard let name = view.propsTypeLookupName,
|
|
264
|
+
let variants = scopedLookup(name, in: view.name, entries: entries)?.filter({ declaration in
|
|
265
|
+
declaration.platforms.contains { platforms.contains($0.name) }
|
|
266
|
+
}),
|
|
267
|
+
let first = variants.first
|
|
268
|
+
else {
|
|
269
|
+
return view
|
|
270
|
+
}
|
|
271
|
+
var joined = view
|
|
272
|
+
joined.propsType = first.name
|
|
273
|
+
joined.props = variants.flatMap(\.props)
|
|
274
|
+
joined.events = variants.flatMap(\.events)
|
|
275
|
+
return joined
|
|
217
276
|
}
|
|
218
277
|
)
|
|
219
278
|
}
|
|
@@ -26,11 +26,11 @@ extension Scanner {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/// Scans `paths` for `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
30
|
-
/// enums, and returns their exported surface plus the run's stats.
|
|
31
|
-
/// tests can drive it without argv/stdout.
|
|
29
|
+
/// Scans `paths` for `@ExpoModule`, `@SharedObject`, `@Record`, `@Union`, `@ExpoView`, and
|
|
30
|
+
/// `@ViewProps` types plus `Enumerable` enums, and returns their exported surface plus the run's stats.
|
|
31
|
+
/// Separate from the public entry so tests can drive it without argv/stdout.
|
|
32
32
|
///
|
|
33
|
-
/// The pre-filter omits `@Event`: an event only declares a member of
|
|
33
|
+
/// The pre-filter omits `@Event`: an event only declares a member of a module or shared object, so a
|
|
34
34
|
/// file containing one already matches on its enclosing type. It does include `Enumerable`, which is a
|
|
35
35
|
/// conformance rather than an attribute: an enum is commonly declared in a file of its own, which no
|
|
36
36
|
/// macro attribute would match.
|
|
@@ -40,10 +40,12 @@ func scanExports(paths: [String]) -> ScanExportsResult {
|
|
|
40
40
|
var records: [ExportedRecord] = []
|
|
41
41
|
var enums: [ExportedEnum] = []
|
|
42
42
|
var unions: [ExportedUnion] = []
|
|
43
|
+
var views: [ExportedView] = []
|
|
44
|
+
var viewProps: [ViewPropsDeclaration] = []
|
|
43
45
|
|
|
44
46
|
let stats = scanFiles(
|
|
45
47
|
paths: paths,
|
|
46
|
-
macros: [.expoModule, .sharedObject, .record, .union],
|
|
48
|
+
macros: [.expoModule, .sharedObject, .record, .union, .expoView, .viewProps],
|
|
47
49
|
conformances: [enumerableConformanceName]
|
|
48
50
|
) { source, file in
|
|
49
51
|
let tree = Parser.parse(source: source)
|
|
@@ -54,18 +56,23 @@ func scanExports(paths: [String]) -> ScanExportsResult {
|
|
|
54
56
|
records.append(contentsOf: visitor.records)
|
|
55
57
|
enums.append(contentsOf: visitor.enums)
|
|
56
58
|
unions.append(contentsOf: visitor.unions)
|
|
59
|
+
views.append(contentsOf: visitor.views)
|
|
60
|
+
viewProps.append(contentsOf: visitor.viewProps)
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
// Refs resolve only once every file has been walked: a type parsed in one file may name a type
|
|
60
64
|
// declared in another, so the full set of declarations has to exist before any lookup is valid. The
|
|
61
|
-
// same holds for a
|
|
65
|
+
// same holds for a view's `@ViewProps` struct, which is joined before the refs resolve so that the
|
|
66
|
+
// refs in its props resolve from the struct, and for a superclass, so the inherited members are
|
|
67
|
+
// merged last.
|
|
62
68
|
let surface = ExportedSurface(
|
|
63
69
|
modules: modules,
|
|
64
70
|
sharedObjects: sharedObjects,
|
|
65
71
|
records: records,
|
|
66
72
|
enums: enums,
|
|
67
|
-
unions: unions
|
|
68
|
-
|
|
73
|
+
unions: unions,
|
|
74
|
+
views: views
|
|
75
|
+
).joiningViewProps(viewProps).resolvingRefs().mergingInheritedMembers()
|
|
69
76
|
|
|
70
77
|
return ScanExportsResult(
|
|
71
78
|
schemaVersion: scanExportsSchemaVersion,
|