Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Plugins/BridgeJS/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
]
),
]
)
20 changes: 20 additions & 0 deletions Plugins/BridgeJS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOS | ./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal emit-skeleton - | ./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal emit-dts -
@JSFunction func foo() throws(JSException) -> Int
@JS class Bar {
@JS init() {}
@JS func baz() {}
}
EOS
```

## Future Work

- [ ] Cast between TS interface
Expand Down
6 changes: 3 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}