@expo/expo-modules-macros-plugin 0.9.0 → 0.11.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 +17 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +78 -2
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +89 -2
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +3 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -47
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +8 -2
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +6 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +487 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +8 -4
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +4 -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 +17 -7
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +7 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +1 -0
- 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 +186 -0
- package/build/types.js +15 -0
- package/package.json +12 -2
|
@@ -50,7 +50,7 @@ func scanFiles(
|
|
|
50
50
|
func collectDetections(
|
|
51
51
|
paths: [String],
|
|
52
52
|
macros: Set<DetectedMacro>,
|
|
53
|
-
configuration: ScanBuildConfiguration =
|
|
53
|
+
configuration: ScanBuildConfiguration? = nil
|
|
54
54
|
) -> (detections: [Detection], warnings: [ScanWarning], stats: ScanStats) {
|
|
55
55
|
var detections: [Detection] = []
|
|
56
56
|
var warnings: [ScanWarning] = []
|
|
@@ -68,7 +68,7 @@ func detect(
|
|
|
68
68
|
source: String,
|
|
69
69
|
file: String,
|
|
70
70
|
macros: Set<DetectedMacro>,
|
|
71
|
-
configuration: ScanBuildConfiguration
|
|
71
|
+
configuration: ScanBuildConfiguration?
|
|
72
72
|
) -> (detections: [Detection], warnings: [ScanWarning]) {
|
|
73
73
|
let tree = Parser.parse(source: source)
|
|
74
74
|
let visitor = DetectionVisitor(file: file, tree: tree, detectedMacros: macros, configuration: configuration)
|
|
@@ -100,11 +100,21 @@ func mightContainMacro(in source: String, prefilter: NSRegularExpression) -> Boo
|
|
|
100
100
|
|
|
101
101
|
// MARK: - File discovery
|
|
102
102
|
|
|
103
|
-
/// Directory names skipped during the recursive walk
|
|
104
|
-
///
|
|
105
|
-
///
|
|
106
|
-
///
|
|
107
|
-
|
|
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
|
+
]
|
|
108
118
|
|
|
109
119
|
/// Expands the given paths into the list of `.swift` files to parse: a file path passes through,
|
|
110
120
|
/// a directory is enumerated recursively (skipping `prunedDirectoryNames`). Order is deterministic
|
|
@@ -178,9 +178,16 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
178
178
|
let records: [ExportedRecord]
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/// Version of the `scan-exports` output shape. Bumped on any breaking change to the envelope or to
|
|
182
|
+
/// anything under `exports`, so a consumer can verify it understands the output before trusting it.
|
|
183
|
+
/// Versioned independently of `scanModulesSchemaVersion`: the two commands serve different consumers
|
|
184
|
+
/// and change for different reasons.
|
|
185
|
+
let scanExportsSchemaVersion = 1
|
|
186
|
+
|
|
181
187
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
182
188
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
183
189
|
struct ScanExportsResult: Encodable, Equatable {
|
|
190
|
+
let schemaVersion: Int
|
|
184
191
|
let exports: ExportedSurface
|
|
185
192
|
let stats: ScanStats
|
|
186
193
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import SwiftParser
|
|
2
3
|
|
|
3
4
|
/// The scanner's public entry point. Argument parsing, subcommand dispatch, and usage text live in
|
|
4
5
|
/// the CLI target; this just runs a command and writes its JSON report to stdout.
|
|
@@ -10,11 +11,10 @@ public enum Scanner {
|
|
|
10
11
|
/// process exit code: `0` on success, `1` if encoding fails. (The deep `scan-exports` command has
|
|
11
12
|
/// its own `runExports` entry returning its own result type.)
|
|
12
13
|
///
|
|
13
|
-
/// `
|
|
14
|
-
///
|
|
15
|
-
public static func runModules(paths: [String],
|
|
16
|
-
let
|
|
17
|
-
let result = scanModules(paths: paths, configuration: configuration)
|
|
14
|
+
/// `defines` (the `--define` options) asserts conditional compilation flags; see `scanModules`
|
|
15
|
+
/// for how they and platforms shape each module's `platforms` list.
|
|
16
|
+
public static func runModules(paths: [String], defines: [String] = []) -> Int32 {
|
|
17
|
+
let result = scanModules(paths: paths, defines: Set(defines))
|
|
18
18
|
|
|
19
19
|
do {
|
|
20
20
|
let encoder = JSONEncoder()
|
|
@@ -31,10 +31,11 @@ public enum Scanner {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/// One module in the `scan-modules` output. Trimmed to what `expo-modules-autolinking` needs to
|
|
34
|
-
/// register a module: the Swift class name, the JS name it registers under,
|
|
35
|
-
/// The richer fields the visitor captures (declaration kind, raw
|
|
36
|
-
/// dropped here — they're redundant for this command (the macro
|
|
37
|
-
/// and the deep `scan-exports` surface carries the richer
|
|
34
|
+
/// register a module: the Swift class name, the JS name it registers under, the platforms that
|
|
35
|
+
/// include it, and the file it's in. The richer fields the visitor captures (declaration kind, raw
|
|
36
|
+
/// macro arguments, line/column) are dropped here — they're redundant for this command (the macro
|
|
37
|
+
/// is always `@ExpoModule` on a class) and the deep `scan-exports` surface carries the richer
|
|
38
|
+
/// per-member detail instead.
|
|
38
39
|
struct ScannedModule: Codable, Equatable {
|
|
39
40
|
/// The Swift class name the module is declared as.
|
|
40
41
|
let name: String
|
|
@@ -50,14 +51,22 @@ struct ScannedModule: Codable, Equatable {
|
|
|
50
51
|
/// classes with a diagnostic instead of emitting a provider that fails to compile.
|
|
51
52
|
let accessLevel: String
|
|
52
53
|
|
|
54
|
+
/// The Apple OSes whose builds include this class, spelled as `os(...)` spells them and compared
|
|
55
|
+
/// case-sensitively. An unconditional module lists every OS. Empty means no build is known to
|
|
56
|
+
/// include it, because the enclosing conditions depend on flags not asserted with `--define` or
|
|
57
|
+
/// on conditions a static scan cannot answer (those are reported in `warnings`). The consumer
|
|
58
|
+
/// must not assume such a class exists.
|
|
59
|
+
let platforms: [String]
|
|
60
|
+
|
|
53
61
|
/// Source file the module was found in, relative to the path the scanner was invoked with.
|
|
54
62
|
let file: String
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/// Version of the `scan-modules` output shape. Bumped on any breaking change to the envelope or to
|
|
58
66
|
/// `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
|
-
|
|
67
|
+
/// trusting it (and fall back to config-declared modules when it doesn't). Version 2 added the
|
|
68
|
+
/// per-module `platforms` list and made the module list platform-agnostic.
|
|
69
|
+
let scanModulesSchemaVersion = 2
|
|
61
70
|
|
|
62
71
|
/// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
|
|
63
72
|
/// command's JSON output. (`scan-exports` returns its own `ScanExportsResult` shape; the two commands
|
|
@@ -73,22 +82,80 @@ struct ScanModulesResult: Codable, Equatable {
|
|
|
73
82
|
let stats: ScanStats
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
/// The Apple OSes a scan attributes modules to. `condition` is what `os(...)` matches; `reported`
|
|
86
|
+
/// is what the JSON carries. They are identical today, but stated separately so the output
|
|
87
|
+
/// contract cannot drift if the matcher's spelling changes.
|
|
88
|
+
private let platformUniverse: [(condition: String, reported: String)] = [
|
|
89
|
+
(condition: "iOS", reported: "iOS"),
|
|
90
|
+
(condition: "macOS", reported: "macOS"),
|
|
91
|
+
(condition: "tvOS", reported: "tvOS"),
|
|
92
|
+
(condition: "watchOS", reported: "watchOS"),
|
|
93
|
+
(condition: "visionOS", reported: "visionOS"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
/// Scans the given paths for top-level `@ExpoModule` types and returns every module found in any
|
|
97
|
+
/// `#if` branch (in file then source order), each with the platforms whose builds include it, plus
|
|
98
|
+
/// the stats for the run — the `scan-modules` command. Kept separate from the public entry (and
|
|
99
|
+
/// `internal`) so tests can drive it without going through argv/stdout.
|
|
100
|
+
///
|
|
101
|
+
/// Each file is parsed once and walked once per platform (plus once unconditionally to enumerate
|
|
102
|
+
/// every module): a module's `platforms` are the OSes whose evaluated walk reached it, given the
|
|
103
|
+
/// asserted `defines`. The scanner reports the facts; filtering to the platform being linked is the
|
|
104
|
+
/// consumer's call.
|
|
105
|
+
func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResult {
|
|
106
|
+
var modules: [ScannedModule] = []
|
|
107
|
+
var warnings: [ScanWarning] = []
|
|
108
|
+
var seenWarnings = Set<ScanWarning>()
|
|
109
|
+
|
|
110
|
+
let stats = scanFiles(paths: paths, macros: [.expoModule]) { source, file in
|
|
111
|
+
let tree = Parser.parse(source: source)
|
|
112
|
+
|
|
113
|
+
// The unconditional walk enumerates every module in the file, in source order.
|
|
114
|
+
let allModules = DetectionVisitor(file: file, tree: tree, detectedMacros: [.expoModule], configuration: nil)
|
|
115
|
+
allModules.walk(tree)
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
// One evaluated walk per OS attributes each module to the platforms that include it. The walks
|
|
118
|
+
// are cheap relative to the parse, which is shared.
|
|
119
|
+
var platformsByDetection: [String: [String]] = [:]
|
|
120
|
+
for platform in platformUniverse {
|
|
121
|
+
let configuration = ScanBuildConfiguration(platform: platform.condition, defines: defines)
|
|
122
|
+
let visitor = DetectionVisitor(file: file, tree: tree, detectedMacros: [.expoModule], configuration: configuration)
|
|
123
|
+
visitor.walk(tree)
|
|
124
|
+
for detection in visitor.detections {
|
|
125
|
+
platformsByDetection[detectionKey(detection), default: []].append(platform.reported)
|
|
126
|
+
}
|
|
127
|
+
// The same unanswerable condition diagnoses identically in every per-platform walk; report
|
|
128
|
+
// it once.
|
|
129
|
+
for warning in visitor.warnings where seenWarnings.insert(warning).inserted {
|
|
130
|
+
warnings.append(warning)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for detection in allModules.detections {
|
|
135
|
+
// Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")` override, else
|
|
136
|
+
// the class name.
|
|
137
|
+
modules.append(
|
|
138
|
+
ScannedModule(
|
|
139
|
+
name: detection.name,
|
|
140
|
+
jsName: detection.jsName ?? detection.name,
|
|
141
|
+
accessLevel: detection.accessLevel,
|
|
142
|
+
platforms: platformsByDetection[detectionKey(detection)] ?? [],
|
|
143
|
+
file: detection.file
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
86
147
|
}
|
|
87
148
|
|
|
88
149
|
return ScanModulesResult(
|
|
89
150
|
schemaVersion: scanModulesSchemaVersion,
|
|
90
151
|
modules: modules,
|
|
91
|
-
warnings:
|
|
92
|
-
stats:
|
|
152
|
+
warnings: warnings,
|
|
153
|
+
stats: stats
|
|
93
154
|
)
|
|
94
155
|
}
|
|
156
|
+
|
|
157
|
+
/// Identifies one declaration across the per-platform walks of the same tree: the source position
|
|
158
|
+
/// is unique within a file, and the name guards against any position ambiguity.
|
|
159
|
+
private func detectionKey(_ detection: Detection) -> String {
|
|
160
|
+
return "\(detection.line):\(detection.column):\(detection.name)"
|
|
161
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ScanExportsResult, ScanModulesResult } from './types';
|
|
2
|
+
export * from './types';
|
|
3
|
+
/** Options accepted by `scanModules`, mirroring the CLI's `scan-modules` flags. */
|
|
4
|
+
export interface ScanModulesOptions {
|
|
5
|
+
/** Conditional compilation flags to treat as set, e.g. `['DEBUG']`. */
|
|
6
|
+
defines?: string[];
|
|
7
|
+
/** Overrides the scanner binary path. Defaults to the one shipped with this package. */
|
|
8
|
+
binaryPath?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Options accepted by `scanExports`. The deep scan doesn't evaluate `#if`, so it takes no flags. */
|
|
11
|
+
export interface ScanExportsOptions {
|
|
12
|
+
/** Overrides the scanner binary path. Defaults to the one shipped with this package. */
|
|
13
|
+
binaryPath?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when the scanner exits non-zero. Exit code 2 is a usage error (bad subcommand or flags) and
|
|
17
|
+
* 1 means the report couldn't be encoded; both put a message on stderr, carried here as `stderr`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ScannerError extends Error {
|
|
20
|
+
readonly exitCode: number | null;
|
|
21
|
+
readonly stderr: string;
|
|
22
|
+
constructor(message: string, exitCode: number | null, stderr: string);
|
|
23
|
+
}
|
|
24
|
+
/** Thrown when the binary's output schema doesn't match what these types were written against. */
|
|
25
|
+
export declare class ScannerSchemaVersionError extends Error {
|
|
26
|
+
readonly command: string;
|
|
27
|
+
readonly found: number;
|
|
28
|
+
readonly expected: number;
|
|
29
|
+
constructor(command: string, found: number, expected: number);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Absolute path to the scanner binary shipped with this package. It doubles as the macro plugin
|
|
33
|
+
* executable, so it lives next to the Swift package rather than in a `bin` directory.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getScannerBinaryPath(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Fast scan for top-level `@ExpoModule` types, for autolinking. Each path is a `.swift` file or a
|
|
38
|
+
* directory, scanned recursively.
|
|
39
|
+
*
|
|
40
|
+
* Conditions the scan can't answer statically come back in `warnings` rather than throwing, so the
|
|
41
|
+
* caller can surface them alongside its own diagnostics.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scanModules(paths: string[], options?: ScanModulesOptions): Promise<ScanModulesResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Deep scan of the full JS-exported surface, for TypeScript type generation. Each path is a `.swift`
|
|
46
|
+
* file or a directory, scanned recursively.
|
|
47
|
+
*
|
|
48
|
+
* Unlike `scanModules`, this doesn't evaluate `#if` blocks, so it takes no define option:
|
|
49
|
+
* conditional declarations are reported as if their conditions held.
|
|
50
|
+
*/
|
|
51
|
+
export declare function scanExports(paths: string[], options?: ScanExportsOptions): Promise<ScanExportsResult>;
|
package/build/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.ScannerSchemaVersionError = exports.ScannerError = void 0;
|
|
21
|
+
exports.getScannerBinaryPath = getScannerBinaryPath;
|
|
22
|
+
exports.scanModules = scanModules;
|
|
23
|
+
exports.scanExports = scanExports;
|
|
24
|
+
const node_child_process_1 = require("node:child_process");
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
26
|
+
const types_1 = require("./types");
|
|
27
|
+
__exportStar(require("./types"), exports);
|
|
28
|
+
/**
|
|
29
|
+
* Thrown when the scanner exits non-zero. Exit code 2 is a usage error (bad subcommand or flags) and
|
|
30
|
+
* 1 means the report couldn't be encoded; both put a message on stderr, carried here as `stderr`.
|
|
31
|
+
*/
|
|
32
|
+
class ScannerError extends Error {
|
|
33
|
+
constructor(message, exitCode, stderr) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.exitCode = exitCode;
|
|
36
|
+
this.stderr = stderr;
|
|
37
|
+
this.name = 'ScannerError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.ScannerError = ScannerError;
|
|
41
|
+
/** Thrown when the binary's output schema doesn't match what these types were written against. */
|
|
42
|
+
class ScannerSchemaVersionError extends Error {
|
|
43
|
+
constructor(command, found, expected) {
|
|
44
|
+
super(`\`${command}\` returned schema version ${found}, but this package understands ${expected}. ` +
|
|
45
|
+
'The scanner binary and this wrapper are out of sync; align their versions.');
|
|
46
|
+
this.command = command;
|
|
47
|
+
this.found = found;
|
|
48
|
+
this.expected = expected;
|
|
49
|
+
this.name = 'ScannerSchemaVersionError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.ScannerSchemaVersionError = ScannerSchemaVersionError;
|
|
53
|
+
/**
|
|
54
|
+
* Absolute path to the scanner binary shipped with this package. It doubles as the macro plugin
|
|
55
|
+
* executable, so it lives next to the Swift package rather than in a `bin` directory.
|
|
56
|
+
*/
|
|
57
|
+
function getScannerBinaryPath() {
|
|
58
|
+
return node_path_1.default.join(__dirname, '..', 'apple', 'ExpoModulesMacros-tool');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Runs a scanner subcommand and parses its JSON report.
|
|
62
|
+
*
|
|
63
|
+
* Paths are passed after the options. The CLI has no `--` separator, so a path spelled exactly
|
|
64
|
+
* `--define` would be read as that option instead; such a path isn't representable and the scan
|
|
65
|
+
* fails with a usage error rather than scanning the wrong thing. `-h`/`--help` are worse, since the
|
|
66
|
+
* CLI answers them from anywhere in argv by printing usage and exiting 0, which would surface as an
|
|
67
|
+
* opaque parse failure. `assertRepresentablePaths` rejects those before spawning.
|
|
68
|
+
*
|
|
69
|
+
* Output is buffered rather than streamed: the report is only usable once complete. Scanning all of
|
|
70
|
+
* `expo/packages` produces ~17 KB, so the raised `maxBuffer` is headroom for a far larger tree
|
|
71
|
+
* rather than a limit anything is expected to approach.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* Rejects paths the CLI can't receive as paths. `-h`/`--help` are recognized anywhere in argv, so
|
|
75
|
+
* passing one as a path prints usage and exits 0: the scan never runs, and without this the caller
|
|
76
|
+
* would see a JSON parse failure pointing at the wrong cause.
|
|
77
|
+
*/
|
|
78
|
+
function assertRepresentablePaths(command, paths) {
|
|
79
|
+
const helpFlag = paths.find((candidate) => candidate === '-h' || candidate === '--help');
|
|
80
|
+
if (helpFlag !== undefined) {
|
|
81
|
+
throw new TypeError(`${command} cannot scan a path named \`${helpFlag}\`: the scanner reads it as a help ` +
|
|
82
|
+
'request. Pass a path that resolves to the same file, such as `./' +
|
|
83
|
+
helpFlag +
|
|
84
|
+
'`.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function runScanner(binaryPath, args) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
(0, node_child_process_1.execFile)(binaryPath, args, { maxBuffer: 64 * 1024 * 1024, encoding: 'utf8' }, (error, stdout, stderr) => {
|
|
90
|
+
if (error) {
|
|
91
|
+
// A spawn failure reports a string `code` (e.g. 'ENOENT'); a non-zero exit reports a
|
|
92
|
+
// number. Only the latter is an exit status.
|
|
93
|
+
const exitCode = typeof error.code === 'number' ? error.code : null;
|
|
94
|
+
if (error.code === 'ENOENT') {
|
|
95
|
+
reject(new ScannerError(`The scanner binary is missing at ${binaryPath}. Run \`npm run build\` in this package to build it.`, null, stderr));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
reject(new ScannerError(`\`${node_path_1.default.basename(binaryPath)} ${args.join(' ')}\` failed: ${stderr.trim() || error.message}`, exitCode, stderr));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
resolve(JSON.parse(stdout));
|
|
103
|
+
}
|
|
104
|
+
catch (parseError) {
|
|
105
|
+
reject(new ScannerError(`Could not parse the scanner's JSON output: ${parseError.message}`, null, stderr));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Fast scan for top-level `@ExpoModule` types, for autolinking. Each path is a `.swift` file or a
|
|
112
|
+
* directory, scanned recursively.
|
|
113
|
+
*
|
|
114
|
+
* Conditions the scan can't answer statically come back in `warnings` rather than throwing, so the
|
|
115
|
+
* caller can surface them alongside its own diagnostics.
|
|
116
|
+
*/
|
|
117
|
+
async function scanModules(paths, options = {}) {
|
|
118
|
+
if (paths.length === 0) {
|
|
119
|
+
throw new TypeError('scanModules requires at least one path');
|
|
120
|
+
}
|
|
121
|
+
assertRepresentablePaths('scanModules', paths);
|
|
122
|
+
const args = ['scan-modules'];
|
|
123
|
+
for (const define of options.defines ?? []) {
|
|
124
|
+
args.push('--define', define);
|
|
125
|
+
}
|
|
126
|
+
args.push(...paths);
|
|
127
|
+
const result = await runScanner(options.binaryPath ?? getScannerBinaryPath(), args);
|
|
128
|
+
if (result.schemaVersion !== types_1.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION) {
|
|
129
|
+
throw new ScannerSchemaVersionError('scan-modules', result.schemaVersion, types_1.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Deep scan of the full JS-exported surface, for TypeScript type generation. Each path is a `.swift`
|
|
135
|
+
* file or a directory, scanned recursively.
|
|
136
|
+
*
|
|
137
|
+
* Unlike `scanModules`, this doesn't evaluate `#if` blocks, so it takes no define option:
|
|
138
|
+
* conditional declarations are reported as if their conditions held.
|
|
139
|
+
*/
|
|
140
|
+
async function scanExports(paths, options = {}) {
|
|
141
|
+
if (paths.length === 0) {
|
|
142
|
+
throw new TypeError('scanExports requires at least one path');
|
|
143
|
+
}
|
|
144
|
+
assertRepresentablePaths('scanExports', paths);
|
|
145
|
+
const result = await runScanner(options.binaryPath ?? getScannerBinaryPath(), [
|
|
146
|
+
'scan-exports',
|
|
147
|
+
...paths,
|
|
148
|
+
]);
|
|
149
|
+
if (result.schemaVersion !== types_1.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION) {
|
|
150
|
+
throw new ScannerSchemaVersionError('scan-exports', result.schemaVersion, types_1.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION);
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written mirrors of the scanner's JSON output. The Swift `Codable` types in
|
|
3
|
+
* `apple/Sources/ExpoModulesScanner` are the source of truth; these are kept in sync by hand, which
|
|
4
|
+
* is why each command carries a `schemaVersion` the wrapper checks at runtime.
|
|
5
|
+
*/
|
|
6
|
+
/** The JS `typeof` category a boundary type reports, mirroring `JSType`. */
|
|
7
|
+
export type JSType = 'undefined' | 'object' | 'boolean' | 'number' | 'bigint' | 'string' | 'symbol' | 'function';
|
|
8
|
+
/**
|
|
9
|
+
* A boundary type as a tagged tree, mirroring `TypeNode`. Discriminate on `kind`.
|
|
10
|
+
*
|
|
11
|
+
* `unknown` carries no `typeof`, and an `optional` reports its *wrapped* node's category, so
|
|
12
|
+
* `(Int, String)?` (an optional around an unknown) has none either. Every other node always has one.
|
|
13
|
+
* The absent (`undefined`) case is carried by the `optional` wrapper itself rather than by its
|
|
14
|
+
* wrapped node.
|
|
15
|
+
*/
|
|
16
|
+
export type TypeNode = {
|
|
17
|
+
kind: 'primitive';
|
|
18
|
+
typeof: JSType;
|
|
19
|
+
name: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'optional';
|
|
22
|
+
typeof?: JSType;
|
|
23
|
+
wrapped: TypeNode;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'array';
|
|
26
|
+
typeof: JSType;
|
|
27
|
+
element: TypeNode;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'dictionary';
|
|
30
|
+
typeof: JSType;
|
|
31
|
+
key: TypeNode;
|
|
32
|
+
value: TypeNode;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'promise';
|
|
35
|
+
typeof: JSType;
|
|
36
|
+
value: TypeNode;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'function';
|
|
39
|
+
typeof: JSType;
|
|
40
|
+
parameters: TypeNode[];
|
|
41
|
+
/** Absent for a `Void` result. */
|
|
42
|
+
returns?: TypeNode;
|
|
43
|
+
async: boolean;
|
|
44
|
+
throws: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Any other named type (record, shared object, enum, ...); `name` may be qualified. */
|
|
47
|
+
| {
|
|
48
|
+
kind: 'ref';
|
|
49
|
+
typeof: JSType;
|
|
50
|
+
name: string;
|
|
51
|
+
}
|
|
52
|
+
/** A type the scanner couldn't interpret; `text` is its source spelling. */
|
|
53
|
+
| {
|
|
54
|
+
kind: 'unknown';
|
|
55
|
+
text: string;
|
|
56
|
+
};
|
|
57
|
+
/** One parameter of a `@JS` function or `@JS init`. */
|
|
58
|
+
export interface ExportedParameter {
|
|
59
|
+
/** The argument label, or `_` when unlabeled. */
|
|
60
|
+
label: string;
|
|
61
|
+
/** The internal parameter name. */
|
|
62
|
+
name: string;
|
|
63
|
+
type: TypeNode;
|
|
64
|
+
/** True when the caller may omit it: a default value or an optional type. */
|
|
65
|
+
optional: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** One `@JS func` on a module or shared object. */
|
|
68
|
+
export interface ExportedFunction {
|
|
69
|
+
name: string;
|
|
70
|
+
/** The JS name it binds under: the `@JS("x")` override, else `name`. */
|
|
71
|
+
jsName: string;
|
|
72
|
+
parameters: ExportedParameter[];
|
|
73
|
+
/** Absent for a `Void` return. */
|
|
74
|
+
returns?: TypeNode;
|
|
75
|
+
async: boolean;
|
|
76
|
+
throws: boolean;
|
|
77
|
+
static: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** One `@JS var` on a module or shared object. */
|
|
80
|
+
export interface ExportedProperty {
|
|
81
|
+
name: string;
|
|
82
|
+
jsName: string;
|
|
83
|
+
/** Absent when the type isn't determinable syntactically; the macro binds it getter-only. */
|
|
84
|
+
type?: TypeNode;
|
|
85
|
+
readonly: boolean;
|
|
86
|
+
static: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** One `@Record` property: a data slot, not a JS accessor. */
|
|
89
|
+
export interface ExportedRecordProperty {
|
|
90
|
+
name: string;
|
|
91
|
+
type: TypeNode;
|
|
92
|
+
/** Optional-typed. */
|
|
93
|
+
optional: boolean;
|
|
94
|
+
/** Whether JS must supply it: neither defaulted nor optional. */
|
|
95
|
+
required: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** An `@ExpoModule` type and its `@JS` surface. */
|
|
98
|
+
export interface ExportedModule {
|
|
99
|
+
name: string;
|
|
100
|
+
/** The JS module name: `@ExpoModule("Foo")` override, else the class name. */
|
|
101
|
+
jsName: string;
|
|
102
|
+
functions: ExportedFunction[];
|
|
103
|
+
properties: ExportedProperty[];
|
|
104
|
+
/** Absolute source path. */
|
|
105
|
+
file: string;
|
|
106
|
+
}
|
|
107
|
+
/** A `@SharedObject` type: a JS class with an optional constructor plus its `@JS` members. */
|
|
108
|
+
export interface ExportedSharedObject {
|
|
109
|
+
name: string;
|
|
110
|
+
jsName: string;
|
|
111
|
+
/** The `@JS init` parameters, or absent when there's none. At most one constructor. */
|
|
112
|
+
constructorParameters?: ExportedParameter[];
|
|
113
|
+
functions: ExportedFunction[];
|
|
114
|
+
properties: ExportedProperty[];
|
|
115
|
+
file: string;
|
|
116
|
+
}
|
|
117
|
+
/** A `@Record` type and its properties. */
|
|
118
|
+
export interface ExportedRecord {
|
|
119
|
+
name: string;
|
|
120
|
+
properties: ExportedRecordProperty[];
|
|
121
|
+
file: string;
|
|
122
|
+
}
|
|
123
|
+
/** The exported types grouped by kind. */
|
|
124
|
+
export interface ExportedSurface {
|
|
125
|
+
modules: ExportedModule[];
|
|
126
|
+
sharedObjects: ExportedSharedObject[];
|
|
127
|
+
records: ExportedRecord[];
|
|
128
|
+
}
|
|
129
|
+
/** A `#if` condition the scan couldn't answer statically. */
|
|
130
|
+
export interface ScanWarning {
|
|
131
|
+
message: string;
|
|
132
|
+
file: string;
|
|
133
|
+
line: number;
|
|
134
|
+
}
|
|
135
|
+
/** Counts describing how much work a scan did. */
|
|
136
|
+
export interface ScanStats {
|
|
137
|
+
/** `.swift` files the walk found and read. */
|
|
138
|
+
filesScanned: number;
|
|
139
|
+
/** Of those, how many contained a macro attribute and so were parsed. */
|
|
140
|
+
filesParsed: number;
|
|
141
|
+
/** Wall-clock duration of the scan, in milliseconds. */
|
|
142
|
+
durationMs: number;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* An Apple OS the scanner attributes modules to, spelled as `os(...)` spells it. The casing is part
|
|
146
|
+
* of the output contract, so consumers with a lowercase convention must fold the case themselves.
|
|
147
|
+
*/
|
|
148
|
+
export type ScannedPlatform = 'iOS' | 'macOS' | 'tvOS' | 'watchOS' | 'visionOS';
|
|
149
|
+
/** One module in the `scan-modules` output. */
|
|
150
|
+
export interface ScannedModule {
|
|
151
|
+
name: string;
|
|
152
|
+
/** The resolved JS name: the `@ExpoModule("Foo")` override, else the class name. */
|
|
153
|
+
jsName: string;
|
|
154
|
+
/**
|
|
155
|
+
* The spelled access level, or `internal` when none is written. The generated provider references
|
|
156
|
+
* the class from the app target, so anything below `public` can't be registered.
|
|
157
|
+
*/
|
|
158
|
+
accessLevel: string;
|
|
159
|
+
/**
|
|
160
|
+
* The OSes whose builds include this class, resolved from the enclosing `#if` conditions. An
|
|
161
|
+
* unconditional module lists every OS. Empty means no build is known to include it, so don't
|
|
162
|
+
* register it.
|
|
163
|
+
*/
|
|
164
|
+
platforms: ScannedPlatform[];
|
|
165
|
+
file: string;
|
|
166
|
+
}
|
|
167
|
+
/** The `scan-modules` result. */
|
|
168
|
+
export interface ScanModulesResult {
|
|
169
|
+
schemaVersion: number;
|
|
170
|
+
modules: ScannedModule[];
|
|
171
|
+
warnings: ScanWarning[];
|
|
172
|
+
stats: ScanStats;
|
|
173
|
+
}
|
|
174
|
+
/** The `scan-exports` result. */
|
|
175
|
+
export interface ScanExportsResult {
|
|
176
|
+
schemaVersion: number;
|
|
177
|
+
exports: ExportedSurface;
|
|
178
|
+
stats: ScanStats;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The output schema versions this package was written against. The wrapper compares these to the
|
|
182
|
+
* `schemaVersion` in each report and throws on a mismatch, so a binary/wrapper version skew surfaces
|
|
183
|
+
* as a clear error instead of silently misread fields.
|
|
184
|
+
*/
|
|
185
|
+
export declare const SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
|
|
186
|
+
export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 1;
|
package/build/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hand-written mirrors of the scanner's JSON output. The Swift `Codable` types in
|
|
4
|
+
* `apple/Sources/ExpoModulesScanner` are the source of truth; these are kept in sync by hand, which
|
|
5
|
+
* is why each command carries a `schemaVersion` the wrapper checks at runtime.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = exports.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* The output schema versions this package was written against. The wrapper compares these to the
|
|
11
|
+
* `schemaVersion` in each report and throws on a mismatch, so a binary/wrapper version skew surfaces
|
|
12
|
+
* as a clear error instead of silently misread fields.
|
|
13
|
+
*/
|
|
14
|
+
exports.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
|
|
15
|
+
exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 1;
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/expo-modules-macros-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Swift macro plugin for Expo modules",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
5
7
|
"license": "MIT",
|
|
6
8
|
"author": "650 Industries, Inc.",
|
|
7
9
|
"homepage": "https://github.com/expo/expo-modules-macros-plugin",
|
|
@@ -10,6 +12,14 @@
|
|
|
10
12
|
"url": "git+https://github.com/expo/expo-modules-macros-plugin.git"
|
|
11
13
|
},
|
|
12
14
|
"scripts": {
|
|
13
|
-
"build": "node apple/build.js"
|
|
15
|
+
"build": "node apple/build.js",
|
|
16
|
+
"build:ts": "tsc -p tsconfig.json",
|
|
17
|
+
"clean": "rm -rf build",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build:ts"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.11.0",
|
|
23
|
+
"typescript": "^5.4.0"
|
|
14
24
|
}
|
|
15
25
|
}
|