Skip to content

Commit 019ab51

Browse files
BridgeJS: Add configuration support with bridge-js.config.json files
1 parent 6b06a79 commit 019ab51

File tree

13 files changed

+186
-40
lines changed

13 files changed

+186
-40
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ Examples/*/package-lock.json
1212
Package.resolved
1313
Plugins/BridgeJS/Sources/JavaScript/package-lock.json
1414
Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/**/*.actual
15+
bridge-js.config.local.json

Examples/ImportTS/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55
let package = Package(
66
name: "MyApp",
77
platforms: [
8-
.macOS(.v10_15),
8+
.macOS(.v11),
99
.iOS(.v13),
1010
.tvOS(.v13),
1111
.watchOS(.v6),

Examples/ImportTS/Sources/main.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ import JavaScriptKit
22

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

99
// Get the document object - this comes from the imported getDocument() function
10-
let document = getDocument()
10+
let document = try getDocument()
1111

1212
// Access and modify properties - the title property is read/write
13-
document.title = "Hello, World!"
13+
try document.setTitle("Hello, World!")
1414

1515
// Access read-only properties - body is defined as readonly in TypeScript
16-
let body = document.body
16+
let body = try document.body
1717

1818
// Create a new element using the document.createElement method
19-
let h1 = document.createElement("h1")
19+
let h1 = try document.createElement("h1")
2020

2121
// Set properties on the created element
22-
h1.innerText = "Hello, World!"
22+
try h1.setInnerText("Hello, World!")
2323

2424
// Call methods on objects - appendChild is defined in the HTMLElement interface
25-
body.appendChild(h1)
25+
try body.appendChild(h1)
2626
}

Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
7979
executable: try context.tool(named: "BridgeJSTool").url,
8080
arguments: [
8181
"import",
82+
"--target-dir",
83+
target.directoryURL.path,
8284
"--output-skeleton",
8385
outputSkeletonPath.path,
8486
"--output-swift",

Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ extension BridgeJSCommandPlugin.Context {
127127
try runBridgeJSTool(
128128
arguments: [
129129
"import",
130+
"--target-dir",
131+
target.directoryURL.path,
130132
"--output-skeleton",
131133
generatedJavaScriptDirectory.appending(path: "BridgeJS.ImportTS.json").path,
132134
"--output-swift",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import struct Foundation.URL
2+
import struct Foundation.Data
3+
import class Foundation.FileManager
4+
import class Foundation.JSONDecoder
5+
6+
/// Configuration file representation for BridgeJS.
7+
public struct BridgeJSConfig: Codable {
8+
/// A mapping of tool names to their override paths.
9+
///
10+
/// If not present, the tool will be searched for in the system PATH.
11+
public var tools: [String: String]?
12+
13+
/// Load the configuration file from the SwiftPM package target directory.
14+
///
15+
/// Files are loaded **in this order** and merged (later files override earlier ones):
16+
/// 1. `bridge-js.config.json`
17+
/// 2. `bridge-js.config.local.json`
18+
public static func load(targetDirectory: URL) throws -> BridgeJSConfig {
19+
// Define file paths in priority order: base first, then local overrides
20+
let files = [
21+
targetDirectory.appendingPathComponent("bridge-js.config.json"),
22+
targetDirectory.appendingPathComponent("bridge-js.config.local.json"),
23+
]
24+
25+
var config = BridgeJSConfig()
26+
27+
for file in files {
28+
do {
29+
if let loaded = try loadConfig(from: file) {
30+
config = config.merging(overrides: loaded)
31+
}
32+
} catch {
33+
throw BridgeJSCoreError("Failed to parse \(file.path): \(error)")
34+
}
35+
}
36+
37+
return config
38+
}
39+
40+
/// Load a config file from the given URL if it exists, otherwise return nil
41+
private static func loadConfig(from url: URL) throws -> BridgeJSConfig? {
42+
guard FileManager.default.fileExists(atPath: url.path) else {
43+
return nil
44+
}
45+
let data = try Data(contentsOf: url)
46+
return try JSONDecoder().decode(BridgeJSConfig.self, from: data)
47+
}
48+
49+
/// Merge the current configuration with the overrides.
50+
func merging(overrides: BridgeJSConfig) -> BridgeJSConfig {
51+
return BridgeJSConfig(
52+
tools: (tools ?? [:]).merging(overrides.tools ?? [:], uniquingKeysWith: { $1 })
53+
)
54+
}
55+
}

Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
@preconcurrency import var Foundation.stderr
44
@preconcurrency import struct Foundation.URL
55
@preconcurrency import struct Foundation.Data
6+
@preconcurrency import struct Foundation.ObjCBool
67
@preconcurrency import class Foundation.JSONEncoder
78
@preconcurrency import class Foundation.FileManager
89
@preconcurrency import class Foundation.JSONDecoder
10+
@preconcurrency import class Foundation.ProcessInfo
911
import SwiftParser
1012

1113
#if canImport(BridgeJSCore)
@@ -50,7 +52,7 @@ import TS2Skeleton
5052
do {
5153
try run()
5254
} catch {
53-
printStderr("Error: \(error)")
55+
printStderr("error: \(error)")
5456
exit(1)
5557
}
5658
}
@@ -83,6 +85,10 @@ import TS2Skeleton
8385
help: "Print verbose output",
8486
required: false
8587
),
88+
"target-dir": OptionRule(
89+
help: "The SwiftPM package target directory",
90+
required: true
91+
),
8692
"output-swift": OptionRule(help: "The output file path for the Swift source code", required: true),
8793
"output-skeleton": OptionRule(
8894
help: "The output file path for the skeleton of the imported TypeScript APIs",
@@ -99,6 +105,9 @@ import TS2Skeleton
99105
)
100106
let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true")
101107
var importer = ImportTS(progress: progress, moduleName: doubleDashOptions["module-name"]!)
108+
let targetDirectory = URL(fileURLWithPath: doubleDashOptions["target-dir"]!)
109+
let config = try BridgeJSConfig.load(targetDirectory: targetDirectory)
110+
let nodePath: URL = try config.findTool("node", targetDirectory: targetDirectory)
102111
for inputFile in positionalArguments {
103112
if inputFile.hasSuffix(".json") {
104113
let sourceURL = URL(fileURLWithPath: inputFile)
@@ -109,7 +118,7 @@ import TS2Skeleton
109118
importer.addSkeleton(skeleton)
110119
} else if inputFile.hasSuffix(".d.ts") {
111120
let tsconfigPath = URL(fileURLWithPath: doubleDashOptions["project"]!)
112-
try importer.addSourceFile(inputFile, tsconfigPath: tsconfigPath.path)
121+
try importer.addSourceFile(inputFile, tsconfigPath: tsconfigPath.path, nodePath: nodePath)
113122
}
114123
}
115124

Plugins/BridgeJS/Sources/TS2Skeleton/TS2Skeleton.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import BridgeJSSkeleton
2222
internal func which(
2323
_ executable: String,
2424
environment: [String: String] = ProcessInfo.processInfo.environment
25-
) throws -> URL {
25+
) -> URL? {
2626
func checkCandidate(_ candidate: URL) -> Bool {
2727
var isDirectory: ObjCBool = false
2828
let fileExists = FileManager.default.fileExists(atPath: candidate.path, isDirectory: &isDirectory)
@@ -51,13 +51,38 @@ internal func which(
5151
return url
5252
}
5353
}
54-
throw BridgeJSCoreError("Executable \(executable) not found in PATH")
54+
return nil
55+
}
56+
57+
extension BridgeJSConfig {
58+
/// Find a tool from the system PATH, using environment variable override, or bridge-js.config.json
59+
public func findTool(_ name: String, targetDirectory: URL) throws -> URL {
60+
if let tool = tools?[name] {
61+
return URL(fileURLWithPath: tool)
62+
}
63+
if let url = which(name) {
64+
return url
65+
}
66+
67+
// Emit a helpful error message with a suggestion to create a local config override.
68+
throw BridgeJSCoreError(
69+
"""
70+
Executable "\(name)" not found in PATH. \
71+
Hint: Try setting the JAVASCRIPTKIT_\(name.uppercased().replacingOccurrences(of: "-", with: "_"))_EXEC environment variable, \
72+
or create a local config override with:
73+
echo '{ "tools": { "\(name)": "'$(which \(name))'" } }' > \(targetDirectory.appendingPathComponent("bridge-js.config.local.json").path)
74+
"""
75+
)
76+
}
5577
}
5678

5779
extension ImportTS {
5880
/// Processes a TypeScript definition file and extracts its API information
59-
public mutating func addSourceFile(_ sourceFile: String, tsconfigPath: String) throws {
60-
let nodePath = try which("node")
81+
public mutating func addSourceFile(
82+
_ sourceFile: String,
83+
tsconfigPath: String,
84+
nodePath: URL
85+
) throws {
6186
let ts2skeletonPath = URL(fileURLWithPath: #filePath)
6287
.deletingLastPathComponent()
6388
.appendingPathComponent("JavaScript")

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SwiftParser
44
import Testing
55
@testable import BridgeJSLink
66
@testable import BridgeJSCore
7+
@testable import TS2Skeleton
78

89
@Suite struct BridgeJSLinkTests {
910
private func snapshot(
@@ -65,7 +66,8 @@ import Testing
6566
let tsconfigPath = url.deletingLastPathComponent().appendingPathComponent("tsconfig.json")
6667

6768
var importTS = ImportTS(progress: .silent, moduleName: "TestModule")
68-
try importTS.addSourceFile(url.path, tsconfigPath: tsconfigPath.path)
69+
let nodePath = try #require(which("node"))
70+
try importTS.addSourceFile(url.path, tsconfigPath: tsconfigPath.path, nodePath: nodePath)
6971
let name = url.deletingPathExtension().deletingPathExtension().lastPathComponent
7072

7173
let encoder = JSONEncoder()

Plugins/BridgeJS/Tests/BridgeJSToolTests/ImportTSTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import Foundation
1818
func snapshot(input: String) throws {
1919
var api = ImportTS(progress: .silent, moduleName: "Check")
2020
let url = Self.inputsDirectory.appendingPathComponent(input)
21+
let nodePath = try #require(which("node"))
2122
let tsconfigPath = url.deletingLastPathComponent().appendingPathComponent("tsconfig.json")
22-
try api.addSourceFile(url.path, tsconfigPath: tsconfigPath.path)
23+
try api.addSourceFile(url.path, tsconfigPath: tsconfigPath.path, nodePath: nodePath)
2324
let outputSwift = try #require(try api.finalize())
2425
let name = url.deletingPathExtension().deletingPathExtension().deletingPathExtension().lastPathComponent
2526
try assertSnapshot(

0 commit comments

Comments
 (0)