@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.
Files changed (29) hide show
  1. package/.github/resources/expo-modules-macros.svg +23 -0
  2. package/.github/workflows/publish.yml +4 -0
  3. package/.github/workflows/swift.yml +6 -0
  4. package/README.md +119 -0
  5. package/apple/ExpoModulesMacros-tool +0 -0
  6. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +3 -1
  7. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +17 -0
  8. package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
  9. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +78 -2
  10. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +89 -2
  11. package/apple/Sources/ExpoModulesMacros/Plugin.swift +3 -0
  12. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -47
  13. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +8 -2
  14. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +6 -0
  15. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +365 -0
  16. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +487 -0
  17. package/apple/Sources/ExpoModulesScanner/CLI.swift +8 -4
  18. package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +4 -4
  19. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +28 -7
  20. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +3 -9
  21. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +17 -7
  22. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +7 -0
  23. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +1 -0
  24. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +89 -22
  25. package/build/index.d.ts +51 -0
  26. package/build/index.js +153 -0
  27. package/build/types.d.ts +186 -0
  28. package/build/types.js +15 -0
  29. 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 = .init(platform: nil, defines: [])
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. These hold build products, dependencies, and
104
- /// git internals never source worth scanning — and pruning them keeps the walk from descending
105
- /// into the bulk of a monorepo's files. `node_modules` makes any npm package root safe to pass as a
106
- /// scan path: nested dependencies are separate packages and get scanned on their own.
107
- private let prunedDirectoryNames: Set<String> = [".build", "Pods", ".git", "node_modules"]
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
  }
@@ -41,6 +41,7 @@ func scanExports(paths: [String]) -> ScanExportsResult {
41
41
  }
42
42
 
43
43
  return ScanExportsResult(
44
+ schemaVersion: scanExportsSchemaVersion,
44
45
  exports: ExportedSurface(modules: modules, sharedObjects: sharedObjects, records: records),
45
46
  stats: stats
46
47
  )
@@ -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
- /// `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
+ /// `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, and the file it's in.
35
- /// The richer fields the visitor captures (declaration kind, raw macro arguments, line/column) are
36
- /// dropped here — they're redundant for this command (the macro is always `@ExpoModule` on a class)
37
- /// and the deep `scan-exports` surface carries the richer per-member detail instead.
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
- let scanModulesSchemaVersion = 1
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
- /// Scans the given paths for top-level `@ExpoModule` types and returns the modules (in file then
77
- /// source order) plus the stats for the run the `scan-modules` command. Kept separate from the
78
- /// public entry (and `internal`) so tests can drive it without going through argv/stdout.
79
- func scanModules(paths: [String], configuration: ScanBuildConfiguration = .init(platform: nil, defines: [])) -> ScanModulesResult {
80
- let scan = collectDetections(paths: paths, macros: [.expoModule], configuration: configuration)
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
- let modules = scan.detections.map {
83
- // Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")` override, else the
84
- // class name.
85
- ScannedModule(name: $0.name, jsName: $0.jsName ?? $0.name, accessLevel: $0.accessLevel, file: $0.file)
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: scan.warnings,
92
- stats: scan.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
+ }
@@ -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
+ }
@@ -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.9.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
  }