From 4291dbbd82fef130aa3db5fd83310246995948f4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 26 Jan 2026 14:54:18 +0900 Subject: [PATCH] BridgeJS: Add internal debug tool for inspecting intermediate stages --- Plugins/BridgeJS/Package.swift | 13 +- Plugins/BridgeJS/README.md | 20 +++ .../Sources/BridgeJSLink/BridgeJSLink.swift | 6 +- .../BridgeJSToolInternal.swift | 137 ++++++++++++++++++ 4 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift diff --git a/Plugins/BridgeJS/Package.swift b/Plugins/BridgeJS/Package.swift index a4385bd3..eb675c86 100644 --- a/Plugins/BridgeJS/Package.swift +++ b/Plugins/BridgeJS/Package.swift @@ -7,7 +7,9 @@ let package = Package( name: "BridgeJS", platforms: [.macOS(.v13)], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1") + .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1"), + // Development dependencies + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.0"), ], targets: [ .target(name: "BridgeJSBuildPlugin"), @@ -71,5 +73,14 @@ let package = Package( .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ), + + .executableTarget( + name: "BridgeJSToolInternal", + dependencies: [ + "BridgeJSCore", + "BridgeJSLink", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), ] ) diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md index 29d7df2e..0e84674a 100644 --- a/Plugins/BridgeJS/README.md +++ b/Plugins/BridgeJS/README.md @@ -163,6 +163,26 @@ Return values use direct Wasm returns for primitives, and imported intrinsic fun For detailed semantics, see the [How It Works sections](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-class#How-It-Works) in the user documentation. +## Debug utilities + +`BridgeJSToolInternal` exposes pipeline stages for debugging: + +- `emit-skeleton` - Parse Swift files (or `-` for stdin) and print the BridgeJS skeleton as JSON. +- `emit-swift-thunks` — Read skeleton JSON (from a file or `-` for stdin) and print the generated Swift glue (export and import thunks). +- `emit-js` / `emit-dts` - Read skeleton JSON files (or `-` for stdin) and print the .js/.d.ts + +Use these to inspect parser output and generated code without running the full generate/link pipeline. + +```console +$ cat < Int +@JS class Bar { + @JS init() {} + @JS func baz() {} +} +EOS +``` + ## Future Work - [ ] Cast between TS interface diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 1f55eb18..625bfc36 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -7,12 +7,12 @@ import BridgeJSSkeleton import BridgeJSUtilities #endif -struct BridgeJSLink { +public struct BridgeJSLink { var skeletons: [BridgeJSSkeleton] = [] let sharedMemory: Bool private let namespaceBuilder = NamespaceBuilder() - init( + public init( skeletons: [BridgeJSSkeleton] = [], sharedMemory: Bool ) { @@ -1035,7 +1035,7 @@ struct BridgeJSLink { return printer.lines.joined(separator: "\n") } - func link() throws -> (outputJs: String, outputDts: String) { + public func link() throws -> (outputJs: String, outputDts: String) { let data = try collectLinkData() let outputJs = try generateJavaScript(data: data) let outputDts = generateTypeScript(data: data) diff --git a/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift b/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift new file mode 100644 index 00000000..8a6a6b7b --- /dev/null +++ b/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift @@ -0,0 +1,137 @@ +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import class Foundation.JSONEncoder +@preconcurrency import class Foundation.JSONDecoder +@preconcurrency import class Foundation.FileHandle +import SwiftParser +import SwiftSyntax + +import BridgeJSCore +import BridgeJSSkeleton +import BridgeJSLink +import BridgeJSUtilities + +import ArgumentParser + +@main struct BridgeJSToolInternal: ParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "bridge-js-tool-internal", + abstract: "BridgeJS Tool Internal", + version: "0.1.0", + subcommands: [ + EmitSkeleton.self, + EmitSwiftThunks.self, + EmitJS.self, + EmitDTS.self, + ] + ) + + static func readData(from file: String) throws -> Data { + if file == "-" { + return try FileHandle.standardInput.readToEnd() ?? Data() + } else { + return try Data(contentsOf: URL(fileURLWithPath: file)) + } + } + + struct EmitSkeleton: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "emit-skeleton", + abstract: "Emit the BridgeJS skeleton", + ) + + @Argument(help: "The input files to emit the BridgeJS skeleton from") + var inputFiles: [String] + + func run() throws { + let swiftToSkeleton = SwiftToSkeleton( + progress: ProgressReporting(verbose: false), + moduleName: "InternalModule", + exposeToGlobal: false + ) + for inputFile in inputFiles.sorted() { + let content = try String(decoding: readData(from: inputFile), as: UTF8.self) + if BridgeJSGeneratedFile.hasSkipComment(content) { + continue + } + let sourceFile = Parser.parse(source: content) + swiftToSkeleton.addSourceFile(sourceFile, inputFilePath: inputFile) + } + let skeleton = try swiftToSkeleton.finalize() + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let skeletonData = try encoder.encode(skeleton) + print(String(data: skeletonData, encoding: .utf8)!) + } + } + + struct EmitSwiftThunks: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "emit-swift-thunks", + abstract: "Emit the Swift thunks", + ) + @Argument(help: "The skeleton file to emit the Swift thunks from") + var skeletonFile: String + + func run() throws { + let skeletonData = try readData(from: skeletonFile) + let skeleton = try JSONDecoder().decode(BridgeJSSkeleton.self, from: skeletonData) + let moduleName = "InternalModule" + let exported = try skeleton.exported.flatMap { + try ExportSwift( + progress: ProgressReporting(verbose: false), + moduleName: moduleName, + skeleton: $0 + ).finalize() + } + let imported = try skeleton.imported.flatMap { + try ImportTS( + progress: ProgressReporting(verbose: false), + moduleName: moduleName, + skeleton: $0 + ).finalize() + } + let combinedSwift = [exported, imported].compactMap { $0 } + print(combinedSwift.joined(separator: "\n\n")) + } + } + + static func linkSkeletons(skeletonFiles: [String]) throws -> (outputJs: String, outputDts: String) { + var skeletons: [BridgeJSSkeleton] = [] + for skeletonFile in skeletonFiles.sorted() { + let skeletonData = try readData(from: skeletonFile) + skeletons.append(try JSONDecoder().decode(BridgeJSSkeleton.self, from: skeletonData)) + } + let link = BridgeJSLink(skeletons: skeletons, sharedMemory: false) + return try link.link() + } + + struct EmitJS: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "emit-js", + abstract: "Emit the JavaScript glue code", + ) + @Argument(help: "The skeleton files to emit the JavaScript glue code from") + var skeletonFiles: [String] + + func run() throws { + let (outputJs, _) = try linkSkeletons(skeletonFiles: skeletonFiles) + print(outputJs) + } + } + + struct EmitDTS: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "emit-dts", + abstract: "Emit the TypeScript type definitions", + ) + @Argument(help: "The skeleton files to emit the TypeScript type definitions from") + var skeletonFiles: [String] + + func run() throws { + let (_, outputDts) = try linkSkeletons(skeletonFiles: skeletonFiles) + print(outputDts) + } + } +}