expo-modules-macros 0.25.0 → 0.27.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/AGENTS.md +8 -8
- package/README.md +40 -3
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.swift +10 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/CLI.swift +45 -2
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +120 -0
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +183 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ExportedSurface.swift +3 -2
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ResolveRefs.swift +9 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/SurfaceVisitor.swift +10 -3
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/TupleTypeAliases.swift +126 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/TypeNode.swift +47 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/SourceScan.swift +8 -2
- package/apple/Sources/ExpoModulesMacros/Expand/MacroExpander.swift +484 -0
- package/apple/Sources/ExpoModulesMacros/{EventMacro.swift → Macros/EventMacro.swift} +23 -10
- package/apple/Sources/ExpoModulesMacros/{ExpoModuleMacro.swift → Macros/ExpoModuleMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{JSMacro.swift → Macros/JSMacro.swift} +71 -54
- package/apple/Sources/ExpoModulesMacros/{RecordMacro.swift → Macros/RecordMacro.swift} +18 -5
- package/apple/Sources/ExpoModulesMacros/{SharedObjectMacro.swift → Macros/SharedObjectMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{UnionMacro.swift → Macros/UnionMacro.swift} +16 -5
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +9 -14
- package/apple/Sources/ExpoModulesMacros/ProvidedMacros.swift +31 -0
- package/apple/Sources/ExpoModulesMacros/{ClosureArguments.swift → Support/ClosureArguments.swift} +30 -4
- package/apple/Sources/ExpoModulesMacros/{DecorateModuleBuilder.swift → Support/DecorateModuleBuilder.swift} +5 -1
- package/apple/Sources/ExpoModulesMacros/{MacroHelpers.swift → Support/MacroHelpers.swift} +11 -4
- package/apple/Sources/ExpoModulesMacros/Support/TupleTypes.swift +272 -0
- package/apple/Sources/ExpoModulesMacros/Support/TypeAliases.swift +75 -0
- package/apple/Sources/ExpoModulesMacros/{TypeConformanceAssertion.swift → Support/TypeConformanceAssertion.swift} +104 -41
- package/build/index.d.ts +31 -1
- package/build/index.js +34 -0
- package/build/types.d.ts +77 -2
- package/build/types.js +4 -3
- package/package.json +5 -5
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/Availability.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ScanExports.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Modules → ExpoModulesCLI/Commands/ScanModules}/ScanModules.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/Detection.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/DetectionVisitor.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/ScanBuildConfiguration.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/StandardStreams.swift +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoModulesOptimizedMacro.swift → Macros/ExpoModulesOptimizedMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoViewMacro.swift → Macros/ExpoViewMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ViewPropsMacro.swift → Macros/ViewPropsMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{FreeFormTypes.swift → Support/FreeFormTypes.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSConstructor.swift → Support/JSConstructor.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSNameCollisions.swift → Support/JSNameCollisions.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{OptimizedFunctionHelpers.swift → Support/OptimizedFunctionHelpers.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{Receiver.swift → Support/Receiver.swift} +0 -0
package/AGENTS.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
## Layout
|
|
6
6
|
|
|
7
7
|
- `apple/Package.swift`: the SwiftPM package. The test targets are declared only when `apple/Tests` exists, because the published npm package leaves it out.
|
|
8
|
-
- `apple/Sources/ExpoModulesMacros`: the
|
|
9
|
-
- `apple/Sources/
|
|
8
|
+
- `apple/Sources/ExpoModulesMacros`: the compiler plugin executable. `Plugin.swift` registers the macros of `ProvidedMacro.all` in `providingMacros` and holds the entry point that sends any invocation with arguments to the CLI. `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`, and `Expand/MacroExpander.swift` runs the macros in process for `expand`, because only this executable target can see them.
|
|
9
|
+
- `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 the report types and the output of `expand`, which gets the expansion from the executable as a `MacroExpander`), and `Core/` holds the shared parsing, `#if` evaluation and output.
|
|
10
10
|
- `apple/Sources/ExpoModulesOptimized`: declarations for the `@OptimizedFunction` macro.
|
|
11
|
-
- `apple/Tests`: `ExpoModulesMacrosTests` (expansion tests) and `
|
|
11
|
+
- `apple/Tests`: `ExpoModulesMacrosTests` (expansion tests) and `ExpoModulesCLITests`.
|
|
12
12
|
- `platforms/`: the Windows packages `@expo/modules-macros-win32-x64` and `-arm64`, and the Linux packages `@expo/modules-macros-linux-x64` and `-arm64` (a `package.json` with `os` and `cpu`, plus `libc` on Linux, a README and a copy of `LICENSE`). The Publish workflow runs `scripts/set-platform-versions.js` to give them the version of this package. Their `ExpoModulesMacros.exe` (Windows) or `ExpoModulesMacros` (Linux) is written there by `pnpm run build` on that platform and isn't committed.
|
|
13
13
|
- `apple/PodTests`: a stub test so that `expo/expo` native tests can install this package as a pod. Do not add real tests here.
|
|
14
14
|
- `src/`: the TypeScript wrapper around the scanner CLI. `types.ts` mirrors the Swift `Codable` output types by hand.
|
|
@@ -48,21 +48,21 @@ CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.gi
|
|
|
48
48
|
- Write new doc comments as `///` line comments, not `/** */` blocks.
|
|
49
49
|
- Tests use Swift Testing (`import Testing`, `@Test`) with backtick-quoted descriptive names, for example ``func `Prunes .build, Pods, .git, and node_modules directories`()``.
|
|
50
50
|
- Macro tests compare full expansions with `assertMacroExpansion` from `SwiftSyntaxMacrosGenericTestSupport`, using `indentationWidth: .spaces(2)`. Each test file wraps it in a local `assertExpansion` helper that passes `recordIssue` (`IssueRecording.swift`) as the failure handler.
|
|
51
|
-
-
|
|
51
|
+
- CLI tests reach internal API with `@testable import ExpoModulesCLI`. Do not make its types `public` only for tests.
|
|
52
52
|
- `@JS` applies to functions, properties and initializers. Do not write code or comments that assume it marks only functions.
|
|
53
53
|
- Code comments describe the code as it is. Do not refer to open PRs, branches or planned work.
|
|
54
54
|
|
|
55
55
|
## Changing macros
|
|
56
56
|
|
|
57
57
|
- 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.
|
|
58
|
-
- The scanner only parses files that pass the pre-filter, `MacroPrefilter` in `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.
|
|
59
|
-
- A new macro needs: the implementation in `apple/Sources/ExpoModulesMacros`, an entry in `
|
|
58
|
+
- 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.
|
|
59
|
+
- A new macro needs: the implementation in `apple/Sources/ExpoModulesMacros/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`.
|
|
60
60
|
|
|
61
61
|
## Contracts with other packages
|
|
62
62
|
|
|
63
|
-
- **Macro declarations live in `expo-modules-core`** (`ios/Core/ExpoModulesMacros.swift` in `expo/expo`), as `#externalMacro(module: "ExpoModulesMacros", type: ...)`. Adding a macro, renaming a macro type or changing a macro's signature needs a matching edit there, and the type must be listed in `
|
|
63
|
+
- **Macro declarations live in `expo-modules-core`** (`ios/Core/ExpoModulesMacros.swift` in `expo/expo`), as `#externalMacro(module: "ExpoModulesMacros", type: ...)`. Adding a macro, renaming a macro type or changing a macro's signature needs a matching edit there, and the type must be listed in `ProvidedMacro.all` in `ProvidedMacros.swift`, with the same attribute name and extension conformances as the declaration.
|
|
64
64
|
- **Generated code calls `expo-modules-core` API.** An expansion that uses a new core symbol only works with a core version that has it.
|
|
65
|
-
- **Scanner output is versioned.** `scanModulesSchemaVersion` (`
|
|
65
|
+
- **Scanner output is versioned.** `scanModulesSchemaVersion` (`Commands/ScanModules/ScanModules.swift`), `scanExportsSchemaVersion` (`Commands/ScanExports/ExportedSurface.swift`) and `expandSchemaVersion` (`Commands/Expand/ExpandReport.swift`) change independently. When the output shape of a command changes, bump its version and update the matching `SUPPORTED_*_SCHEMA_VERSION` constant and mirror types in `src/types.ts`. `expo-modules-autolinking` also checks the `scan-modules` version.
|
|
66
66
|
- **The binary path is part of the contract.** `expo-modules-autolinking` resolves this package from `expo-modules-core` and passes `-load-plugin-executable <package>/apple/<binary>#ExpoModulesMacros` to the compiler. The scanner wrapper in autolinking uses the same path, and `expo-modules-cli` uses `getScannerBinaryPath()`. On Windows the binary is `ExpoModulesMacros.exe` in `@expo/modules-macros-win32-<arch>`, and on Linux `ExpoModulesMacros` in `@expo/modules-macros-linux-<arch>`. Renaming or moving a binary needs a matching change in `expo/expo`.
|
|
67
67
|
|
|
68
68
|
## Releases
|
package/README.md
CHANGED
|
@@ -25,12 +25,14 @@ 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.
|
|
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
|
|
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. Outside a module or shared object, 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. 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 or the module's default isolation, 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
29
|
- **`@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.
|
|
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 module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses.
|
|
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. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
32
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.
|
|
33
33
|
|
|
34
|
+
A tuple can cross the boundary as a `@JS` argument, result or property, a `@Record` property, a `@Union` payload, an `@Event` payload, or a parameter or result of a closure argument that isn't `async`. An unlabeled tuple such as `(Int, String)` maps to an array with one value per element, and a labeled tuple such as `(x: Double, y: Double)` to an object with one property per label. A tuple must have two or more elements, all labeled or none, and each element must be convertible. Tuples nest in other tuples, optionals, arrays and `String`-keyed dictionaries. `@ExpoModule` and `@SharedObject` also resolve a type alias of a tuple, such as `typealias Span = (start: Int, end: Int)`, when the class body declares it. A macro sees only the declaration it is attached to, so an alias declared anywhere else doesn't resolve.
|
|
35
|
+
|
|
34
36
|
How that looks in a module:
|
|
35
37
|
|
|
36
38
|
```swift
|
|
@@ -72,22 +74,57 @@ ExpoModulesMacros <subcommand> [options] <path> [<path> ...]
|
|
|
72
74
|
subcommands:
|
|
73
75
|
scan-modules fast scan for top-level @ExpoModule types (autolinking)
|
|
74
76
|
scan-exports deep scan of the full JS-exported surface (type generation)
|
|
77
|
+
expand print the code that the macros generate, and the macro diagnostics
|
|
75
78
|
|
|
76
79
|
options (scan-modules only):
|
|
77
80
|
--define <flag> treat a conditional compilation flag as set; repeatable
|
|
81
|
+
|
|
82
|
+
options (expand only):
|
|
83
|
+
--symbol <name> only a type ('MyModule', 'MyModule.Options') or a member ('MyModule.greet'); repeatable
|
|
84
|
+
--diagnostics-only only the diagnostics, followed by a summary
|
|
85
|
+
--json a JSON report instead of text
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For the scans, each path is a `.swift` file or a directory, scanned recursively for `.swift` files. Both scans 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.
|
|
89
|
+
|
|
90
|
+
## Expand
|
|
91
|
+
|
|
92
|
+
`expand` shows the code that the macros generate, as "Expand Macro" does in Xcode, but in a terminal and without a build. It takes `.swift` files and directories, runs the macros of this executable on them in process with swift-syntax, and prints the expanded source to stdout. The syntax errors and the macro diagnostics go to stderr in the compiler's `<file>:<line>:<column>: error: <message>` format, and the exit code is `1` when there is an error. With `--diagnostics-only`, it prints only the diagnostics, so it can check a module in CI or for an agent.
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
ExpoModulesMacros expand ios/HapticsModule.swift
|
|
96
|
+
ExpoModulesMacros expand --symbol HapticsModule.impactAsync ios/HapticsModule.swift
|
|
97
|
+
ExpoModulesMacros expand --diagnostics-only ios/
|
|
78
98
|
```
|
|
79
99
|
|
|
80
|
-
|
|
100
|
+
A file is always expanded and keeps its path as it was passed. A directory is walked like the scans walk it, skipping dependency, build, test and example directories, and only its files that use one of the macros are expanded, with their absolute paths.
|
|
101
|
+
|
|
102
|
+
`--symbol` limits the output to one declaration:
|
|
103
|
+
|
|
104
|
+
- A type (`HapticsModule`, `HapticsModule.ImpactStyle`) prints the expanded type and the extensions that its macros add.
|
|
105
|
+
- A member (`HapticsModule.impactAsync`, `HapticsModule.init`) prints its type, expanded with only the declarations of that name (all overloads). So `_decorateModule` holds only the code for that member.
|
|
106
|
+
|
|
107
|
+
With `--symbol`, only the diagnostics inside those declarations are printed.
|
|
108
|
+
|
|
109
|
+
With `--json`, `expand` prints an `ExpandResult` with its own `schemaVersion`, and the exit code is `0` also with error diagnostics, because the report holds them. A missing path, a file that isn't Swift, or a symbol that no file declares is a usage error (`2`) in both modes.
|
|
110
|
+
|
|
111
|
+
The macros only read the syntax tree, so the expansion is what the compiler gets, with two differences:
|
|
112
|
+
|
|
113
|
+
- The names that a macro makes unique (`makeUniqueName`) can differ.
|
|
114
|
+
- The compiler passes an extension macro only the conformances that the type doesn't have yet. `expand` passes all of them, and the macros skip the ones in the inheritance clause, so the result differs only for a conformance inherited from somewhere else.
|
|
115
|
+
|
|
116
|
+
Errors that need type checking, for example the conformance assertions in `_assertTypesConformance`, still need a build.
|
|
81
117
|
|
|
82
118
|
# TypeScript wrapper
|
|
83
119
|
|
|
84
120
|
Node consumers can call the scanner through this package instead of locating the binary and shelling out themselves:
|
|
85
121
|
|
|
86
122
|
```ts
|
|
87
|
-
import { scanModules, scanExports } from 'expo-modules-macros';
|
|
123
|
+
import { expandMacros, scanModules, scanExports } from 'expo-modules-macros';
|
|
88
124
|
|
|
89
125
|
const { modules, warnings } = await scanModules(['ios/'], { defines: ['DEBUG'], timeout: 30_000 });
|
|
90
126
|
const { exports } = await scanExports(['ios/']);
|
|
127
|
+
const { files } = await expandMacros(['ios/HapticsModule.swift'], { symbols: ['HapticsModule.impactAsync'] });
|
|
91
128
|
```
|
|
92
129
|
|
|
93
130
|
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 or an exceeded `timeout` into a `ScannerError`. The result types are hand-written mirrors of the Swift `Codable` types, which is what the version check guards against drifting.
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
package/apple/Package.swift
CHANGED
|
@@ -20,13 +20,18 @@ let package = Package(
|
|
|
20
20
|
.macro(
|
|
21
21
|
name: "ExpoModulesMacros",
|
|
22
22
|
dependencies: [
|
|
23
|
-
"
|
|
23
|
+
"ExpoModulesCLI",
|
|
24
24
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
|
25
25
|
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
|
26
|
+
// For `expand`, which runs the macros in process.
|
|
27
|
+
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
|
|
28
|
+
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
29
|
+
.product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
|
|
30
|
+
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
|
|
26
31
|
]
|
|
27
32
|
),
|
|
28
33
|
.target(
|
|
29
|
-
name: "
|
|
34
|
+
name: "ExpoModulesCLI",
|
|
30
35
|
dependencies: [
|
|
31
36
|
.product(name: "SwiftSyntax", package: "swift-syntax"),
|
|
32
37
|
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
@@ -44,15 +49,16 @@ if FileManager.default.fileExists(atPath: Context.packageDirectory + "/Tests") {
|
|
|
44
49
|
name: "ExpoModulesMacrosTests",
|
|
45
50
|
dependencies: [
|
|
46
51
|
"ExpoModulesMacros",
|
|
52
|
+
"ExpoModulesCLI",
|
|
47
53
|
.product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"),
|
|
48
54
|
]
|
|
49
55
|
)
|
|
50
56
|
)
|
|
51
57
|
package.targets.append(
|
|
52
58
|
.testTarget(
|
|
53
|
-
name: "
|
|
59
|
+
name: "ExpoModulesCLITests",
|
|
54
60
|
dependencies: [
|
|
55
|
-
"
|
|
61
|
+
"ExpoModulesCLI",
|
|
56
62
|
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
57
63
|
]
|
|
58
64
|
)
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
/// Subcommands:
|
|
9
9
|
/// scan-modules <path>... fast: top-level `@ExpoModule` types, for autolinking
|
|
10
10
|
/// scan-exports <path>... deep: full JS-exported surface, for TS type generation
|
|
11
|
+
/// expand <path>... the code that the macros generate, and their diagnostics
|
|
11
12
|
public enum ScannerCLI {
|
|
12
13
|
/// Runs the CLI for the given arguments (argv without the executable path) and returns the
|
|
13
14
|
/// process exit code: `0` on success, `1` if encoding the report fails, `2` on a usage error.
|
|
14
|
-
|
|
15
|
+
/// `expand` also returns `1` for an error diagnostic (see `Scanner.runExpand`).
|
|
16
|
+
///
|
|
17
|
+
/// `expander` expands the macros for `expand`. The plugin executable passes it, because the macro
|
|
18
|
+
/// implementations live there; without it, `expand` is a usage error.
|
|
19
|
+
public static func run(arguments: [String], expander: MacroExpander? = nil) -> Int32 {
|
|
15
20
|
// `-h`/`--help` anywhere is treated as a help request: print usage to stdout and exit 0.
|
|
16
21
|
if arguments.contains(where: { $0 == "-h" || $0 == "--help" }) {
|
|
17
22
|
writeToStandardOutput(usageText)
|
|
@@ -26,6 +31,7 @@ public enum ScannerCLI {
|
|
|
26
31
|
var paths: [String] = []
|
|
27
32
|
var platform: String?
|
|
28
33
|
var defines: [String] = []
|
|
34
|
+
var expandOptions = ExpandOptions()
|
|
29
35
|
|
|
30
36
|
var rest = arguments.dropFirst().makeIterator()
|
|
31
37
|
while let argument = rest.next() {
|
|
@@ -40,11 +46,25 @@ public enum ScannerCLI {
|
|
|
40
46
|
return usageError("--define requires a value")
|
|
41
47
|
}
|
|
42
48
|
defines.append(value)
|
|
49
|
+
case "--symbol":
|
|
50
|
+
guard let value = rest.next() else {
|
|
51
|
+
return usageError("--symbol requires a value")
|
|
52
|
+
}
|
|
53
|
+
expandOptions.symbols.append(value)
|
|
54
|
+
case "--json":
|
|
55
|
+
expandOptions.json = true
|
|
56
|
+
case "--diagnostics-only":
|
|
57
|
+
expandOptions.diagnosticsOnly = true
|
|
43
58
|
default:
|
|
44
59
|
paths.append(argument)
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
|
|
63
|
+
// The scans print JSON in any case and have no symbols to filter.
|
|
64
|
+
if subcommand != "expand" && expandOptions != ExpandOptions() {
|
|
65
|
+
return usageError("--symbol, --json and --diagnostics-only are options of expand")
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
switch subcommand {
|
|
49
69
|
case "scan-modules":
|
|
50
70
|
// scan-modules is platform-agnostic: each reported module carries the platforms that include
|
|
@@ -70,6 +90,19 @@ public enum ScannerCLI {
|
|
|
70
90
|
}
|
|
71
91
|
return Scanner.runExports(paths: paths)
|
|
72
92
|
|
|
93
|
+
case "expand":
|
|
94
|
+
guard platform == nil, defines.isEmpty else {
|
|
95
|
+
return usageError("expand does not support --platform or --define")
|
|
96
|
+
}
|
|
97
|
+
guard !paths.isEmpty else {
|
|
98
|
+
return usageError("expand requires at least one path")
|
|
99
|
+
}
|
|
100
|
+
guard let expander else {
|
|
101
|
+
return usageError("expand is only available in the macro plugin executable")
|
|
102
|
+
}
|
|
103
|
+
expandOptions.paths = paths
|
|
104
|
+
return Scanner.runExpand(options: expandOptions, expander: expander)
|
|
105
|
+
|
|
73
106
|
default:
|
|
74
107
|
return usageError("unknown subcommand '\(subcommand)'")
|
|
75
108
|
}
|
|
@@ -80,7 +113,7 @@ public enum ScannerCLI {
|
|
|
80
113
|
/// (the `ExpoModulesMacros` shipped in the package, or a locally built `ExpoModulesMacros-tool`).
|
|
81
114
|
private var toolName: String {
|
|
82
115
|
guard let path = CommandLine.arguments.first else {
|
|
83
|
-
return "
|
|
116
|
+
return "ExpoModulesMacros"
|
|
84
117
|
}
|
|
85
118
|
// Windows paths may use either separator.
|
|
86
119
|
return String(path.split(whereSeparator: { $0 == "/" || $0 == "\\" }).last ?? Substring(path))
|
|
@@ -93,11 +126,21 @@ private var usageText: String {
|
|
|
93
126
|
subcommands:
|
|
94
127
|
scan-modules fast scan for top-level @ExpoModule types (autolinking)
|
|
95
128
|
scan-exports deep scan of the full JS-exported surface (type generation)
|
|
129
|
+
expand print the code that the macros generate in the given .swift files (and in the
|
|
130
|
+
files of the given directories that use a macro), and the macro diagnostics
|
|
131
|
+
on stderr; exits with 1 when there is an error
|
|
96
132
|
|
|
97
133
|
options (scan-modules only):
|
|
98
134
|
--define <flag> treat a conditional compilation flag (e.g. DEBUG) as set when resolving
|
|
99
135
|
each module's 'platforms' list; repeatable
|
|
100
136
|
|
|
137
|
+
options (expand only):
|
|
138
|
+
--symbol <name> print only the expansion of a type ('MyModule', 'MyModule.Options') or a
|
|
139
|
+
member ('MyModule.greet'), and only its diagnostics; repeatable
|
|
140
|
+
--diagnostics-only print only the diagnostics, followed by a summary
|
|
141
|
+
--json print a JSON report with the expansions and diagnostics instead; exits with
|
|
142
|
+
0 also when there is an error
|
|
143
|
+
|
|
101
144
|
options:
|
|
102
145
|
-h, --help print this help and exit
|
|
103
146
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/// Version of the `expand --json` output shape. Independent of the scan schema versions: bump it when
|
|
2
|
+
/// the shape of `ExpandResult` changes, together with `SUPPORTED_EXPAND_SCHEMA_VERSION` and the mirror
|
|
3
|
+
/// types in `src/types.ts`.
|
|
4
|
+
public let expandSchemaVersion = 1
|
|
5
|
+
|
|
6
|
+
/// The JSON report of `expand --json`.
|
|
7
|
+
public struct ExpandResult: Codable, Equatable, Sendable {
|
|
8
|
+
public let schemaVersion: Int
|
|
9
|
+
|
|
10
|
+
/// One entry per expanded file: the files in argument order, the files of a directory sorted.
|
|
11
|
+
public let files: [ExpandedFile]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// What the macros generate for one source file, and what they report about it.
|
|
15
|
+
public struct ExpandedFile: Codable, Equatable, Sendable {
|
|
16
|
+
/// The path as it was passed on the command line, or the absolute path of a file found in a
|
|
17
|
+
/// directory.
|
|
18
|
+
public let file: String
|
|
19
|
+
|
|
20
|
+
/// Without `--symbol`, one expansion of the whole file. With it, one expansion per symbol found in
|
|
21
|
+
/// this file, in the order of the options. Empty with `--diagnostics-only`.
|
|
22
|
+
public let expansions: [Expansion]
|
|
23
|
+
|
|
24
|
+
/// The syntax errors in the file and the diagnostics of the macros, in source order. With
|
|
25
|
+
/// `--symbol`, only those inside the declarations of the symbols.
|
|
26
|
+
public let diagnostics: [ExpansionDiagnostic]
|
|
27
|
+
|
|
28
|
+
public init(file: String, expansions: [Expansion], diagnostics: [ExpansionDiagnostic]) {
|
|
29
|
+
self.file = file
|
|
30
|
+
self.expansions = expansions
|
|
31
|
+
self.diagnostics = diagnostics
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Expanded source code.
|
|
36
|
+
public struct Expansion: Codable, Equatable, Sendable {
|
|
37
|
+
/// The `--symbol` value this expansion is for, or `nil` for the whole file.
|
|
38
|
+
public let symbol: String?
|
|
39
|
+
|
|
40
|
+
public let source: String
|
|
41
|
+
|
|
42
|
+
public init(symbol: String?, source: String) {
|
|
43
|
+
self.symbol = symbol
|
|
44
|
+
self.source = source
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// A diagnostic, at its location in the original source file. A diagnostic that a macro attaches to
|
|
49
|
+
/// the code it generates has the location of the macro attribute.
|
|
50
|
+
public struct ExpansionDiagnostic: Codable, Equatable, Sendable {
|
|
51
|
+
public enum Severity: String, Codable, Sendable {
|
|
52
|
+
case error
|
|
53
|
+
case warning
|
|
54
|
+
case note
|
|
55
|
+
case remark
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public let severity: Severity
|
|
59
|
+
public let message: String
|
|
60
|
+
|
|
61
|
+
/// 1-based, as in the compiler's output. The column counts UTF-8 bytes.
|
|
62
|
+
public let line: Int
|
|
63
|
+
public let column: Int
|
|
64
|
+
|
|
65
|
+
public let notes: [ExpansionNote]
|
|
66
|
+
|
|
67
|
+
/// The messages of the Fix-Its, for example `replace 'var' with 'let'`.
|
|
68
|
+
public let fixIts: [String]
|
|
69
|
+
|
|
70
|
+
public init(
|
|
71
|
+
severity: Severity,
|
|
72
|
+
message: String,
|
|
73
|
+
line: Int,
|
|
74
|
+
column: Int,
|
|
75
|
+
notes: [ExpansionNote] = [],
|
|
76
|
+
fixIts: [String] = []
|
|
77
|
+
) {
|
|
78
|
+
self.severity = severity
|
|
79
|
+
self.message = message
|
|
80
|
+
self.line = line
|
|
81
|
+
self.column = column
|
|
82
|
+
self.notes = notes
|
|
83
|
+
self.fixIts = fixIts
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// A note attached to a diagnostic, at its own location.
|
|
88
|
+
public struct ExpansionNote: Codable, Equatable, Sendable {
|
|
89
|
+
public let message: String
|
|
90
|
+
public let line: Int
|
|
91
|
+
public let column: Int
|
|
92
|
+
|
|
93
|
+
public init(message: String, line: Int, column: Int) {
|
|
94
|
+
self.message = message
|
|
95
|
+
self.line = line
|
|
96
|
+
self.column = column
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Expands the macros in one source file. The macro implementations live in the plugin executable,
|
|
101
|
+
/// which this library can't depend on, so the executable passes this in (see `ScannerCLI`).
|
|
102
|
+
public struct MacroExpander {
|
|
103
|
+
/// The attribute names of the macros, such as `ExpoModule`. A file found in a directory is expanded
|
|
104
|
+
/// only when it contains one of them after an `@`.
|
|
105
|
+
let attributeNames: Set<String>
|
|
106
|
+
|
|
107
|
+
/// - Parameters:
|
|
108
|
+
/// - source: The contents of the file.
|
|
109
|
+
/// - file: The path of the file, for the diagnostics and the expansion context.
|
|
110
|
+
/// - symbols: The `--symbol` values. Empty to expand the whole file.
|
|
111
|
+
let expand: (_ source: String, _ file: String, _ symbols: [String]) -> ExpandedFile
|
|
112
|
+
|
|
113
|
+
public init(
|
|
114
|
+
attributeNames: Set<String>,
|
|
115
|
+
expand: @escaping (_ source: String, _ file: String, _ symbols: [String]) -> ExpandedFile
|
|
116
|
+
) {
|
|
117
|
+
self.attributeNames = attributeNames
|
|
118
|
+
self.expand = expand
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#if canImport(FoundationEssentials)
|
|
2
|
+
import FoundationEssentials
|
|
3
|
+
#else
|
|
4
|
+
import Foundation
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
/// The options of `expand`, parsed by `ScannerCLI`.
|
|
8
|
+
struct ExpandOptions: Equatable {
|
|
9
|
+
var paths: [String] = []
|
|
10
|
+
var symbols: [String] = []
|
|
11
|
+
var json = false
|
|
12
|
+
var diagnosticsOnly = false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
extension Scanner {
|
|
16
|
+
/// Runs `expand`: expands the macros in each file with `expander` and prints the result. A
|
|
17
|
+
/// directory stands for the `.swift` files in it that use one of the macros.
|
|
18
|
+
///
|
|
19
|
+
/// As text, the expanded source goes to stdout and the diagnostics go to stderr in the
|
|
20
|
+
/// `<file>:<line>:<column>: <severity>: <message>` format of the compiler. The exit code is `1`
|
|
21
|
+
/// when there is an error diagnostic. With `--json`, the report goes to stdout and the exit code
|
|
22
|
+
/// is `0` also with error diagnostics, because the report holds them; `1` then means that encoding
|
|
23
|
+
/// failed. An input that can't be expanded (a missing path, a file that isn't Swift, a symbol that
|
|
24
|
+
/// no file declares) is a usage error, `2`.
|
|
25
|
+
static func runExpand(options: ExpandOptions, expander: MacroExpander) -> Int32 {
|
|
26
|
+
let result: ExpandResult
|
|
27
|
+
switch expandFiles(options: options, expander: expander) {
|
|
28
|
+
case .success(let expanded):
|
|
29
|
+
result = expanded
|
|
30
|
+
case .failure(let error):
|
|
31
|
+
writeToStandardError("error: \(error.message)\n")
|
|
32
|
+
return 2
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if options.json {
|
|
36
|
+
do {
|
|
37
|
+
let encoder = JSONEncoder()
|
|
38
|
+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
39
|
+
let data = try encoder.encode(result)
|
|
40
|
+
writeToStandardOutput(String(decoding: data, as: UTF8.self) + "\n")
|
|
41
|
+
return 0
|
|
42
|
+
} catch {
|
|
43
|
+
writeToStandardError("error: failed to encode results: \(error)\n")
|
|
44
|
+
return 1
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
writeToStandardOutput(renderExpansions(result.files))
|
|
49
|
+
writeToStandardError(renderDiagnostics(result.files))
|
|
50
|
+
if options.diagnosticsOnly {
|
|
51
|
+
writeToStandardError(renderSummary(result.files))
|
|
52
|
+
}
|
|
53
|
+
let hasErrors = result.files.contains { $0.diagnostics.contains { $0.severity == .error } }
|
|
54
|
+
return hasErrors ? 1 : 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The absolute path of `path` with symbolic links resolved, to tell whether two paths name the same
|
|
59
|
+
/// file. A path that the directory walk returns can differ from the one passed for the same file, for
|
|
60
|
+
/// example `/private/var` and `/var` on macOS.
|
|
61
|
+
private func fileIdentity(_ path: String) -> String {
|
|
62
|
+
URL(fileURLWithPath: path).standardizedFileURL.resolvingSymlinksInPath().path
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// An input that `expand` can't take.
|
|
66
|
+
struct ExpandInputError: Error, Equatable {
|
|
67
|
+
let message: String
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Reads the files of `options`, expands each with `expander`, and returns the report that `expand`
|
|
71
|
+
/// prints. Separate from `runExpand` so tests can drive it without stdout.
|
|
72
|
+
///
|
|
73
|
+
/// A file keeps its path as it was passed and is always expanded. A directory is walked like the
|
|
74
|
+
/// scans walk it (`swiftFiles(in:)`, which skips dependency, build, test and example directories),
|
|
75
|
+
/// and only the files that use one of the macros are expanded, with their absolute paths. A file that
|
|
76
|
+
/// a directory also contains is expanded once.
|
|
77
|
+
func expandFiles(options: ExpandOptions, expander: MacroExpander) -> Result<ExpandResult, ExpandInputError> {
|
|
78
|
+
let prefilter = MacroPrefilter(attributeNames: expander.attributeNames)
|
|
79
|
+
var sources: [(file: String, source: String)] = []
|
|
80
|
+
var seen: Set<String> = []
|
|
81
|
+
for path in options.paths {
|
|
82
|
+
switch isDirectory(atPath: path) {
|
|
83
|
+
case nil:
|
|
84
|
+
return .failure(ExpandInputError(message: "no such file or directory '\(path)'"))
|
|
85
|
+
case .some(true):
|
|
86
|
+
for file in swiftFiles(in: [path]) where !seen.contains(fileIdentity(file)) {
|
|
87
|
+
guard let source = try? String(contentsOfFile: file, encoding: .utf8) else {
|
|
88
|
+
return .failure(ExpandInputError(message: "could not read '\(file)'"))
|
|
89
|
+
}
|
|
90
|
+
seen.insert(fileIdentity(file))
|
|
91
|
+
if mightContainMacro(in: source, prefilter: prefilter) {
|
|
92
|
+
sources.append((file, source))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
case .some(false):
|
|
96
|
+
guard path.hasSuffix(".swift") else {
|
|
97
|
+
return .failure(ExpandInputError(message: "expand takes .swift files and directories, but got '\(path)'"))
|
|
98
|
+
}
|
|
99
|
+
guard !seen.contains(fileIdentity(path)) else {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
guard let source = try? String(contentsOfFile: path, encoding: .utf8) else {
|
|
103
|
+
return .failure(ExpandInputError(message: "could not read '\(path)'"))
|
|
104
|
+
}
|
|
105
|
+
seen.insert(fileIdentity(path))
|
|
106
|
+
sources.append((path, source))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let files = sources.map { expander.expand($0.source, $0.file, options.symbols) }
|
|
111
|
+
|
|
112
|
+
let found = Set(files.flatMap { $0.expansions.compactMap(\.symbol) })
|
|
113
|
+
if let missing = options.symbols.first(where: { !found.contains($0) }) {
|
|
114
|
+
return .failure(
|
|
115
|
+
ExpandInputError(
|
|
116
|
+
message: "no declaration of '\(missing)' in the given files; name a type ('Module'), a nested "
|
|
117
|
+
+ "type ('Module.Options') or a member ('Module.method')"
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return .success(
|
|
123
|
+
ExpandResult(
|
|
124
|
+
schemaVersion: expandSchemaVersion,
|
|
125
|
+
files: options.diagnosticsOnly
|
|
126
|
+
? files.map { ExpandedFile(file: $0.file, expansions: [], diagnostics: $0.diagnostics) }
|
|
127
|
+
: files
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The expanded sources as text. A single expansion is printed as it is; more than one each get a
|
|
133
|
+
/// `// <file>` or `// <file>: <symbol>` header and are separated by an empty line.
|
|
134
|
+
func renderExpansions(_ files: [ExpandedFile]) -> String {
|
|
135
|
+
let expansions = files.flatMap { file in file.expansions.map { (file: file.file, expansion: $0) } }
|
|
136
|
+
if expansions.count == 1, let only = expansions.first {
|
|
137
|
+
return withTrailingNewline(only.expansion.source)
|
|
138
|
+
}
|
|
139
|
+
return expansions.map { file, expansion in
|
|
140
|
+
let header = expansion.symbol.map { "// \(file): \($0)" } ?? "// \(file)"
|
|
141
|
+
return "\(header)\n\n" + withTrailingNewline(expansion.source)
|
|
142
|
+
}
|
|
143
|
+
.joined(separator: "\n")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The diagnostics in the compiler's format, one line each, followed by their notes and Fix-Its.
|
|
147
|
+
func renderDiagnostics(_ files: [ExpandedFile]) -> String {
|
|
148
|
+
var text = ""
|
|
149
|
+
for file in files {
|
|
150
|
+
for diagnostic in file.diagnostics {
|
|
151
|
+
text +=
|
|
152
|
+
"\(file.file):\(diagnostic.line):\(diagnostic.column): \(diagnostic.severity.rawValue): \(diagnostic.message)\n"
|
|
153
|
+
for note in diagnostic.notes {
|
|
154
|
+
text += "\(file.file):\(note.line):\(note.column): note: \(note.message)\n"
|
|
155
|
+
}
|
|
156
|
+
for fixIt in diagnostic.fixIts {
|
|
157
|
+
text += "\(file.file):\(diagnostic.line):\(diagnostic.column): note: fix-it: \(fixIt)\n"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return text
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// The summary line of `--diagnostics-only`, for example `1 error and 2 warnings in 3 files`.
|
|
165
|
+
func renderSummary(_ files: [ExpandedFile]) -> String {
|
|
166
|
+
let diagnostics = files.flatMap(\.diagnostics)
|
|
167
|
+
let errors = diagnostics.filter { $0.severity == .error }.count
|
|
168
|
+
let warnings = diagnostics.filter { $0.severity == .warning }.count
|
|
169
|
+
let checked = count(files.count, "file")
|
|
170
|
+
if errors == 0 && warnings == 0 {
|
|
171
|
+
return "No macro errors or warnings in \(checked).\n"
|
|
172
|
+
}
|
|
173
|
+
let parts = [errors > 0 ? count(errors, "error") : nil, warnings > 0 ? count(warnings, "warning") : nil]
|
|
174
|
+
return "\(parts.compactMap { $0 }.joined(separator: " and ")) in \(checked).\n"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private func count(_ n: Int, _ noun: String) -> String {
|
|
178
|
+
"\(n) \(noun)\(n == 1 ? "" : "s")"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func withTrailingNewline(_ text: String) -> String {
|
|
182
|
+
text.hasSuffix("\n") ? text : text + "\n"
|
|
183
|
+
}
|
|
@@ -463,8 +463,9 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
463
463
|
/// qualified names and gives a resolved ref the name of its declaration; version 8 reports
|
|
464
464
|
/// declarations in `#if` blocks, adds `platforms` (with deprecations), `accessLevel`, and
|
|
465
465
|
/// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
|
|
466
|
-
/// `sharedRef` type kind and the `sharedRef` of a shared object
|
|
467
|
-
|
|
466
|
+
/// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
|
|
467
|
+
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes.
|
|
468
|
+
let scanExportsSchemaVersion = 10
|
|
468
469
|
|
|
469
470
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
470
471
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
@@ -136,6 +136,15 @@ extension TypeNode {
|
|
|
136
136
|
)
|
|
137
137
|
case .promise(let value):
|
|
138
138
|
return .promise(value: value.resolvingRefs(using: index, in: scope))
|
|
139
|
+
case .tuple(let elements, let name):
|
|
140
|
+
return .tuple(elements: elements.map { $0.resolvingRefs(using: index, in: scope) }, name: name)
|
|
141
|
+
case .object(let properties, let name):
|
|
142
|
+
return .object(
|
|
143
|
+
properties: properties.map {
|
|
144
|
+
TypeNodeProperty(name: $0.name, type: $0.type.resolvingRefs(using: index, in: scope))
|
|
145
|
+
},
|
|
146
|
+
name: name
|
|
147
|
+
)
|
|
139
148
|
case .function(let parameters, let returns, let isAsync, let isThrowing):
|
|
140
149
|
return .function(
|
|
141
150
|
parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
|
|
@@ -142,13 +142,14 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
142
142
|
guard scope.isEmpty else {
|
|
143
143
|
return
|
|
144
144
|
}
|
|
145
|
+
let aliases = TupleTypeAliases(in: members, scope: qualified)
|
|
145
146
|
let (functions, properties, events, _) = collectJSMembers(members)
|
|
146
147
|
modules.append(
|
|
147
148
|
ExportedModule(
|
|
148
149
|
name: qualified,
|
|
149
150
|
jsName: stringArgument(of: attribute) ?? name,
|
|
150
|
-
functions: functions,
|
|
151
|
-
properties: properties,
|
|
151
|
+
functions: functions.map(aliases.resolving),
|
|
152
|
+
properties: properties.map(aliases.resolving),
|
|
152
153
|
events: events,
|
|
153
154
|
// `jsName` is filled in by `ExportedSurface.resolvingRefs()`, since the `@SharedObject` can
|
|
154
155
|
// be declared in another file.
|
|
@@ -164,7 +165,13 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
if let attribute = attributes.firstAttribute(named: DetectedMacro.sharedObject.rawValue) {
|
|
167
|
-
let (
|
|
168
|
+
let aliases = TupleTypeAliases(in: members, scope: qualified)
|
|
169
|
+
var (functions, properties, events, initializer) = collectJSMembers(members)
|
|
170
|
+
functions = functions.map(aliases.resolving)
|
|
171
|
+
properties = properties.map(aliases.resolving)
|
|
172
|
+
if let parameters = initializer?.parameters {
|
|
173
|
+
initializer?.parameters = parameters.map(aliases.resolving)
|
|
174
|
+
}
|
|
168
175
|
sharedObjects.append(
|
|
169
176
|
ExportedSharedObject(
|
|
170
177
|
name: qualified,
|