Implement the scan-exports CLI subcommand#25
Merged
Conversation
7a19cb0 to
0e36307
Compare
Implements the stubbed `scan-exports` command: a deep scan of the full JS-exported surface of every `@ExpoModule`, `@SharedObject`, and `@Record` type, for TypeScript type generation. Where `scan-modules` reports just enough to autolink a module, `scan-exports` descends into type bodies and captures each `@JS func` (sync/async/throws/ static), `@JS var` (readonly/static), `@JS init` parameters, and `@Record` property (optional/required). Every boundary type is parsed into a structured `TypeNode` tree (primitive/optional/array/dictionary/promise/function/ref/ unknown) carrying a `typeof` category on every node, so a generator walks a tagged tree instead of re-parsing Swift type syntax. The new `Exports/` target folder mirrors `Modules/`; the shared file walk and pre-filter move into a reusable `scanFiles` in `Core/`. The output JSON uses TS-keyword keys (`async`, `throws`, `static`, `readonly`, `optional`, `required`, `typeof`) and is a platform-neutral contract the Kotlin scanner will target too. Events, the `@Convertible(typeof:)` marker, and record-field `readonly` are left to follow-ups.
0e36307 to
293d724
Compare
Kudo
approved these changes
Jun 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the stubbed
scan-exportscommand: a deep scan of the full JS-exported surface of every@ExpoModule,@SharedObject, and@Recordtype, for TypeScript type generation.Where
scan-modulesreports just enough to autolink a module,scan-exportsdescends into type bodies and captures each@JS func,@JS var,@JS init, and@Recordproperty. Every boundary type is parsed into a structuredTypeNodetree, so a generator walks a tagged tree instead of re-parsing Swift type syntax.Layout
Exports/target folder (mirrorsModules/):ExportedSurface.swift(result types),TypeNode.swift(the structured type tree),SurfaceVisitor.swift(descends type bodies),ScanExports.swift(scanExportsplusScanner.runExports).scanFilesinCore/;scan-modulesnow layers on it.main.swiftdispatchesscan-exportstorunExports, replacing the not-yet-implemented stub.Output shape
Top-level envelope, the surface nested under
exports:{ "exports": { "modules": [ /* ExportedModule */ ], "sharedObjects": [ /* ExportedSharedObject */ ], "records": [ /* ExportedRecord */ ] }, "stats": { "filesScanned": 1, "filesParsed": 1, "durationMs": 4.2 } }Per-type:
name,jsName,functions[],properties[],fileconstructorParameters(nullwhen no@JS init)name,properties[],fileMembers (JSON keys use the TS keyword spelling):
name,jsName,parameters[],returns?(nullforVoid),async,throws,staticlabel,name,type,optional(defaulted or optional-typed)@JS var)name,jsName,type?,readonly,staticname,type,optional,required(!hasDefault && !isOptional)A record crosses the boundary by value (decoded into a fresh Swift instance, re-serialized on the way out), so every record property is inherently read-only from JS. That's a record-level fact, so it isn't stamped per property; a generator makes the whole record interface read-only.
Every boundary type is a
TypeNodecarryingkindplustypeof(thetypeofcategory:number/string/boolean/object/function) plus per-kind fields:kindprimitivename,typeofInt/Double/String/BooloptionalwrappedT?/T!/Optional<T>arrayelement[T]/Array<T>dictionarykey,value[K: V]/Dictionary<K,V>promisevaluePromise<T>functionparameters,returns?,async,throws(A) async throws -> BrefnameunknowntextA
refis resolved by the consumer against the scanned records/shared objects, a known built-in (CGSize,UIColor, etc.), or treated as unsupported. The built-in catalog lives in the generator, since convertibility is a runtime fact the syntactic scan can't see.Example
Cross-platform
The JSON shape is a platform-neutral contract: a parallel Kotlin/Android scanner will emit the identical structure.
typeofis the shared type signal;TypeNode.primitive.nameis the only intentionally source-language-specific field (SwiftInt/Bool, KotlinInt/Boolean).Follow-ups (out of scope here, all additive)
@Eventmembers (a newevents[]array on modules and shared objects)Testing
137 tests pass, including a
TypeNodeparser suite (primitives, optionals, arrays, dictionaries, promises, closures, nesting, unmodeled generics,typeofon every node) and member-extraction suites (functions, properties includingwillSet/didSetsettability, records, macro routing, scoping, end-to-end directory scan).