Skip to content

BridgeJS: Require placing bridge-js.config.json in target directory #370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 13, 2025
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
2 changes: 1 addition & 1 deletion Benchmarks/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
.executableTarget(
name: "Benchmarks",
dependencies: ["JavaScriptKit"],
exclude: ["Generated/JavaScript", "bridge.d.ts"],
exclude: ["Generated/JavaScript", "bridge-js.d.ts"],
swiftSettings: [
.enableExperimentalFeature("Extern")
]
Expand Down
1 change: 1 addition & 0 deletions Benchmarks/Sources/bridge-js.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
File renamed without changes.
4 changes: 2 additions & 2 deletions Examples/ImportTS/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import JavaScriptKit

// This function is automatically generated by the @JS plugin
// It demonstrates how to use TypeScript functions and types imported from bridge.d.ts
// It demonstrates how to use TypeScript functions and types imported from bridge-js.d.ts
@JS public func run() {
// Call the imported consoleLog function defined in bridge.d.ts
// Call the imported consoleLog function defined in bridge-js.d.ts
consoleLog("Hello, World!")

// Get the document object - this comes from the imported getDocument() function
Expand Down
8 changes: 4 additions & 4 deletions Plugins/BridgeJS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ graph LR
A.swift --> E1[[bridge-js export]]
B.swift --> E1
E1 --> G1[ExportSwift.swift]
B1[bridge.d.ts]-->I1[[bridge-js import]]
B1[bridge-js.d.ts]-->I1[[bridge-js import]]
I1 --> G2[ImportTS.swift]
end
I1 --> G4[ImportTS.json]
Expand All @@ -32,7 +32,7 @@ graph LR
C.swift --> E2[[bridge-js export]]
D.swift --> E2
E2 --> G5[ExportSwift.swift]
B2[bridge.d.ts]-->I2[[bridge-js import]]
B2[bridge-js.d.ts]-->I2[[bridge-js import]]
I2 --> G6[ImportTS.swift]
end
I2 --> G8[ImportTS.json]
Expand All @@ -42,8 +42,8 @@ graph LR
G7 --> L1
G8 --> L1

L1 --> F1[bridge.js]
L1 --> F2[bridge.d.ts]
L1 --> F1[bridge-js.js]
L1 --> F2[bridge-js.d.ts]
ModuleA -----> App[App.wasm]
ModuleB -----> App

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,32 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
guard let swiftSourceModuleTarget = target as? SwiftSourceModuleTarget else {
return []
}
return try [
createExportSwiftCommand(context: context, target: swiftSourceModuleTarget),
createImportTSCommand(context: context, target: swiftSourceModuleTarget),
]
var commands: [Command] = []
commands.append(try createExportSwiftCommand(context: context, target: swiftSourceModuleTarget))
if let importCommand = try createImportTSCommand(context: context, target: swiftSourceModuleTarget) {
commands.append(importCommand)
}
return commands
}

private func pathToConfigFile(target: SwiftSourceModuleTarget) -> URL {
return target.directoryURL.appending(path: "bridge-js.config.json")
}

private func createExportSwiftCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command {
let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.swift")
let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.json")
let inputFiles = target.sourceFiles.filter { !$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/") }
.map(\.url)
let inputSwiftFiles = target.sourceFiles.filter {
!$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/")
}
.map(\.url)
let configFile = pathToConfigFile(target: target)
let inputFiles: [URL]
if FileManager.default.fileExists(atPath: configFile.path) {
inputFiles = inputSwiftFiles + [configFile]
} else {
inputFiles = inputSwiftFiles
}
return .buildCommand(
displayName: "Export Swift API",
executable: try context.tool(named: "BridgeJSTool").url,
Expand All @@ -31,21 +46,32 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
outputSkeletonPath.path,
"--output-swift",
outputSwiftPath.path,
// Generate the output files even if nothing is exported not to surprise
// the build system.
"--always-write", "true",
] + inputFiles.map(\.path),
] + inputSwiftFiles.map(\.path),
inputFiles: inputFiles,
outputFiles: [
outputSwiftPath
]
)
}

private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command {
private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command? {
let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.swift")
let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.json")
let inputFiles = [
target.directoryURL.appending(path: "bridge.d.ts")
]
let inputTSFile = target.directoryURL.appending(path: "bridge-js.d.ts")
guard FileManager.default.fileExists(atPath: inputTSFile.path) else {
return nil
}

let configFile = pathToConfigFile(target: target)
let inputFiles: [URL]
if FileManager.default.fileExists(atPath: configFile.path) {
inputFiles = [inputTSFile, configFile]
} else {
inputFiles = [inputTSFile]
}
return .buildCommand(
displayName: "Import TypeScript API",
executable: try context.tool(named: "BridgeJSTool").url,
Expand All @@ -57,10 +83,13 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
outputSwiftPath.path,
"--module-name",
target.name,
// Generate the output files even if nothing is imported not to surprise
// the build system.
"--always-write", "true",
"--project",
context.package.directoryURL.appending(path: "tsconfig.json").path,
] + inputFiles.map(\.path),
inputTSFile.path,
],
inputFiles: inputFiles,
outputFiles: [
outputSwiftPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ struct BridgeJSCommandPlugin: CommandPlugin {

struct Options {
var targets: [String]
var verbose: Bool

static func parse(extractor: inout ArgumentExtractor) -> Options {
let targets = extractor.extractOption(named: "target")
return Options(targets: targets)
let verbose = extractor.extractFlag(named: "verbose")
return Options(targets: targets, verbose: verbose != 0)
}

static func help() -> String {
Expand All @@ -29,13 +31,13 @@ struct BridgeJSCommandPlugin: CommandPlugin {
OPTIONS:
--target <target> Specify target(s) to generate bridge code for. If omitted,
generates for all targets with JavaScriptKit dependency.
--verbose Print verbose output.
"""
}
}

func performCommand(context: PluginContext, arguments: [String]) throws {
// Check for help flags to display usage information
// This allows users to run `swift package plugin bridge-js --help` to understand the plugin's functionality
if arguments.contains(where: { ["-h", "--help"].contains($0) }) {
printStderr(Options.help())
return
Expand All @@ -45,83 +47,103 @@ struct BridgeJSCommandPlugin: CommandPlugin {
let options = Options.parse(extractor: &extractor)
let remainingArguments = extractor.remainingArguments

let context = Context(options: options, context: context)

if options.targets.isEmpty {
try runOnTargets(
context: context,
try context.runOnTargets(
remainingArguments: remainingArguments,
where: { target in
target.hasDependency(named: Self.JAVASCRIPTKIT_PACKAGE_NAME)
}
)
} else {
try runOnTargets(
context: context,
try context.runOnTargets(
remainingArguments: remainingArguments,
where: { options.targets.contains($0.name) }
)
}
}

private func runOnTargets(
context: PluginContext,
struct Context {
let options: Options
let context: PluginContext
}
}

extension BridgeJSCommandPlugin.Context {
func runOnTargets(
remainingArguments: [String],
where predicate: (SwiftSourceModuleTarget) -> Bool
) throws {
for target in context.package.targets {
guard let target = target as? SwiftSourceModuleTarget else {
continue
}
let configFilePath = target.directoryURL.appending(path: "bridge-js.config.json")
if !FileManager.default.fileExists(atPath: configFilePath.path) {
printVerbose("No bridge-js.config.json found for \(target.name), skipping...")
continue
}
guard predicate(target) else {
continue
}
try runSingleTarget(context: context, target: target, remainingArguments: remainingArguments)
try runSingleTarget(target: target, remainingArguments: remainingArguments)
}
}

private func runSingleTarget(
context: PluginContext,
target: SwiftSourceModuleTarget,
remainingArguments: [String]
) throws {
Diagnostics.progress("Exporting Swift API for \(target.name)...")
printStderr("Generating bridge code for \(target.name)...")

printVerbose("Exporting Swift API for \(target.name)...")

let generatedDirectory = target.directoryURL.appending(path: "Generated")
let generatedJavaScriptDirectory = generatedDirectory.appending(path: "JavaScript")

try runBridgeJSTool(
context: context,
arguments: [
"export",
"--output-skeleton",
generatedJavaScriptDirectory.appending(path: "ExportSwift.json").path,
"--output-swift",
generatedDirectory.appending(path: "ExportSwift.swift").path,
"--verbose",
options.verbose ? "true" : "false",
]
+ target.sourceFiles.filter {
!$0.url.path.hasPrefix(generatedDirectory.path + "/")
}.map(\.url.path) + remainingArguments
)

try runBridgeJSTool(
context: context,
arguments: [
"import",
"--output-skeleton",
generatedJavaScriptDirectory.appending(path: "ImportTS.json").path,
"--output-swift",
generatedDirectory.appending(path: "ImportTS.swift").path,
"--module-name",
target.name,
"--project",
context.package.directoryURL.appending(path: "tsconfig.json").path,
target.directoryURL.appending(path: "bridge.d.ts").path,
] + remainingArguments
)
printVerbose("Importing TypeScript API for \(target.name)...")

let bridgeDtsPath = target.directoryURL.appending(path: "bridge-js.d.ts")
// Execute import only if bridge-js.d.ts exists
if FileManager.default.fileExists(atPath: bridgeDtsPath.path) {
try runBridgeJSTool(
arguments: [
"import",
"--output-skeleton",
generatedJavaScriptDirectory.appending(path: "ImportTS.json").path,
"--output-swift",
generatedDirectory.appending(path: "ImportTS.swift").path,
"--verbose",
options.verbose ? "true" : "false",
"--module-name",
target.name,
"--project",
context.package.directoryURL.appending(path: "tsconfig.json").path,
bridgeDtsPath.path,
] + remainingArguments
)
}
}

private func runBridgeJSTool(context: PluginContext, arguments: [String]) throws {
private func runBridgeJSTool(arguments: [String]) throws {
let tool = try context.tool(named: "BridgeJSTool").url
printStderr("$ \(tool.path) \(arguments.joined(separator: " "))")
printVerbose("$ \(tool.path) \(arguments.joined(separator: " "))")
let process = Process()
process.executableURL = tool
process.arguments = arguments
Expand All @@ -133,6 +155,12 @@ struct BridgeJSCommandPlugin: CommandPlugin {
exit(process.terminationStatus)
}
}

private func printVerbose(_ message: String) {
if options.verbose {
printStderr(message)
}
}
}

private func printStderr(_ message: String) {
Expand Down
17 changes: 15 additions & 2 deletions Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import SwiftParser
"""
)
}
let progress = ProgressReporting()
switch subcommand {
case "import":
let parser = ArgumentParser(
Expand All @@ -71,6 +70,10 @@ import SwiftParser
help: "Always write the output files even if no APIs are imported",
required: false
),
"verbose": OptionRule(
help: "Print verbose output",
required: false
),
"output-swift": OptionRule(help: "The output file path for the Swift source code", required: true),
"output-skeleton": OptionRule(
help: "The output file path for the skeleton of the imported TypeScript APIs",
Expand All @@ -85,6 +88,7 @@ import SwiftParser
let (positionalArguments, _, doubleDashOptions) = try parser.parse(
arguments: Array(arguments.dropFirst())
)
let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true")
var importer = ImportTS(progress: progress, moduleName: doubleDashOptions["module-name"]!)
for inputFile in positionalArguments {
if inputFile.hasSuffix(".json") {
Expand Down Expand Up @@ -145,11 +149,16 @@ import SwiftParser
help: "Always write the output files even if no APIs are exported",
required: false
),
"verbose": OptionRule(
help: "Print verbose output",
required: false
),
]
)
let (positionalArguments, _, doubleDashOptions) = try parser.parse(
arguments: Array(arguments.dropFirst())
)
let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true")
let exporter = ExportSwift(progress: progress)
for inputFile in positionalArguments {
let sourceURL = URL(fileURLWithPath: inputFile)
Expand Down Expand Up @@ -253,7 +262,11 @@ private func printStderr(_ message: String) {
struct ProgressReporting {
let print: (String) -> Void

init(print: @escaping (String) -> Void = { Swift.print($0) }) {
init(verbose: Bool) {
self.init(print: verbose ? { Swift.print($0) } : { _ in })
}

private init(print: @escaping (String) -> Void) {
self.print = print
}

Expand Down
2 changes: 1 addition & 1 deletion Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ExportSwift {
private var exportedClasses: [ExportedClass] = []
private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver()

init(progress: ProgressReporting = ProgressReporting()) {
init(progress: ProgressReporting) {
self.progress = progress
}

Expand Down
Loading