@expo/expo-modules-macros-plugin 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +4 -0
- package/.github/workflows/swift.yml +6 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +3 -1
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +10 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +2 -2
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +24 -2
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +2 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -16
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +487 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +8 -4
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +6 -4
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +28 -7
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +3 -9
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +24 -8
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +133 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +25 -6
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +315 -6
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +35 -6
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +89 -22
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- package/package.json +12 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg width="303" height="48" viewBox="0 0 303 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0_748_204)">
|
|
3
|
+
<rect width="48" height="48" rx="10" fill="#63607A"/>
|
|
4
|
+
<g opacity="0.4">
|
|
5
|
+
<rect x="-28" y="-28" width="64" height="64" rx="10" fill="url(#paint0_radial_748_204)"/>
|
|
6
|
+
</g>
|
|
7
|
+
<path opacity="0.55" d="M21.4 24H24.5M24.5 17V31M24.5 17H28.8M24.5 24H28.8M24.5 31H28.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
8
|
+
<circle cx="18" cy="24" r="3.4" stroke="white" stroke-width="2"/>
|
|
9
|
+
<circle cx="31" cy="17" r="2.2" stroke="white" stroke-width="2"/>
|
|
10
|
+
<circle cx="31" cy="24" r="2.2" stroke="white" stroke-width="2"/>
|
|
11
|
+
<circle cx="31" cy="31" r="2.2" stroke="white" stroke-width="2"/>
|
|
12
|
+
</g>
|
|
13
|
+
<path d="M69.5781 32.5H80.0469V30.0703H72.4766V25.7031H79.4453V23.2734H72.4766V18.9297H79.9844V16.5H69.5781ZM85.1328 20.5H82.1328L85.6953 26.5L82.0391 32.5H85.0391L87.5547 28.1797L90.0938 32.5H93.0703L89.3906 26.5L93.0078 20.5H90.0156L87.5547 24.9297ZM95.1953 37.0H98.0234V30.6094H98.1406C98.5859 31.4844 99.5156 32.7109 101.5781 32.7109C104.4062 32.7109 106.5234 30.4688 106.5234 26.5156C106.5234 22.5156 104.3438 20.3438 101.5703 20.3438C99.4531 20.3438 98.5703 21.6172 98.1406 22.4844H97.9766V20.5H95.1953ZM97.9688 26.5C97.9688 24.1719 98.9688 22.6641 100.7891 22.6641C102.6719 22.6641 103.6406 24.2656 103.6406 26.5C103.6406 28.75 102.6562 30.3906 100.7891 30.3906C98.9844 30.3906 97.9688 28.8281 97.9688 26.5ZM114.1719 32.7344C117.6875 32.7344 119.9219 30.2578 119.9219 26.5469C119.9219 22.8281 117.6875 20.3438 114.1719 20.3438C110.6562 20.3438 108.4219 22.8281 108.4219 26.5469C108.4219 30.2578 110.6562 32.7344 114.1719 32.7344ZM114.1875 30.4688C112.2422 30.4688 111.2891 28.7344 111.2891 26.5391C111.2891 24.3438 112.2422 22.5859 114.1875 22.5859C116.1016 22.5859 117.0547 24.3438 117.0547 26.5391C117.0547 28.7344 116.1016 30.4688 114.1875 30.4688ZM127.8984 16.5V32.5H130.6797V21.4844H130.8281L135.25 32.4531H137.3281L141.75 21.5078H141.8984V32.5H144.6797V16.5H141.1328L136.3828 28.0938H136.1953L131.4453 16.5ZM152.9453 32.7344C156.4609 32.7344 158.6953 30.2578 158.6953 26.5469C158.6953 22.8281 156.4609 20.3438 152.9453 20.3438C149.4297 20.3438 147.1953 22.8281 147.1953 26.5469C147.1953 30.2578 149.4297 32.7344 152.9453 32.7344ZM152.9609 30.4688C151.0156 30.4688 150.0625 28.7344 150.0625 26.5391C150.0625 24.3438 151.0156 22.5859 152.9609 22.5859C154.875 22.5859 155.8281 24.3438 155.8281 26.5391C155.8281 28.7344 154.875 30.4688 152.9609 30.4688ZM165.5391 32.7109C167.6016 32.7109 168.5312 31.4844 168.9766 30.6094H169.1484V32.5H171.9297V16.5H169.0938V22.4844H168.9766C168.5469 21.6172 167.6641 20.3438 165.5469 20.3438C162.7734 20.3438 160.5938 22.5156 160.5938 26.5156C160.5938 30.4688 162.7109 32.7109 165.5391 32.7109ZM166.3281 30.3906C164.4609 30.3906 163.4766 28.75 163.4766 26.5C163.4766 24.2656 164.4453 22.6641 166.3281 22.6641C168.1484 22.6641 169.1484 24.1719 169.1484 26.5C169.1484 28.8281 168.1328 30.3906 166.3281 30.3906ZM182.6016 27.4531C182.6016 29.2812 181.2969 30.1875 180.0469 30.1875C178.6875 30.1875 177.7812 29.2266 177.7812 27.7031V20.5H174.9531V28.1406C174.9531 31.0234 176.5938 32.6562 178.9531 32.6562C180.75 32.6562 182.0156 31.7109 182.5625 30.3672H182.6875V32.5H185.4297V20.5H182.6016ZM191.1641 16.5H188.3359V32.5H191.1641ZM199.3828 32.7344C202.1797 32.7344 204.1016 31.3672 204.6016 29.2812L201.9609 28.9844C201.5781 30.0 200.6406 30.5312 199.4219 30.5312C197.5938 30.5312 196.3828 29.3281 196.3594 27.2734H204.7188V26.4062C204.7188 22.1953 202.1875 20.3438 199.2344 20.3438C195.7969 20.3438 193.5547 22.8672 193.5547 26.5703C193.5547 30.3359 195.7656 32.7344 199.3828 32.7344ZM196.3672 25.3672C196.4531 23.8359 197.5859 22.5469 199.2734 22.5469C200.8984 22.5469 201.9922 23.7344 202.0078 25.3672ZM216.5938 23.6719C216.2031 21.6406 214.5781 20.3438 211.7656 20.3438C208.875 20.3438 206.9062 21.7656 206.9141 23.9844C206.9062 25.7344 207.9844 26.8906 210.2891 27.3672L212.3359 27.7969C213.4375 28.0391 213.9531 28.4844 213.9531 29.1641C213.9531 29.9844 213.0625 30.6016 211.7188 30.6016C210.4219 30.6016 209.5781 30.0391 209.3359 28.9609L206.5781 29.2266C206.9297 31.4297 208.7812 32.7344 211.7266 32.7344C214.7266 32.7344 216.8438 31.1797 216.8516 28.9062C216.8438 27.1953 215.7422 26.1484 213.4766 25.6562L211.4297 25.2188C210.2109 24.9453 209.7266 24.5234 209.7344 23.8281C209.7266 23.0156 210.625 22.4531 211.8047 22.4531C213.1094 22.4531 213.7969 23.1641 214.0156 23.9531ZM224.7734 16.5V32.5H227.5547V21.4844H227.7031L232.125 32.4531H234.2031L238.625 21.5078H238.7734V32.5H241.5547V16.5H238.0078L233.2578 28.0938H233.0703L228.3203 16.5ZM248.0391 32.7422C249.9219 32.7422 251.0469 31.8594 251.5625 30.8516H251.6562V32.5H254.375V24.4688C254.375 21.2969 251.7891 20.3438 249.5 20.3438C246.9766 20.3438 245.0391 21.4688 244.4141 23.6562L247.0547 24.0312C247.3359 23.2109 248.1328 22.5078 249.5156 22.5078C250.8281 22.5078 251.5469 23.1797 251.5469 24.3594V24.4062C251.5469 25.2188 250.6953 25.2578 248.5781 25.4844C246.25 25.7344 244.0234 26.4297 244.0234 29.1328C244.0234 31.4922 245.75 32.7422 248.0391 32.7422ZM248.7734 30.6641C247.5938 30.6641 246.75 30.125 246.75 29.0859C246.75 28.0 247.6953 27.5469 248.9609 27.3672C249.7031 27.2656 251.1875 27.0781 251.5547 26.7812V28.1953C251.5547 29.5312 250.4766 30.6641 248.7734 30.6641ZM262.4609 32.7344C265.4766 32.7344 267.3984 30.9453 267.6016 28.3984H264.8984C264.6562 29.6875 263.7266 30.4297 262.4844 30.4297C260.7188 30.4297 259.5781 28.9531 259.5781 26.5C259.5781 24.0781 260.7422 22.625 262.4844 22.625C263.8438 22.625 264.6797 23.5 264.8984 24.6562H267.6016C267.4062 22.0547 265.375 20.3438 262.4453 20.3438C258.9297 20.3438 256.7109 22.8828 256.7109 26.5469C256.7109 30.1797 258.875 32.7344 262.4609 32.7344ZM269.9297 32.5H272.7578V25.4453C272.7578 23.9219 273.9062 22.8438 275.4609 22.8438C275.9375 22.8438 276.5312 22.9297 276.7734 23.0078V20.4062C276.5156 20.3594 276.0703 20.3281 275.7578 20.3281C274.3828 20.3281 273.2344 21.1094 272.7969 22.5H272.6719V20.5H269.9297ZM283.8906 32.7344C287.4062 32.7344 289.6406 30.2578 289.6406 26.5469C289.6406 22.8281 287.4062 20.3438 283.8906 20.3438C280.375 20.3438 278.1406 22.8281 278.1406 26.5469C278.1406 30.2578 280.375 32.7344 283.8906 32.7344ZM283.9062 30.4688C281.9609 30.4688 281.0078 28.7344 281.0078 26.5391C281.0078 24.3438 281.9609 22.5859 283.9062 22.5859C285.8203 22.5859 286.7734 24.3438 286.7734 26.5391C286.7734 28.7344 285.8203 30.4688 283.9062 30.4688ZM301.5156 23.6719C301.125 21.6406 299.5 20.3438 296.6875 20.3438C293.7969 20.3438 291.8281 21.7656 291.8359 23.9844C291.8281 25.7344 292.9062 26.8906 295.2109 27.3672L297.2578 27.7969C298.3594 28.0391 298.875 28.4844 298.875 29.1641C298.875 29.9844 297.9844 30.6016 296.6406 30.6016C295.3438 30.6016 294.5 30.0391 294.2578 28.9609L291.5 29.2266C291.8516 31.4297 293.7031 32.7344 296.6484 32.7344C299.6484 32.7344 301.7656 31.1797 301.7734 28.9062C301.7656 27.1953 300.6641 26.1484 298.3984 25.6562L296.3516 25.2188C295.1328 24.9453 294.6484 24.5234 294.6562 23.8281C294.6484 23.0156 295.5469 22.4531 296.7266 22.4531C298.0312 22.4531 298.7188 23.1641 298.9375 23.9531Z" fill="#63607A"/>
|
|
14
|
+
<defs>
|
|
15
|
+
<radialGradient id="paint0_radial_748_204" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(4 4) rotate(90) scale(32)">
|
|
16
|
+
<stop stop-color="white"/>
|
|
17
|
+
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
|
18
|
+
</radialGradient>
|
|
19
|
+
<clipPath id="clip0_748_204">
|
|
20
|
+
<rect width="48" height="48" rx="10" fill="white"/>
|
|
21
|
+
</clipPath>
|
|
22
|
+
</defs>
|
|
23
|
+
</svg>
|
|
@@ -81,6 +81,10 @@ jobs:
|
|
|
81
81
|
run: swift package resolve
|
|
82
82
|
working-directory: apple
|
|
83
83
|
|
|
84
|
+
- name: Install dependencies
|
|
85
|
+
# The wrapper's `prepublishOnly` build needs `tsc`; the tool binary needs none.
|
|
86
|
+
run: npm install
|
|
87
|
+
|
|
84
88
|
- name: Build release tool
|
|
85
89
|
# Runs apple/build.js: builds the release plugin binary,
|
|
86
90
|
# copies it to apple/ExpoModulesMacros-tool, and strips it.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<a href="https://docs.expo.dev/modules/">
|
|
3
|
+
<img
|
|
4
|
+
src=".github/resources/expo-modules-macros.svg"
|
|
5
|
+
alt="expo-modules-macros-plugin"
|
|
6
|
+
height="64" />
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
`@expo/expo-modules-macros-plugin` is the Swift compiler plugin behind the Expo Modules API. It implements the macros that [`expo-modules-core`](https://github.com/expo/expo/tree/main/packages/expo-modules-core) declares, so a module author writes plain Swift declarations and the plugin synthesizes the code that binds them to JavaScript.
|
|
11
|
+
|
|
12
|
+
The same executable doubles as a source scanner CLI.
|
|
13
|
+
|
|
14
|
+
# Installation
|
|
15
|
+
|
|
16
|
+
This package is not meant to be installed directly. It is a dependency of `expo-modules-core`, so every Expo project already has it. The published package contains a prebuilt universal (arm64 + x86_64) macOS binary at `apple/ExpoModulesMacros-tool`, so consumers never build the plugin themselves.
|
|
17
|
+
|
|
18
|
+
# Macros
|
|
19
|
+
|
|
20
|
+
- **`@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.
|
|
21
|
+
- **`@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. The macro also checks that every type crossing the boundary is convertible in the direction it travels, so a bad type is reported on the author's own declaration rather than on the enclosing type. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it.
|
|
22
|
+
- **`@Event(_ name: String? = nil, sync: Bool = false)`** 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. `sync: true` dispatches inline instead of asynchronously.
|
|
23
|
+
- **`@SharedObject(_ name: String? = nil)`** on a `SharedObject` subclass. Collects the class's `@JS` members into a class definition that a module exposes through `@ExpoModule(classes:)`.
|
|
24
|
+
- **`@Record()`** on a record type. 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. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
25
|
+
- **`@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.
|
|
26
|
+
|
|
27
|
+
How that looks in a module:
|
|
28
|
+
|
|
29
|
+
```swift
|
|
30
|
+
import ExpoModulesCore
|
|
31
|
+
|
|
32
|
+
@ExpoModule(classes: [Cache.self])
|
|
33
|
+
public final class MyModule {
|
|
34
|
+
@JS
|
|
35
|
+
func greet(name: String) -> String {
|
|
36
|
+
"Hi, \(name)"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@JS("doWork")
|
|
40
|
+
func performWork() async throws { ... }
|
|
41
|
+
|
|
42
|
+
@Event
|
|
43
|
+
var onProgress: (ProgressEvent) -> Void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@SharedObject
|
|
47
|
+
final class Cache: SharedObject {
|
|
48
|
+
@JS
|
|
49
|
+
init(name: String) { ... }
|
|
50
|
+
|
|
51
|
+
@JS
|
|
52
|
+
func get(_ key: String) -> String? { ... }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The author-facing documentation for each macro lives next to its declaration in `expo-modules-core`, in `ios/Core/ExpoModulesMacros.swift`. This repository holds the implementations.
|
|
57
|
+
|
|
58
|
+
# Scanner
|
|
59
|
+
|
|
60
|
+
The Swift compiler launches a plugin executable with no arguments and speaks the plugin protocol over stdin, so the binary treats any argument as a scanner invocation instead:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
ExpoModulesMacros-tool <subcommand> [options] <path> [<path> ...]
|
|
64
|
+
|
|
65
|
+
subcommands:
|
|
66
|
+
scan-modules fast scan for top-level @ExpoModule types (autolinking)
|
|
67
|
+
scan-exports deep scan of the full JS-exported surface (type generation)
|
|
68
|
+
|
|
69
|
+
options (scan-modules only):
|
|
70
|
+
--platform <os> evaluate '#if os(...)' against this platform
|
|
71
|
+
--define <flag> treat a conditional compilation flag as set; repeatable
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Each path is a `.swift` file or a directory, scanned recursively for `.swift` files. Both subcommands print a JSON report to stdout, each carrying its own `schemaVersion` so a consumer can check it understands the shape before trusting it. The two versions are independent: the commands serve different consumers and change for different reasons.
|
|
75
|
+
|
|
76
|
+
# TypeScript wrapper
|
|
77
|
+
|
|
78
|
+
Node consumers can call the scanner through this package instead of locating the binary and shelling out themselves:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { scanModules, scanExports } from '@expo/expo-modules-macros-plugin';
|
|
82
|
+
|
|
83
|
+
const { modules, warnings } = await scanModules(['ios/'], { defines: ['DEBUG'] });
|
|
84
|
+
const { exports } = await scanExports(['ios/']);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The binary is a compiled executable, so each call still spawns a process. What the wrapper owns is the part consumers would otherwise duplicate: resolving the shipped binary, building the arguments, parsing the JSON, checking `schemaVersion`, and turning a non-zero exit into a `ScannerError`. The result types are hand-written mirrors of the Swift `Codable` types, which is what the version check guards against drifting.
|
|
88
|
+
|
|
89
|
+
# How the plugin reaches the compiler
|
|
90
|
+
|
|
91
|
+
`expo-modules-core` declares the macro signatures with `#externalMacro(module: "ExpoModulesMacros", type: …)`. During `pod install`, `expo-modules-autolinking` resolves this package from the core package and appends
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
-Xfrontend -load-plugin-executable -Xfrontend <plugin>/apple/ExpoModulesMacros-tool#ExpoModulesMacros
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
to `OTHER_SWIFT_FLAGS` for `ExpoModulesCore`, every pod that depends on it, and their test specs. Expo's SPM prebuilds pass the same flag when they generate `Package.swift`, so both build systems load the same binary.
|
|
98
|
+
|
|
99
|
+
The module and type names in `#externalMacro` must stay in sync with `apple/Sources/ExpoModulesMacros/Plugin.swift`.
|
|
100
|
+
|
|
101
|
+
# Development
|
|
102
|
+
|
|
103
|
+
Requires macOS 13 or newer and a toolchain with Swift 6.2, which means Xcode 26 or newer.
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
cd apple
|
|
107
|
+
swift build
|
|
108
|
+
swift test
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`npm run build` runs `apple/build.js`, which builds the release binary for arm64 and x86_64, merges the slices into `apple/ExpoModulesMacros-tool` with `lipo`, strips it, and verifies both slices are present. SwiftPM only builds macro tools for the host architecture, so the x86_64 slice is produced by running the toolchain under Rosetta; the script installs Rosetta if it is missing. The resulting binary is committed to the repository.
|
|
112
|
+
|
|
113
|
+
# Releasing
|
|
114
|
+
|
|
115
|
+
The **Publish** workflow is manual (`workflow_dispatch`) and takes a release type. It bumps the version, builds the universal binary, and publishes to npm through OIDC trusted publishing. The commit, tag and GitHub release are created only after the publish succeeds, so a failed build leaves the branch untouched.
|
|
116
|
+
|
|
117
|
+
# Contributing
|
|
118
|
+
|
|
119
|
+
Contributions are very welcome! Please refer to the guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
|
|
Binary file
|
|
@@ -192,7 +192,9 @@ internal struct JSFunction {
|
|
|
192
192
|
}
|
|
193
193
|
callArguments.append(label == "_" ? value : "\(label): \(value)")
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
// `try` only for a throwing function. An `async` call that doesn't throw must not get one, or the
|
|
196
|
+
// expansion warns that no throwing call occurs within the `try` expression.
|
|
197
|
+
let tryKeyword = isThrowing ? "try " : ""
|
|
196
198
|
let awaitKeyword = isAsync ? "await " : ""
|
|
197
199
|
return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments.joined(separator: ", ")))"
|
|
198
200
|
}
|
|
@@ -88,6 +88,16 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
88
88
|
// JS object name, so they can't diverge.
|
|
89
89
|
emitted.append("public static let _jsName = \"\(raw: moduleName)\"")
|
|
90
90
|
|
|
91
|
+
// View classes are registered by type, not spliced as definitions: an `@ExpoView` emits no
|
|
92
|
+
// definition function, so there is nothing to call on each entry. Core reads a view's props
|
|
93
|
+
// type from its `Props` typealias and its event names from that type's `_eventNames`, both
|
|
94
|
+
// static. Emitted only when the argument is present, so a module without views gains nothing.
|
|
95
|
+
let viewTypes = classListArgument(of: node, label: "views")
|
|
96
|
+
if !viewTypes.isEmpty {
|
|
97
|
+
let entries = viewTypes.map { "\($0).self" }.joined(separator: ", ")
|
|
98
|
+
emitted.append("public static let _viewTypes: [Any.Type] = [\(raw: entries)]")
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
// `Module`/`BaseModule` already provide `appContext` storage and the
|
|
92
102
|
// `init(appContext:)` requirement, so we only synthesize them for classes that
|
|
93
103
|
// inherit from neither. Each is skipped individually if the user wrote their own,
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxBuilder
|
|
4
|
+
import SwiftSyntaxMacros
|
|
5
|
+
|
|
6
|
+
/// Member macro applied to an `ExpoView` subclass, binding it to its props type:
|
|
7
|
+
///
|
|
8
|
+
/// @ExpoView<CardProps>
|
|
9
|
+
/// final class CardView: ExpoView {
|
|
10
|
+
/// override func didUpdateProps(_ diff: CardProps.Diff) {
|
|
11
|
+
/// if diff.changed(.color) {
|
|
12
|
+
/// backgroundColor = props.color
|
|
13
|
+
/// }
|
|
14
|
+
/// }
|
|
15
|
+
/// }
|
|
16
|
+
///
|
|
17
|
+
/// The props type is the attribute's **generic argument**, so it sits in type position: no
|
|
18
|
+
/// `.self` metatype to write, and the macro reads it straight off the attribute's
|
|
19
|
+
/// `genericArgumentClause`. Core's declaration constrains the parameter
|
|
20
|
+
/// (`macro ExpoView<Props: AnyViewProps>()`), so passing a type that isn't a props type is a
|
|
21
|
+
/// compile error at the author's own line ("requires that 'X' conform to 'AnyViewProps'")
|
|
22
|
+
/// rather than a macro diagnostic. This macro therefore validates nothing about the props type.
|
|
23
|
+
///
|
|
24
|
+
/// The whole expansion is one line:
|
|
25
|
+
///
|
|
26
|
+
/// public typealias Props = CardProps
|
|
27
|
+
///
|
|
28
|
+
/// That is deliberately all of it. There is **no view definition**: no `ViewDefinition`, no
|
|
29
|
+
/// `View(…) { }` builder, no `Props(_:)` or `Events(_:)` element. `_synthesizedViewDefinition()`
|
|
30
|
+
/// existed only to hand core a DSL value the old registration path could consume, and modules and
|
|
31
|
+
/// shared objects already moved to direct hooks. Core reads the props type from this typealias and
|
|
32
|
+
/// the event names from the props type's own `_eventNames` (synthesized by `@ViewProps`), both
|
|
33
|
+
/// static, neither needing a builder to be evaluated.
|
|
34
|
+
///
|
|
35
|
+
/// Views also get no `_decorateView` hook in its place: `_decorateModule` and `_decorateSharedObject`
|
|
36
|
+
/// exist to bind members into a JS object, and a view has no such object. Props reach a view through
|
|
37
|
+
/// Fabric's mounting layer, not a JS call, so a view's runtime entry point is the props update hook.
|
|
38
|
+
///
|
|
39
|
+
/// `didUpdateProps` is a core-called override, not synthesized here.
|
|
40
|
+
public struct ExpoViewMacro: MemberMacro {
|
|
41
|
+
public static func expansion(
|
|
42
|
+
of node: AttributeSyntax,
|
|
43
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
44
|
+
conformingTo protocols: [TypeSyntax],
|
|
45
|
+
in context: some MacroExpansionContext
|
|
46
|
+
) throws -> [DeclSyntax] {
|
|
47
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
48
|
+
throw MacroExpansionErrorMessage("@ExpoView can only be applied to a class")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
guard inheritsFromAny(classDecl, names: expoViewBaseClassNames) else {
|
|
52
|
+
throw DiagnosticsError(diagnostics: [missingBaseClassDiagnostic(for: classDecl)])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A generic view can't be registered: `@ExpoModule(views: [CardView.self])` on a generic type
|
|
56
|
+
// fails with "generic parameter 'T' could not be inferred", and the props type ignores the
|
|
57
|
+
// parameter anyway. Reject it here, at the author's line, rather than at the registration site.
|
|
58
|
+
// `@ViewProps` rejects generic props types for the same reason.
|
|
59
|
+
if classDecl.genericParameterClause != nil {
|
|
60
|
+
throw MacroExpansionErrorMessage(
|
|
61
|
+
"@ExpoView does not support generic classes — a view registered with @ExpoModule(views:) must be concrete"
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The expansion is a `Props` typealias, so a member of that name already on the class collides.
|
|
66
|
+
// Left alone, the compiler reports "invalid redeclaration of 'Props'" positioned inside the
|
|
67
|
+
// macro expansion, which is code the author never wrote. Name it here instead.
|
|
68
|
+
if let existing = existingPropsMemberKind(in: classDecl) {
|
|
69
|
+
throw MacroExpansionErrorMessage(
|
|
70
|
+
"@ExpoView synthesizes a `Props` typealias, but this class already declares a \(existing) named 'Props'. Rename it"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The props type is the attribute's generic argument. Core's declaration is generic, so a bare
|
|
75
|
+
// `@ExpoView` never reaches expansion: it fails type-checking first with "generic parameter
|
|
76
|
+
// 'Props' could not be inferred". This diagnostic covers the case where the macro is declared
|
|
77
|
+
// differently than core declares it, so the author still gets a message naming the attribute.
|
|
78
|
+
guard let propsType = genericArgument(of: node) else {
|
|
79
|
+
throw MacroExpansionErrorMessage(
|
|
80
|
+
"@ExpoView requires its props type as a generic argument, as in `@ExpoView<MyViewProps>`"
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
"""
|
|
86
|
+
public typealias Props = \(raw: propsType)
|
|
87
|
+
"""
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// MARK: - Diagnostics
|
|
93
|
+
|
|
94
|
+
/// The error for a class that doesn't name `ExpoView` in its inheritance clause, carrying a fix-it
|
|
95
|
+
/// that adds it.
|
|
96
|
+
///
|
|
97
|
+
/// The fix-it only applies where the edit is unambiguous. With no inheritance clause at all it
|
|
98
|
+
/// inserts `: ExpoView`; with an existing clause it prepends `ExpoView` as the first entry, since
|
|
99
|
+
/// Swift requires the superclass to come before any protocol. A class that already names some other
|
|
100
|
+
/// superclass gets the diagnostic without a fix-it: replacing that superclass is a decision the macro
|
|
101
|
+
/// can't make (see the indirect-inheritance note on `expoViewBaseClassNames`).
|
|
102
|
+
private func missingBaseClassDiagnostic(for classDecl: ClassDeclSyntax) -> Diagnostic {
|
|
103
|
+
let message = ExpoViewDiagnosticMessage(
|
|
104
|
+
"@ExpoView class must inherit from ExpoView. Add `: ExpoView` to the class declaration.",
|
|
105
|
+
id: "expoview-missing-base-class"
|
|
106
|
+
)
|
|
107
|
+
var fixIts: [FixIt] = []
|
|
108
|
+
|
|
109
|
+
if classDecl.inheritanceClause == nil {
|
|
110
|
+
// `class CardView {` → `class CardView: ExpoView {`. The name carries the space before `{` as
|
|
111
|
+
// trailing trivia, so move it onto the clause to keep the spacing.
|
|
112
|
+
let name = classDecl.name
|
|
113
|
+
let clause = InheritanceClauseSyntax(
|
|
114
|
+
colon: .colonToken(trailingTrivia: .space),
|
|
115
|
+
inheritedTypes: InheritedTypeListSyntax([
|
|
116
|
+
InheritedTypeSyntax(type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("ExpoView"))))
|
|
117
|
+
])
|
|
118
|
+
)
|
|
119
|
+
fixIts.append(
|
|
120
|
+
FixIt(
|
|
121
|
+
message: ExpoViewFixItMessage("Inherit from 'ExpoView'", id: "expoview-add-base-class"),
|
|
122
|
+
changes: [
|
|
123
|
+
.replace(
|
|
124
|
+
oldNode: Syntax(classDecl),
|
|
125
|
+
newNode: Syntax(
|
|
126
|
+
classDecl
|
|
127
|
+
.with(\.name, name.with(\.trailingTrivia, []))
|
|
128
|
+
.with(\.inheritanceClause, clause.with(\.trailingTrivia, name.trailingTrivia))
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
} else if let clause = classDecl.inheritanceClause, inheritsOnlyProtocolsByConvention(clause) {
|
|
135
|
+
// `class CardView: Identifiable {` → `class CardView: ExpoView, Identifiable {`.
|
|
136
|
+
var inherited = clause.inheritedTypes
|
|
137
|
+
if var first = inherited.first {
|
|
138
|
+
first.trailingComma = .commaToken(trailingTrivia: .space)
|
|
139
|
+
first.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("ExpoView")))
|
|
140
|
+
inherited.insert(first, at: inherited.startIndex)
|
|
141
|
+
}
|
|
142
|
+
fixIts.append(
|
|
143
|
+
FixIt(
|
|
144
|
+
message: ExpoViewFixItMessage("Inherit from 'ExpoView'", id: "expoview-add-base-class"),
|
|
145
|
+
changes: [
|
|
146
|
+
.replace(
|
|
147
|
+
oldNode: Syntax(clause),
|
|
148
|
+
newNode: Syntax(clause.with(\.inheritedTypes, inherited))
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Diagnostic(node: classDecl.name, message: message, fixIts: fixIts)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Whether every entry in the clause looks like a protocol rather than a superclass, by the
|
|
159
|
+
/// capitalization-free heuristic that a Swift superclass must come first: if the first entry is one
|
|
160
|
+
/// the macro would be replacing, prepending `ExpoView` would produce two superclasses. A macro can't
|
|
161
|
+
/// resolve names, so this only reports true when the clause is empty of anything that could be a
|
|
162
|
+
/// base class the author chose deliberately. Conservative by design: a wrong guess here would emit a
|
|
163
|
+
/// fix-it that doesn't compile.
|
|
164
|
+
private func inheritsOnlyProtocolsByConvention(_ clause: InheritanceClauseSyntax) -> Bool {
|
|
165
|
+
// Any entry at all could be a superclass, so only offer the prepend when the author wrote
|
|
166
|
+
// something that is definitely not one: a known protocol-shaped name from the standard library.
|
|
167
|
+
let knownProtocols: Set<String> = [
|
|
168
|
+
"Identifiable", "Equatable", "Hashable", "Codable", "Encodable", "Decodable",
|
|
169
|
+
"Sendable", "CustomStringConvertible", "ObservableObject",
|
|
170
|
+
]
|
|
171
|
+
return clause.inheritedTypes.allSatisfy { entry in
|
|
172
|
+
guard let name = baseIdentifier(of: entry.type) else {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
return knownProtocols.contains(name)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private struct ExpoViewDiagnosticMessage: DiagnosticMessage {
|
|
180
|
+
let message: String
|
|
181
|
+
let diagnosticID: MessageID
|
|
182
|
+
let severity: DiagnosticSeverity = .error
|
|
183
|
+
|
|
184
|
+
init(_ message: String, id: String) {
|
|
185
|
+
self.message = message
|
|
186
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private struct ExpoViewFixItMessage: FixItMessage {
|
|
191
|
+
let message: String
|
|
192
|
+
let fixItID: MessageID
|
|
193
|
+
|
|
194
|
+
init(_ message: String, id: String) {
|
|
195
|
+
self.message = message
|
|
196
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Describes an existing `Props` member on the class, for the collision diagnostic, or `nil` when the
|
|
201
|
+
/// name is free. Covers the shapes that would clash with the synthesized typealias: another typealias,
|
|
202
|
+
/// and a nested type of any kind.
|
|
203
|
+
private func existingPropsMemberKind(in classDecl: ClassDeclSyntax) -> String? {
|
|
204
|
+
for member in classDecl.memberBlock.members {
|
|
205
|
+
let decl = member.decl
|
|
206
|
+
if let typealiasDecl = decl.as(TypeAliasDeclSyntax.self), typealiasDecl.name.text == "Props" {
|
|
207
|
+
return "typealias"
|
|
208
|
+
}
|
|
209
|
+
if let structDecl = decl.as(StructDeclSyntax.self), structDecl.name.text == "Props" {
|
|
210
|
+
return "struct"
|
|
211
|
+
}
|
|
212
|
+
if let nestedClass = decl.as(ClassDeclSyntax.self), nestedClass.name.text == "Props" {
|
|
213
|
+
return "class"
|
|
214
|
+
}
|
|
215
|
+
if let enumDecl = decl.as(EnumDeclSyntax.self), enumDecl.name.text == "Props" {
|
|
216
|
+
return "enum"
|
|
217
|
+
}
|
|
218
|
+
if let actorDecl = decl.as(ActorDeclSyntax.self), actorDecl.name.text == "Props" {
|
|
219
|
+
return "actor"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return nil
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Base classes an `@ExpoView` may inherit from. The check is syntactic, matching the name in the
|
|
226
|
+
/// class's own inheritance clause: both `: ExpoView` and a qualified `: ExpoModulesCore.ExpoView`
|
|
227
|
+
/// pass, since `baseIdentifier` reads the trailing name.
|
|
228
|
+
///
|
|
229
|
+
/// Indirect inheritance does not: a view whose superclass is itself an `ExpoView` subclass
|
|
230
|
+
/// (`class Base: ExpoView` then `class CardView: Base`) is rejected, because a macro can't resolve
|
|
231
|
+
/// the superclass to check. `@SharedObject` has the same limitation, through the same helper. The
|
|
232
|
+
/// workaround is to apply `@ExpoView` to the class that names `ExpoView` directly.
|
|
233
|
+
private let expoViewBaseClassNames: Set<String> = ["ExpoView"]
|
|
234
|
+
|
|
235
|
+
/// The attribute's single generic argument, verbatim (`@ExpoView<CardProps>` → `"CardProps"`), or
|
|
236
|
+
/// `nil` when the attribute carries no generic argument clause.
|
|
237
|
+
///
|
|
238
|
+
/// The type is available syntactically, with no type resolution. Two spellings carry it, mirroring
|
|
239
|
+
/// `baseIdentifier`'s handling of the inheritance clause: a bare `@ExpoView<CardProps>` parses as an
|
|
240
|
+
/// `IdentifierTypeSyntax`, and a qualified `@ExpoModulesCore.ExpoView<CardProps>` as a
|
|
241
|
+
/// `MemberTypeSyntax`. Both have a `genericArgumentClause`, and both are legal at the use site, so
|
|
242
|
+
/// reading only the bare form would reject valid code while claiming the author omitted the argument.
|
|
243
|
+
///
|
|
244
|
+
/// Only the first argument is read: core's declaration has exactly one parameter, so a second is
|
|
245
|
+
/// already a compile error at the use site ("specialized with too many type parameters").
|
|
246
|
+
private func genericArgument(of attribute: AttributeSyntax) -> String? {
|
|
247
|
+
let arguments: GenericArgumentListSyntax?
|
|
248
|
+
if let identifier = attribute.attributeName.as(IdentifierTypeSyntax.self) {
|
|
249
|
+
arguments = identifier.genericArgumentClause?.arguments
|
|
250
|
+
} else if let member = attribute.attributeName.as(MemberTypeSyntax.self) {
|
|
251
|
+
arguments = member.genericArgumentClause?.arguments
|
|
252
|
+
} else {
|
|
253
|
+
arguments = nil
|
|
254
|
+
}
|
|
255
|
+
guard let first = arguments?.first, case .type(let type) = first.argument else {
|
|
256
|
+
return nil
|
|
257
|
+
}
|
|
258
|
+
return type.trimmedDescription
|
|
259
|
+
}
|
|
@@ -3,8 +3,8 @@ import SwiftSyntax
|
|
|
3
3
|
import SwiftSyntaxMacros
|
|
4
4
|
|
|
5
5
|
/// Marker macro applied to module / shared-object members that should be exposed to JavaScript.
|
|
6
|
-
/// `@ExpoModule` and `@SharedObject` discover declarations carrying this attribute and
|
|
7
|
-
///
|
|
6
|
+
/// `@ExpoModule` and `@SharedObject` discover declarations carrying this attribute and bind each one
|
|
7
|
+
/// directly into the JS object, with the decode-call-encode body inlined into the binding; that part
|
|
8
8
|
/// of the expansion lives in those macros.
|
|
9
9
|
///
|
|
10
10
|
/// On its own, `@JS` emits one thing: a never-called peer that asserts each type crossing the JS
|
|
@@ -135,10 +135,17 @@ internal func classListArgument(of attribute: AttributeSyntax, label: String) ->
|
|
|
135
135
|
return array.elements.compactMap { element -> String? in
|
|
136
136
|
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
137
137
|
memberAccess.declName.baseName.text == "self",
|
|
138
|
-
let base = memberAccess.base
|
|
138
|
+
let base = memberAccess.base else {
|
|
139
139
|
return nil
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
// A bare `CardView.self` has a `DeclReferenceExprSyntax` base; a qualified
|
|
142
|
+
// `Outer.CardView.self` has a `MemberAccessExprSyntax` one. Both name a type the generated
|
|
143
|
+
// code can spell, so take the base verbatim rather than only its last component: dropping the
|
|
144
|
+
// qualified form would silently omit the entry, and the type would never be registered.
|
|
145
|
+
if base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self) {
|
|
146
|
+
return base.trimmedDescription
|
|
147
|
+
}
|
|
148
|
+
return nil
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
return []
|
|
@@ -370,6 +377,21 @@ internal func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
|
370
377
|
}
|
|
371
378
|
}
|
|
372
379
|
|
|
380
|
+
/// True if the variable declaration carries a modifier that excludes it from being a stored property
|
|
381
|
+
/// of the surface: `static`, `class` (type-level storage), `private`, `fileprivate`, or `lazy`.
|
|
382
|
+
/// Shared by `@Record` and `@ViewProps`, which apply the same "every stored property counts" rule.
|
|
383
|
+
internal func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
384
|
+
for modifier in modifiers {
|
|
385
|
+
switch modifier.name.tokenKind {
|
|
386
|
+
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
387
|
+
return true
|
|
388
|
+
default:
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
|
|
373
395
|
/// True if the type's inheritance clause already lists a protocol with the given name. Matches
|
|
374
396
|
/// either the bare identifier (`Record`) or a qualified member access ending in the name
|
|
375
397
|
/// (`ExpoModulesCore.Record`). Used by the extension macros to skip a conformance the author already
|
|
@@ -462,22 +462,6 @@ private func initializerParameterLabels(of declaration: some DeclGroupSyntax) ->
|
|
|
462
462
|
return signatures
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
/**
|
|
466
|
-
True if the variable declaration carries a modifier that excludes it from being a property:
|
|
467
|
-
`static`, `class` (type-level storage), `private`, `fileprivate`, or `lazy`.
|
|
468
|
-
*/
|
|
469
|
-
private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
470
|
-
for modifier in modifiers {
|
|
471
|
-
switch modifier.name.tokenKind {
|
|
472
|
-
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
473
|
-
return true
|
|
474
|
-
default:
|
|
475
|
-
continue
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return false
|
|
479
|
-
}
|
|
480
|
-
|
|
481
465
|
/**
|
|
482
466
|
True if the class declaration has any inheritance clause. Used as a heuristic for
|
|
483
467
|
whether the superclass also conforms to `Record` and provides the synthesized methods;
|