Skip to content

Commit 6f3ab1c

Browse files
committed
BridgeJS: Add additionalSourceDirs config for cross-plugin file discovery
1 parent a98e49d commit 6f3ab1c

File tree

3 files changed

+104
-3
lines changed

3 files changed

+104
-3
lines changed

Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
3232
inputFiles.append(configFile)
3333
}
3434

35+
let additionalDirs = resolveAdditionalSourceDirs(targetDirectory: target.directoryURL)
36+
for dir in additionalDirs {
37+
inputFiles.append(contentsOf: recursivelyCollectSwiftFiles(from: dir))
38+
}
39+
3540
let inputTSFile = target.directoryURL.appending(path: "bridge-js.d.ts")
3641
let tsconfigPath = context.package.directoryURL.appending(path: "tsconfig.json")
3742

@@ -47,7 +52,6 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
4752
]
4853

4954
if FileManager.default.fileExists(atPath: inputTSFile.path) {
50-
// Add .d.ts file and tsconfig.json as inputs
5155
inputFiles.append(contentsOf: [inputTSFile, tsconfigPath])
5256
arguments.append(contentsOf: [
5357
"--project",
@@ -66,4 +70,52 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
6670
)
6771
}
6872
}
73+
74+
private struct PluginConfig: Decodable {
75+
var additionalSourceDirs: [String]?
76+
}
77+
78+
private func resolveAdditionalSourceDirs(targetDirectory: URL) -> [URL] {
79+
let configFiles = [
80+
targetDirectory.appending(path: "bridge-js.config.json"),
81+
targetDirectory.appending(path: "bridge-js.config.local.json"),
82+
]
83+
var dirs: [String] = []
84+
for file in configFiles {
85+
guard FileManager.default.fileExists(atPath: file.path),
86+
let data = try? Data(contentsOf: file),
87+
let config = try? JSONDecoder().decode(PluginConfig.self, from: data),
88+
let additional = config.additionalSourceDirs else { continue }
89+
dirs.append(contentsOf: additional)
90+
}
91+
return dirs.compactMap { dir in
92+
let resolved = targetDirectory.appending(path: dir).standardized
93+
var isDirectory: ObjCBool = false
94+
guard FileManager.default.fileExists(atPath: resolved.path, isDirectory: &isDirectory),
95+
isDirectory.boolValue else {
96+
return nil
97+
}
98+
return resolved
99+
}
100+
}
101+
102+
private func recursivelyCollectSwiftFiles(from directory: URL) -> [URL] {
103+
var swiftFiles: [URL] = []
104+
guard let enumerator = FileManager.default.enumerator(
105+
at: directory,
106+
includingPropertiesForKeys: [.isRegularFileKey],
107+
options: [.skipsHiddenFiles]
108+
) else {
109+
return []
110+
}
111+
for case let fileURL as URL in enumerator {
112+
if fileURL.pathExtension == "swift" {
113+
let resourceValues = try? fileURL.resourceValues(forKeys: [.isRegularFileKey])
114+
if resourceValues?.isRegularFile == true {
115+
swiftFiles.append(fileURL)
116+
}
117+
}
118+
}
119+
return swiftFiles.sorted { $0.path < $1.path }
120+
}
69121
#endif

Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ public struct BridgeJSCoreError: Swift.Error, CustomStringConvertible {
309309

310310
import struct Foundation.URL
311311
import struct Foundation.Data
312+
import struct Foundation.ObjCBool
312313
import class Foundation.FileManager
313314
import class Foundation.JSONDecoder
314315

@@ -328,20 +329,33 @@ public struct BridgeJSConfig: Codable {
328329
/// Default: `false`
329330
public var exposeToGlobal: Bool
330331

331-
public init(tools: [String: String]? = nil, exposeToGlobal: Bool = false) {
332+
/// Additional directories containing Swift source files to scan for
333+
/// `@JS` annotations.
334+
///
335+
/// Paths are resolved relative to the target directory. This is useful
336+
/// when Swift files with `@JS` annotations are generated by another
337+
/// build plugin whose outputs aren't included in `target.sourceFiles`.
338+
///
339+
/// Default: `nil` (no additional directories)
340+
public var additionalSourceDirs: [String]?
341+
342+
public init(tools: [String: String]? = nil, exposeToGlobal: Bool = false, additionalSourceDirs: [String]? = nil) {
332343
self.tools = tools
333344
self.exposeToGlobal = exposeToGlobal
345+
self.additionalSourceDirs = additionalSourceDirs
334346
}
335347

336348
enum CodingKeys: String, CodingKey {
337349
case tools
338350
case exposeToGlobal
351+
case additionalSourceDirs
339352
}
340353

341354
public init(from decoder: Decoder) throws {
342355
let container = try decoder.container(keyedBy: CodingKeys.self)
343356
tools = try container.decodeIfPresent([String: String].self, forKey: .tools)
344357
exposeToGlobal = try container.decodeIfPresent(Bool.self, forKey: .exposeToGlobal) ?? false
358+
additionalSourceDirs = try container.decodeIfPresent([String].self, forKey: .additionalSourceDirs)
345359
}
346360

347361
/// Load the configuration file from the SwiftPM package target directory.
@@ -380,11 +394,35 @@ public struct BridgeJSConfig: Codable {
380394
return try JSONDecoder().decode(BridgeJSConfig.self, from: data)
381395
}
382396

397+
/// Resolve additional source directories relative to a base URL.
398+
///
399+
/// Returns absolute URLs for each configured additional source directory.
400+
/// Directories that don't exist are silently skipped.
401+
public func resolveAdditionalSourceDirs(relativeTo baseURL: URL) -> [URL] {
402+
guard let dirs = additionalSourceDirs else { return [] }
403+
return dirs.compactMap { dir in
404+
let resolved = baseURL.appending(path: dir).standardized
405+
var isDirectory: ObjCBool = false
406+
guard FileManager.default.fileExists(atPath: resolved.path, isDirectory: &isDirectory),
407+
isDirectory.boolValue else {
408+
return nil
409+
}
410+
return resolved
411+
}
412+
}
413+
383414
/// Merge the current configuration with the overrides.
384415
func merging(overrides: BridgeJSConfig) -> BridgeJSConfig {
416+
let mergedDirs: [String]? = {
417+
let base = self.additionalSourceDirs ?? []
418+
let extra = overrides.additionalSourceDirs ?? []
419+
let combined = base + extra
420+
return combined.isEmpty ? nil : combined
421+
}()
385422
return BridgeJSConfig(
386423
tools: (tools ?? [:]).merging(overrides.tools ?? [:], uniquingKeysWith: { $1 }),
387-
exposeToGlobal: overrides.exposeToGlobal
424+
exposeToGlobal: overrides.exposeToGlobal,
425+
additionalSourceDirs: mergedDirs
388426
)
389427
}
390428
}

Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ import BridgeJSUtilities
146146
var inputFiles = withSpan("Collecting Swift files") {
147147
return inputSwiftFiles(targetDirectory: targetDirectory, positionalArguments: positionalArguments)
148148
}
149+
150+
let additionalDirs = config.resolveAdditionalSourceDirs(relativeTo: targetDirectory)
151+
for dir in additionalDirs {
152+
let additionalFiles = recursivelyCollectSwiftFiles(from: dir).map(\.path)
153+
.filter { !inputFiles.contains($0) }
154+
if !additionalFiles.isEmpty {
155+
progress.print("Found \(additionalFiles.count) additional Swift files in \(dir.lastPathComponent)")
156+
}
157+
inputFiles.append(contentsOf: additionalFiles)
158+
}
159+
149160
// BridgeJS.Macros.swift contains imported declarations (@JSFunction, @JSClass, etc.) that need
150161
// to be processed by SwiftToSkeleton to populate the imported skeleton. The command plugin
151162
// filters out Generated/ files, so we explicitly add it here after generation.

0 commit comments

Comments
 (0)