@expo/expo-modules-macros-plugin 0.8.0 → 0.10.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 +3 -0
- package/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Package.swift +7 -10
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +3 -4
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +7 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +59 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +2 -2
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +188 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +77 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +19 -1
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -31
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +8 -2
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +17 -9
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +110 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +16 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +60 -10
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +121 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +38 -14
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +32 -6
- package/package.json +1 -1
- package/apple/Sources/ExpoModulesScannerCLI/main.swift +0 -71
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import SwiftIfConfig
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
|
|
4
|
+
/// The build configuration that `#if` conditions are evaluated against during a scan, built from
|
|
5
|
+
/// the CLI's `--platform` and `--define` options. The scan is static: it knows the target OS and
|
|
6
|
+
/// the spelled compilation flags, and nothing else. Every condition it cannot answer throws, which
|
|
7
|
+
/// SwiftIfConfig turns into an inactive region plus a diagnostic, so an unanswerable `#if` skips
|
|
8
|
+
/// its declarations and surfaces a warning instead of guessing.
|
|
9
|
+
struct ScanBuildConfiguration: BuildConfiguration {
|
|
10
|
+
/// The target OS name to answer `os(...)` with, as spelled in the condition (`iOS`, `macOS`,
|
|
11
|
+
/// `tvOS`, `watchOS`, `visionOS`; compared case-insensitively), or `nil` when no `--platform`
|
|
12
|
+
/// was given, in which case `os(...)` conditions are unanswerable.
|
|
13
|
+
let platform: String?
|
|
14
|
+
|
|
15
|
+
/// The conditional compilation flags treated as set, from repeated `--define` options.
|
|
16
|
+
let defines: Set<String>
|
|
17
|
+
|
|
18
|
+
func isCustomConditionSet(name: String) throws -> Bool {
|
|
19
|
+
return defines.contains(name)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func isActiveTargetOS(name: String) throws -> Bool {
|
|
23
|
+
guard let platform else {
|
|
24
|
+
throw ScanConfigurationError("cannot evaluate 'os(\(name))': no --platform was given")
|
|
25
|
+
}
|
|
26
|
+
return name.lowercased() == platform.lowercased()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Unanswerable conditions
|
|
30
|
+
|
|
31
|
+
// These vary within a single platform's build (device vs simulator, arm64 vs x86_64) or depend
|
|
32
|
+
// on the consumer's toolchain, so a static scan has no correct answer. Throwing makes the region
|
|
33
|
+
// inactive and emits a warning naming the condition.
|
|
34
|
+
|
|
35
|
+
func hasFeature(name: String) throws -> Bool {
|
|
36
|
+
throw ScanConfigurationError("cannot evaluate 'hasFeature(\(name))' in a static scan")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func hasAttribute(name: String) throws -> Bool {
|
|
40
|
+
throw ScanConfigurationError("cannot evaluate 'hasAttribute(\(name))' in a static scan")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool {
|
|
44
|
+
let module = importPath.map(\.1).joined(separator: ".")
|
|
45
|
+
|
|
46
|
+
// A curated set of SDK frameworks is answerable from the target platform alone. Everything
|
|
47
|
+
// else (arbitrary modules, submodule paths, versioned checks) stays unanswerable: a wrong
|
|
48
|
+
// "yes" would surface a declaration that doesn't exist in the real build.
|
|
49
|
+
guard importPath.count == 1,
|
|
50
|
+
case .unversioned = version,
|
|
51
|
+
let frameworkPlatforms = sdkFrameworkPlatforms[module] else {
|
|
52
|
+
throw ScanConfigurationError("cannot evaluate 'canImport(\(module))' in a static scan")
|
|
53
|
+
}
|
|
54
|
+
guard let platform else {
|
|
55
|
+
throw ScanConfigurationError("cannot evaluate 'canImport(\(module))': no --platform was given")
|
|
56
|
+
}
|
|
57
|
+
return frameworkPlatforms.contains(platform.lowercased())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func isActiveTargetArchitecture(name: String) throws -> Bool {
|
|
61
|
+
throw ScanConfigurationError("cannot evaluate 'arch(\(name))' in a static scan")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func isActiveTargetEnvironment(name: String) throws -> Bool {
|
|
65
|
+
throw ScanConfigurationError("cannot evaluate 'targetEnvironment(\(name))' in a static scan")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func isActiveTargetRuntime(name: String) throws -> Bool {
|
|
69
|
+
throw ScanConfigurationError("cannot evaluate '_runtime(\(name))' in a static scan")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func isActiveTargetPointerAuthentication(name: String) throws -> Bool {
|
|
73
|
+
throw ScanConfigurationError("cannot evaluate '_ptrauth(\(name))' in a static scan")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MARK: - Fixed answers
|
|
77
|
+
|
|
78
|
+
// Non-throwing protocol requirements, so they need a value. These are constant across Apple
|
|
79
|
+
// targets (the only ones Expo modules compile for), except the versions, which assume a current
|
|
80
|
+
// toolchain; a module class gated on a *lower* Swift version would be wrongly included, which is
|
|
81
|
+
// rare enough to accept for a scan.
|
|
82
|
+
|
|
83
|
+
var targetPointerBitWidth: Int { 64 }
|
|
84
|
+
var targetAtomicBitWidths: [Int] { [32, 64, 128] }
|
|
85
|
+
var endianness: Endianness { .little }
|
|
86
|
+
var languageVersion: VersionTuple { VersionTuple(6) }
|
|
87
|
+
var compilerVersion: VersionTuple { VersionTuple(6, 2) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The platforms (lowercased, as compared against `--platform`) that ship each of a curated set of
|
|
91
|
+
/// Apple SDK frameworks, so `canImport` of one is answerable from the platform alone. The list is
|
|
92
|
+
/// deliberately small and high-confidence: it covers the frameworks realistically used to gate a
|
|
93
|
+
/// module class, and a framework missing here degrades to the skip-with-warning path rather than a
|
|
94
|
+
/// wrong answer.
|
|
95
|
+
private let sdkFrameworkPlatforms: [String: Set<String>] = [
|
|
96
|
+
"UIKit": ["ios", "tvos", "watchos", "visionos"],
|
|
97
|
+
"AppKit": ["macos"],
|
|
98
|
+
"SwiftUI": ["ios", "macos", "tvos", "watchos", "visionos"],
|
|
99
|
+
"WatchKit": ["watchos"],
|
|
100
|
+
"TVUIKit": ["tvos"],
|
|
101
|
+
"WebKit": ["ios", "macos", "visionos"],
|
|
102
|
+
"SafariServices": ["ios", "macos", "visionos"],
|
|
103
|
+
"ARKit": ["ios", "visionos"],
|
|
104
|
+
"RealityKit": ["ios", "macos", "visionos"],
|
|
105
|
+
"CarPlay": ["ios"],
|
|
106
|
+
"MessageUI": ["ios"],
|
|
107
|
+
"CoreNFC": ["ios"],
|
|
108
|
+
"HealthKit": ["ios", "watchos", "visionos"],
|
|
109
|
+
"HomeKit": ["ios", "tvos", "watchos", "visionos"],
|
|
110
|
+
"WidgetKit": ["ios", "macos", "watchos", "visionos"],
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
/// An unanswerable `#if` condition. SwiftIfConfig converts the thrown error into a diagnostic on
|
|
114
|
+
/// the condition's node and treats the region as inactive.
|
|
115
|
+
struct ScanConfigurationError: Error, CustomStringConvertible {
|
|
116
|
+
let description: String
|
|
117
|
+
|
|
118
|
+
init(_ description: String) {
|
|
119
|
+
self.description = description
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -44,23 +44,36 @@ func scanFiles(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/// Walks `paths`, parses each `.swift` file that might contain one of `macros`, and returns every
|
|
47
|
-
/// detection (in file then source order) with the run's stats —
|
|
48
|
-
/// A thin layer over `scanFiles` that accumulates the per-file
|
|
49
|
-
|
|
47
|
+
/// detection (in file then source order) with the accumulated `#if` warnings and the run's stats —
|
|
48
|
+
/// the shape `scan-modules` projects. A thin layer over `scanFiles` that accumulates the per-file
|
|
49
|
+
/// results.
|
|
50
|
+
func collectDetections(
|
|
51
|
+
paths: [String],
|
|
52
|
+
macros: Set<DetectedMacro>,
|
|
53
|
+
configuration: ScanBuildConfiguration = .init(platform: nil, defines: [])
|
|
54
|
+
) -> (detections: [Detection], warnings: [ScanWarning], stats: ScanStats) {
|
|
50
55
|
var detections: [Detection] = []
|
|
56
|
+
var warnings: [ScanWarning] = []
|
|
51
57
|
let stats = scanFiles(paths: paths, macros: macros) { source, file in
|
|
52
|
-
|
|
58
|
+
let result = detect(source: source, file: file, macros: macros, configuration: configuration)
|
|
59
|
+
detections.append(contentsOf: result.detections)
|
|
60
|
+
warnings.append(contentsOf: result.warnings)
|
|
53
61
|
}
|
|
54
|
-
return (detections, stats)
|
|
62
|
+
return (detections, warnings, stats)
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
/// Parses one source string and returns its detections for the given macro set
|
|
58
|
-
/// tests exercise.
|
|
59
|
-
func detect(
|
|
65
|
+
/// Parses one source string and returns its detections for the given macro set, plus the warnings
|
|
66
|
+
/// for `#if` conditions the configuration couldn't answer. The unit of work the tests exercise.
|
|
67
|
+
func detect(
|
|
68
|
+
source: String,
|
|
69
|
+
file: String,
|
|
70
|
+
macros: Set<DetectedMacro>,
|
|
71
|
+
configuration: ScanBuildConfiguration
|
|
72
|
+
) -> (detections: [Detection], warnings: [ScanWarning]) {
|
|
60
73
|
let tree = Parser.parse(source: source)
|
|
61
|
-
let visitor = DetectionVisitor(file: file, tree: tree, detectedMacros: macros)
|
|
74
|
+
let visitor = DetectionVisitor(file: file, tree: tree, detectedMacros: macros, configuration: configuration)
|
|
62
75
|
visitor.walk(tree)
|
|
63
|
-
return visitor.detections
|
|
76
|
+
return (visitor.detections, visitor.warnings)
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
// MARK: - Pre-filter
|
|
@@ -87,10 +100,21 @@ func mightContainMacro(in source: String, prefilter: NSRegularExpression) -> Boo
|
|
|
87
100
|
|
|
88
101
|
// MARK: - File discovery
|
|
89
102
|
|
|
90
|
-
/// Directory names skipped during the recursive walk
|
|
91
|
-
///
|
|
92
|
-
/// into the bulk of
|
|
93
|
-
|
|
103
|
+
/// Directory names skipped during the recursive walk, in two groups:
|
|
104
|
+
/// - Build products, dependencies, and git internals (`.build`, `Pods`, `.git`, `node_modules`) are
|
|
105
|
+
/// never source worth scanning, and pruning them keeps the walk from descending into the bulk of
|
|
106
|
+
/// a monorepo's files. `node_modules` also makes any npm package root safe to pass as a scan
|
|
107
|
+
/// path: nested dependencies are separate packages and get scanned on their own.
|
|
108
|
+
/// - Test and example directories, by the layout conventions of Expo module packages (`Tests`,
|
|
109
|
+
/// `UITests`, `__tests__`, `__mocks__`, `example(s)`, `e2e`). Their sources are not compiled into the
|
|
110
|
+
/// package's product (they belong to a `test_spec` or a standalone example app), so a declaration
|
|
111
|
+
/// found there would name a type the consumer can't reference. This is a name-based heuristic;
|
|
112
|
+
/// a package keeping product sources in such a directory can declare its modules in
|
|
113
|
+
/// `expo-module.config.json` instead.
|
|
114
|
+
private let prunedDirectoryNames: Set<String> = [
|
|
115
|
+
".build", "Pods", ".git", "node_modules",
|
|
116
|
+
"Tests", "UITests", "__tests__", "__mocks__", "example", "examples", "e2e",
|
|
117
|
+
]
|
|
94
118
|
|
|
95
119
|
/// Expands the given paths into the list of `.swift` files to parse: a file path passes through,
|
|
96
120
|
/// a directory is enumerated recursively (skipping `prunedDirectoryNames`). Order is deterministic
|
|
@@ -9,8 +9,12 @@ public enum Scanner {
|
|
|
9
9
|
/// Runs the `scan-modules` command over `paths`, prints the JSON report to stdout, and returns a
|
|
10
10
|
/// process exit code: `0` on success, `1` if encoding fails. (The deep `scan-exports` command has
|
|
11
11
|
/// its own `runExports` entry returning its own result type.)
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
///
|
|
13
|
+
/// `platform` and `defines` (the `--platform` and `--define` options) form the configuration that
|
|
14
|
+
/// `#if` conditions are evaluated against; see `ScanBuildConfiguration`.
|
|
15
|
+
public static func runModules(paths: [String], platform: String? = nil, defines: [String] = []) -> Int32 {
|
|
16
|
+
let configuration = ScanBuildConfiguration(platform: platform, defines: Set(defines))
|
|
17
|
+
let result = scanModules(paths: paths, configuration: configuration)
|
|
14
18
|
|
|
15
19
|
do {
|
|
16
20
|
let encoder = JSONEncoder()
|
|
@@ -40,29 +44,51 @@ struct ScannedModule: Codable, Equatable {
|
|
|
40
44
|
/// and the consumer never has to apply the fallback itself.
|
|
41
45
|
let jsName: String
|
|
42
46
|
|
|
47
|
+
/// The class's spelled access modifier (`open`, `public`, `package`, `fileprivate`, `private`),
|
|
48
|
+
/// or `internal` when none is written. The generated modules provider references the class from
|
|
49
|
+
/// the app target, which requires `public`/`open`, so the consumer uses this to skip inaccessible
|
|
50
|
+
/// classes with a diagnostic instead of emitting a provider that fails to compile.
|
|
51
|
+
let accessLevel: String
|
|
52
|
+
|
|
43
53
|
/// Source file the module was found in, relative to the path the scanner was invoked with.
|
|
44
54
|
let file: String
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
/// Version of the `scan-modules` output shape. Bumped on any breaking change to the envelope or to
|
|
58
|
+
/// `ScannedModule`, so `expo-modules-autolinking` can verify it understands the output before
|
|
59
|
+
/// trusting it (and fall back to config-declared modules when it doesn't).
|
|
60
|
+
let scanModulesSchemaVersion = 1
|
|
61
|
+
|
|
47
62
|
/// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
|
|
48
63
|
/// command's JSON output. (`scan-exports` returns its own `ScanExportsResult` shape; the two commands
|
|
49
64
|
/// serve different consumers and don't share an envelope.)
|
|
50
65
|
struct ScanModulesResult: Codable, Equatable {
|
|
66
|
+
let schemaVersion: Int
|
|
51
67
|
let modules: [ScannedModule]
|
|
68
|
+
|
|
69
|
+
/// Warnings for `#if` conditions the scan couldn't answer statically (see `ScanWarning`). Carried
|
|
70
|
+
/// in the report rather than on stderr so the consumer can attach them to its own output.
|
|
71
|
+
let warnings: [ScanWarning]
|
|
72
|
+
|
|
52
73
|
let stats: ScanStats
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/// Scans the given paths for top-level `@ExpoModule` types and returns the modules (in file then
|
|
56
77
|
/// source order) plus the stats for the run — the `scan-modules` command. Kept separate from the
|
|
57
78
|
/// public entry (and `internal`) so tests can drive it without going through argv/stdout.
|
|
58
|
-
func scanModules(paths: [String]) -> ScanModulesResult {
|
|
59
|
-
let scan = collectDetections(paths: paths, macros: [.expoModule])
|
|
79
|
+
func scanModules(paths: [String], configuration: ScanBuildConfiguration = .init(platform: nil, defines: [])) -> ScanModulesResult {
|
|
80
|
+
let scan = collectDetections(paths: paths, macros: [.expoModule], configuration: configuration)
|
|
60
81
|
|
|
61
82
|
let modules = scan.detections.map {
|
|
62
83
|
// Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")` override, else the
|
|
63
84
|
// class name.
|
|
64
|
-
ScannedModule(name: $0.name, jsName: $0.jsName ?? $0.name, file: $0.file)
|
|
85
|
+
ScannedModule(name: $0.name, jsName: $0.jsName ?? $0.name, accessLevel: $0.accessLevel, file: $0.file)
|
|
65
86
|
}
|
|
66
87
|
|
|
67
|
-
return ScanModulesResult(
|
|
88
|
+
return ScanModulesResult(
|
|
89
|
+
schemaVersion: scanModulesSchemaVersion,
|
|
90
|
+
modules: modules,
|
|
91
|
+
warnings: scan.warnings,
|
|
92
|
+
stats: scan.stats
|
|
93
|
+
)
|
|
68
94
|
}
|
package/package.json
CHANGED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import ExpoModulesScanner
|
|
2
|
-
import Foundation
|
|
3
|
-
|
|
4
|
-
/// Command-line front end for the scanner. Parses the subcommand and paths, then delegates to the
|
|
5
|
-
/// matching library entry (which runs the scan and writes its JSON output). Each path may be a
|
|
6
|
-
/// `.swift` file or a directory (scanned recursively for `.swift` files).
|
|
7
|
-
///
|
|
8
|
-
/// Subcommands:
|
|
9
|
-
/// scan-modules <path>... fast: top-level `@ExpoModule` types, for autolinking
|
|
10
|
-
/// scan-exports <path>... deep: full JS-exported surface, for TS type generation
|
|
11
|
-
|
|
12
|
-
let toolName = "ExpoModulesScanner"
|
|
13
|
-
|
|
14
|
-
let usageText = """
|
|
15
|
-
usage: \(toolName) <subcommand> <path> [<path> ...]
|
|
16
|
-
|
|
17
|
-
subcommands:
|
|
18
|
-
scan-modules fast scan for top-level @ExpoModule types (autolinking)
|
|
19
|
-
scan-exports deep scan of the full JS-exported surface (type generation)
|
|
20
|
-
|
|
21
|
-
options:
|
|
22
|
-
-h, --help print this help and exit
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
/// Prints the usage text to the given handle. Goes to stdout when help was explicitly requested
|
|
27
|
-
/// (a successful action), stderr when it accompanies a usage error.
|
|
28
|
-
func printUsage(to handle: FileHandle = .standardError) {
|
|
29
|
-
handle.write(Data(usageText.utf8))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func fail(_ message: String, usage: Bool = false, code: Int32 = 2) -> Never {
|
|
33
|
-
FileHandle.standardError.write(Data("error: \(message)\n".utf8))
|
|
34
|
-
if usage {
|
|
35
|
-
printUsage()
|
|
36
|
-
}
|
|
37
|
-
exit(code)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
var arguments = Array(CommandLine.arguments.dropFirst())
|
|
41
|
-
|
|
42
|
-
// `-h`/`--help` anywhere is treated as a help request: print usage to stdout and exit 0.
|
|
43
|
-
if arguments.contains(where: { $0 == "-h" || $0 == "--help" }) {
|
|
44
|
-
printUsage(to: .standardOutput)
|
|
45
|
-
exit(0)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
guard !arguments.isEmpty else {
|
|
49
|
-
printUsage()
|
|
50
|
-
exit(2)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let subcommand = arguments.removeFirst()
|
|
54
|
-
let paths = arguments
|
|
55
|
-
|
|
56
|
-
switch subcommand {
|
|
57
|
-
case "scan-modules":
|
|
58
|
-
guard !paths.isEmpty else {
|
|
59
|
-
fail("scan-modules requires at least one path", usage: true)
|
|
60
|
-
}
|
|
61
|
-
exit(Scanner.runModules(paths: paths))
|
|
62
|
-
|
|
63
|
-
case "scan-exports":
|
|
64
|
-
guard !paths.isEmpty else {
|
|
65
|
-
fail("scan-exports requires at least one path", usage: true)
|
|
66
|
-
}
|
|
67
|
-
exit(Scanner.runExports(paths: paths))
|
|
68
|
-
|
|
69
|
-
default:
|
|
70
|
-
fail("unknown subcommand '\(subcommand)'", usage: true)
|
|
71
|
-
}
|