diff --git a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json
index 0bddddfb6..8e9c1cd6b 100644
--- a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json
+++ b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json
@@ -3415,5 +3415,8 @@
}
]
},
- "moduleName" : "Benchmarks"
+ "moduleName" : "Benchmarks",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Examples/MultiModule/Package.swift b/Examples/MultiModule/Package.swift
new file mode 100644
index 000000000..870e2a5e8
--- /dev/null
+++ b/Examples/MultiModule/Package.swift
@@ -0,0 +1,38 @@
+// swift-tools-version:6.0
+
+import PackageDescription
+
+let package = Package(
+ name: "MultiModule",
+ platforms: [
+ .macOS(.v14)
+ ],
+ dependencies: [.package(name: "JavaScriptKit", path: "../../")],
+ targets: [
+ .target(
+ name: "Core",
+ dependencies: [
+ "JavaScriptKit"
+ ],
+ swiftSettings: [
+ .enableExperimentalFeature("Extern")
+ ],
+ plugins: [
+ .plugin(name: "BridgeJS", package: "JavaScriptKit")
+ ]
+ ),
+ .executableTarget(
+ name: "MultiModule",
+ dependencies: [
+ "Core",
+ "JavaScriptKit",
+ ],
+ swiftSettings: [
+ .enableExperimentalFeature("Extern")
+ ],
+ plugins: [
+ .plugin(name: "BridgeJS", package: "JavaScriptKit")
+ ]
+ ),
+ ]
+)
diff --git a/Examples/MultiModule/README.md b/Examples/MultiModule/README.md
new file mode 100644
index 000000000..741394d6f
--- /dev/null
+++ b/Examples/MultiModule/README.md
@@ -0,0 +1,17 @@
+# MultiModule Example
+
+This example demonstrates using `@JS` types defined in one module (`Core`) from another module (`App`) within the same Swift package.
+
+## Building and Running
+
+1. Build the project:
+ ```sh
+ swift package --swift-sdk $SWIFT_SDK_ID js --use-cdn
+ ```
+
+2. Serve the files:
+ ```sh
+ npx serve
+ ```
+
+Then open your browser to `http://localhost:3000`.
diff --git a/Examples/MultiModule/Sources/Core/Vector3D.swift b/Examples/MultiModule/Sources/Core/Vector3D.swift
new file mode 100644
index 000000000..988892bcc
--- /dev/null
+++ b/Examples/MultiModule/Sources/Core/Vector3D.swift
@@ -0,0 +1,17 @@
+import JavaScriptKit
+
+@JS public struct Vector3D {
+ public let x: Double
+ public let y: Double
+ public let z: Double
+
+ @JS public init(x: Double, y: Double, z: Double) {
+ self.x = x
+ self.y = y
+ self.z = z
+ }
+
+ @JS public func magnitude() -> Double {
+ (x * x + y * y + z * z).squareRoot()
+ }
+}
diff --git a/Examples/MultiModule/Sources/Core/bridge-js.config.json b/Examples/MultiModule/Sources/Core/bridge-js.config.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Examples/MultiModule/Sources/Core/bridge-js.config.json
@@ -0,0 +1 @@
+{}
diff --git a/Examples/MultiModule/Sources/MultiModule/bridge-js.config.json b/Examples/MultiModule/Sources/MultiModule/bridge-js.config.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Examples/MultiModule/Sources/MultiModule/bridge-js.config.json
@@ -0,0 +1 @@
+{}
diff --git a/Examples/MultiModule/Sources/MultiModule/main.swift b/Examples/MultiModule/Sources/MultiModule/main.swift
new file mode 100644
index 000000000..dd238c00d
--- /dev/null
+++ b/Examples/MultiModule/Sources/MultiModule/main.swift
@@ -0,0 +1,6 @@
+import Core
+import JavaScriptKit
+
+@JS public func currentVelocity() -> Vector3D {
+ Vector3D(x: 0.1, y: 0.2, z: 0.3)
+}
diff --git a/Examples/MultiModule/index.html b/Examples/MultiModule/index.html
new file mode 100644
index 000000000..d9066820f
--- /dev/null
+++ b/Examples/MultiModule/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ MultiModule Example
+
+
+
+
+
+
+
diff --git a/Examples/MultiModule/index.js b/Examples/MultiModule/index.js
new file mode 100644
index 000000000..83e79f16d
--- /dev/null
+++ b/Examples/MultiModule/index.js
@@ -0,0 +1,10 @@
+import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
+const { exports } = await init({});
+
+const velocity = exports.currentVelocity();
+
+const output = document.createElement("pre");
+output.innerText =
+ `currentVelocity() = (${velocity.x}, ${velocity.y}, ${velocity.z})\n`
+ + `magnitude = ${velocity.magnitude()}`;
+document.body.appendChild(output);
diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json
index 1a21916ee..89ce6ae3a 100644
--- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json
+++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json
@@ -300,5 +300,8 @@
}
]
},
- "moduleName" : "PlayBridgeJS"
+ "moduleName" : "PlayBridgeJS",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/main.swift b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/main.swift
index ec9eda774..a30eb3b06 100644
--- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/main.swift
+++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/main.swift
@@ -45,7 +45,12 @@ import class Foundation.JSONDecoder
func _update(swiftSource: String, dtsSource: String) throws -> PlayBridgeJSOutput {
let moduleName = "Playground"
- let swiftToSkeleton = SwiftToSkeleton(progress: .silent, moduleName: moduleName, exposeToGlobal: false)
+ let swiftToSkeleton = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: moduleName,
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
swiftToSkeleton.addSourceFile(Parser.parse(source: swiftSource), inputFilePath: "Playground.swift")
let ts2swift = try createTS2Swift()
diff --git a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift
index 3cb6dc860..b78f5f9a6 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift
@@ -63,6 +63,19 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
])
}
+ for skeleton in dependencySkeletons(context: context, target: target) {
+ arguments.append(contentsOf: [
+ "--dependency-skeleton",
+ "\(skeleton.moduleName)=\(skeleton.skeletonURL.path)",
+ ])
+ // We have to use the Swift file, not the skeleton, as the input file,
+ // since we can’t make the skeleton file an output file without it being
+ // treated as a resource by the build system (and thus included in the
+ // resource bundle). We need to use something as the inputFile to maintain
+ // correct ordering.
+ inputFiles.append(skeleton.bridgeJSSwiftURL)
+ }
+
let allSwiftFiles = inputSwiftFiles + pluginGeneratedSwiftFiles
arguments.append(contentsOf: allSwiftFiles.map(\.path))
@@ -74,5 +87,56 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
outputFiles: [outputSwiftPath]
)
}
+
+ private struct DependencySkeleton {
+ let moduleName: String
+ let skeletonURL: URL
+ let bridgeJSSwiftURL: URL
+ }
+
+ /// We only read skeletons from dependencies with a `bridge-js.config.json` file.
+ /// For the build system to correctly order the plugins, we need to set the skeleton
+ /// files as input. However, I don’t think we have enough information here to determine
+ /// whether the plugin which generates this is applied to the dependency, so we use
+ /// the presence of `bridge-js.config.json` instead.
+ private func dependencySkeletons(
+ context: PluginContext,
+ target: SwiftSourceModuleTarget
+ ) -> [DependencySkeleton] {
+ let localTargets: [SwiftSourceModuleTarget] = target.recursiveTargetDependencies
+ .compactMap { dependency in
+ guard
+ let swiftTarget = dependency as? SwiftSourceModuleTarget,
+ context.package.targets.contains(where: { $0.id == swiftTarget.id }),
+ FileManager.default.fileExists(atPath: pathToConfigFile(target: swiftTarget).path)
+ else {
+ return nil
+ }
+ return swiftTarget
+ }
+
+ var skeletons: [DependencySkeleton] = []
+ var seenTargetNames = Set()
+ for swiftTarget in localTargets where seenTargetNames.insert(swiftTarget.name).inserted {
+ let skeletonURL = BridgeJSPluginPaths.skeletonURL(
+ targetName: swiftTarget.name,
+ packageID: context.package.id,
+ buildPluginWorkDirectoryURL: context.pluginWorkDirectoryURL
+ )
+ let bridgeJSSwiftURL = BridgeJSPluginPaths.bridgeJSSwiftURL(
+ targetName: swiftTarget.name,
+ packageID: context.package.id,
+ buildPluginWorkDirectoryURL: context.pluginWorkDirectoryURL
+ )
+ skeletons.append(
+ DependencySkeleton(
+ moduleName: swiftTarget.name,
+ skeletonURL: skeletonURL,
+ bridgeJSSwiftURL: bridgeJSSwiftURL
+ )
+ )
+ }
+ return skeletons
+ }
}
#endif
diff --git a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSPluginUtilities b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSPluginUtilities
new file mode 120000
index 000000000..2daaa446e
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSPluginUtilities
@@ -0,0 +1 @@
+../BridgeJSPluginUtilities
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
index 23fbae567..24b96f53d 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
@@ -74,28 +74,62 @@ struct BridgeJSCommandPlugin: CommandPlugin {
}
extension BridgeJSCommandPlugin.Context {
- func runOnTargets(
- remainingArguments: [String],
- where predicate: (SwiftSourceModuleTarget) -> Bool
- ) throws {
+ private func collectBridgeJSTargets() -> [String: SwiftSourceModuleTarget] {
+ var bridgeJSTargets: [String: SwiftSourceModuleTarget] = [:]
for target in context.package.targets {
- guard let target = target as? SwiftSourceModuleTarget else {
+ guard
+ let swiftTarget = target as? SwiftSourceModuleTarget,
+ FileManager.default.fileExists(
+ atPath: swiftTarget.directoryURL.appending(path: "bridge-js.config.json").path
+ )
+ 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
+ bridgeJSTargets[swiftTarget.name] = swiftTarget
+ }
+ return bridgeJSTargets
+ }
+
+ private func targetsInDependencyOrder(
+ _ bridgeJSTargets: [String: SwiftSourceModuleTarget]
+ ) -> [SwiftSourceModuleTarget] {
+ var visitedTargetNames = Set()
+ var orderedTargets: [SwiftSourceModuleTarget] = []
+ func visit(_ target: SwiftSourceModuleTarget) {
+ if !visitedTargetNames.insert(target.name).inserted {
+ return
}
- guard predicate(target) else {
- continue
+ for dependency in target.recursiveTargetDependencies {
+ if let dependencyTarget = bridgeJSTargets[dependency.name] {
+ visit(dependencyTarget)
+ }
}
- try runSingleTarget(target: target, remainingArguments: remainingArguments)
+ orderedTargets.append(target)
+ }
+ for target in bridgeJSTargets.values.sorted(by: { $0.name < $1.name }) {
+ visit(target)
+ }
+ return orderedTargets
+ }
+
+ func runOnTargets(
+ remainingArguments: [String],
+ where predicate: (SwiftSourceModuleTarget) -> Bool
+ ) throws {
+ let allBridgeJSTargets = collectBridgeJSTargets()
+ let requestedTargets = allBridgeJSTargets.filter { predicate($1) }
+ for target in targetsInDependencyOrder(requestedTargets) {
+ try runSingleTarget(
+ target: target,
+ bridgeJSTargets: allBridgeJSTargets,
+ remainingArguments: remainingArguments
+ )
}
}
private func runSingleTarget(
target: SwiftSourceModuleTarget,
+ bridgeJSTargets: [String: SwiftSourceModuleTarget],
remainingArguments: [String]
) throws {
printStderr("Generating bridge code for \(target.name)...")
@@ -126,6 +160,25 @@ extension BridgeJSCommandPlugin.Context {
])
}
+ for dependency in target.recursiveTargetDependencies {
+ guard let dependencyTarget = bridgeJSTargets[dependency.name] else { continue }
+ let dependencySkeletonPath = dependencyTarget.directoryURL
+ .appending(path: "Generated/JavaScript/BridgeJS.json")
+ guard FileManager.default.fileExists(atPath: dependencySkeletonPath.path) else {
+ throw BridgeJSCommandPluginError(
+ """
+ Dependency '\(dependencyTarget.name)' is configured for BridgeJS, but its AOT skeleton has not been generated yet. \
+ Run `swift package bridge-js --target \(dependencyTarget.name)` to generate it first, \
+ or run without `--target` to process in dependency order.
+ """
+ )
+ }
+ generateArguments.append(contentsOf: [
+ "--dependency-skeleton",
+ "\(dependencyTarget.name)=\(dependencySkeletonPath.path)",
+ ])
+ }
+
generateArguments.append(
contentsOf: target.sourceFiles.filter {
!$0.url.path.hasPrefix(generatedDirectory.path + "/")
@@ -162,6 +215,14 @@ private func printStderr(_ message: String) {
fputs(message + "\n", stderr)
}
+struct BridgeJSCommandPluginError: Error, CustomStringConvertible {
+ let description: String
+
+ init(_ message: String) {
+ self.description = message
+ }
+}
+
extension SwiftSourceModuleTarget {
func hasDependency(named name: String) -> Bool {
return dependencies.contains(where: {
diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExternalModuleIndex.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExternalModuleIndex.swift
new file mode 100644
index 000000000..91fd4388a
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExternalModuleIndex.swift
@@ -0,0 +1,93 @@
+#if canImport(BridgeJSSkeleton)
+import BridgeJSSkeleton
+#endif
+
+/// Index of `@JS` types from dependencies.
+public struct ExternalModuleIndex {
+ public struct ExternalType: Equatable {
+ public let moduleName: String
+ public let bridgeType: BridgeType
+ }
+
+ public enum LookupResult: Equatable {
+ case unique(ExternalType)
+ case ambiguous(candidates: [ExternalType])
+ }
+
+ public static var empty: ExternalModuleIndex {
+ ExternalModuleIndex(dependencies: [])
+ }
+
+ private let byModuleAndPath: [String: [String: ExternalType]]
+ private let byPath: [String: [ExternalType]]
+
+ public var moduleNames: Set { Set(byModuleAndPath.keys) }
+
+ public init(dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)]) {
+ var entriesByModule: [String: [String: ExternalType]] = [:]
+ var entriesByDotPath: [String: [ExternalType]] = [:]
+
+ for (moduleName, skeleton) in dependencies {
+ guard let exported = skeleton.exported else { continue }
+ var moduleEntries = entriesByModule[moduleName] ?? [:]
+
+ func register(dotPath: String, bridgeType: BridgeType) {
+ let externalType = ExternalType(moduleName: moduleName, bridgeType: bridgeType)
+ if moduleEntries[dotPath] == nil {
+ moduleEntries[dotPath] = externalType
+ entriesByDotPath[dotPath, default: []].append(externalType)
+ }
+ }
+
+ for klass in exported.classes {
+ register(dotPath: klass.swiftCallName, bridgeType: .swiftHeapObject(klass.swiftCallName))
+ }
+ for structDef in exported.structs {
+ register(dotPath: structDef.swiftCallName, bridgeType: .swiftStruct(structDef.swiftCallName))
+ }
+ for enumDef in exported.enums {
+ let bridgeType: BridgeType
+ switch enumDef.enumType {
+ case .simple:
+ bridgeType = .caseEnum(enumDef.swiftCallName)
+ case .rawValue:
+ guard let rawType = enumDef.rawType else { continue }
+ bridgeType = .rawValueEnum(enumDef.swiftCallName, rawType)
+ case .associatedValue:
+ bridgeType = .associatedValueEnum(enumDef.swiftCallName)
+ case .namespace:
+ bridgeType = .namespaceEnum(enumDef.swiftCallName)
+ }
+ register(dotPath: enumDef.swiftCallName, bridgeType: bridgeType)
+ }
+ for proto in exported.protocols {
+ register(dotPath: proto.name, bridgeType: .swiftProtocol(proto.name))
+ }
+
+ entriesByModule[moduleName] = moduleEntries
+ }
+
+ self.byModuleAndPath = entriesByModule
+ self.byPath = entriesByDotPath
+ }
+
+ public var isEmpty: Bool { byModuleAndPath.isEmpty }
+
+ public func isKnownModule(_ name: String) -> Bool {
+ byModuleAndPath[name] != nil
+ }
+
+ public func lookup(dotPath: String) -> LookupResult? {
+ guard let matches = byPath[dotPath], !matches.isEmpty else { return nil }
+ if matches.count == 1 {
+ return .unique(matches[0])
+ }
+ return .ambiguous(candidates: matches)
+ }
+
+ public func lookup(dotPath: String, module moduleName: String) -> LookupResult? {
+ guard let moduleEntries = byModuleAndPath[moduleName] else { return nil }
+ guard let externalType = moduleEntries[dotPath] else { return nil }
+ return .unique(externalType)
+ }
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
index 3d8f417e3..42ba2fbaa 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
@@ -18,15 +18,25 @@ public final class SwiftToSkeleton {
public let exposeToGlobal: Bool
public let identityMode: String?
- private var sourceFiles: [(sourceFile: SourceFileSyntax, inputFilePath: String)] = []
let typeDeclResolver: TypeDeclResolver
+ let externalModuleIndex: ExternalModuleIndex
- public init(progress: ProgressReporting, moduleName: String, exposeToGlobal: Bool, identityMode: String? = nil) {
+ private var sourceFiles: [(sourceFile: SourceFileSyntax, inputFilePath: String)] = []
+ private var usedExternalModules = Set()
+
+ public init(
+ progress: ProgressReporting,
+ moduleName: String,
+ exposeToGlobal: Bool,
+ externalModuleIndex: ExternalModuleIndex,
+ identityMode: String? = nil
+ ) {
self.progress = progress
self.moduleName = moduleName
self.exposeToGlobal = exposeToGlobal
self.identityMode = identityMode
self.typeDeclResolver = TypeDeclResolver()
+ self.externalModuleIndex = externalModuleIndex
// Index known types provided by JavaScriptKit
self.typeDeclResolver.addSourceFile(
@@ -110,7 +120,12 @@ public final class SwiftToSkeleton {
}()
let exportedSkeleton: ExportedSkeleton? = exported.isEmpty ? nil : exported
- return BridgeJSSkeleton(moduleName: moduleName, exported: exportedSkeleton, imported: importedSkeleton)
+ return BridgeJSSkeleton(
+ moduleName: moduleName,
+ exported: exportedSkeleton,
+ imported: importedSkeleton,
+ usedExternalModules: usedExternalModules.sorted()
+ )
}
func lookupType(for type: TypeSyntax, errors: inout [DiagnosticError]) -> BridgeType? {
@@ -303,79 +318,130 @@ public final class SwiftToSkeleton {
return primitiveType
}
- guard let typeDecl = typeDeclResolver.resolve(type) else {
- errors.append(
- DiagnosticError(
- node: type,
- message: "Unsupported type '\(type.trimmedDescription)'.",
- hint: "Only primitive types and types defined in the same module are allowed"
- )
- )
- return nil
- }
-
- if typeDecl.is(ProtocolDeclSyntax.self) {
- let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: typeDecl, itemName: typeDecl.name.text)
- return .swiftProtocol(swiftCallName)
- }
+ if let typeDecl = typeDeclResolver.resolve(type) {
+ if typeDecl.is(ProtocolDeclSyntax.self) {
+ let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: typeDecl, itemName: typeDecl.name.text)
+ return .swiftProtocol(swiftCallName)
+ }
- if let enumDecl = typeDecl.as(EnumDeclSyntax.self) {
- let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: enumDecl, itemName: enumDecl.name.text)
- let rawTypeString = enumDecl.inheritanceClause?.inheritedTypes.first { inheritedType in
- let typeName = inheritedType.type.trimmedDescription
- return ExportSwiftConstants.supportedRawTypes.contains(typeName)
- }?.type.trimmedDescription
+ if let enumDecl = typeDecl.as(EnumDeclSyntax.self) {
+ let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: enumDecl, itemName: enumDecl.name.text)
+ let rawTypeString = enumDecl.inheritanceClause?.inheritedTypes.first { inheritedType in
+ let typeName = inheritedType.type.trimmedDescription
+ return ExportSwiftConstants.supportedRawTypes.contains(typeName)
+ }?.type.trimmedDescription
- if let rawType = SwiftEnumRawType(rawTypeString) {
- return .rawValueEnum(swiftCallName, rawType)
- } else {
- let hasAnyCases = enumDecl.memberBlock.members.contains { member in
- member.decl.is(EnumCaseDeclSyntax.self)
- }
- if !hasAnyCases {
- return .namespaceEnum(swiftCallName)
- }
- let hasAssociatedValues =
- enumDecl.memberBlock.members.contains { member in
- guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { return false }
- return caseDecl.elements.contains { element in
- if let params = element.parameterClause?.parameters {
- return !params.isEmpty
+ if let rawType = SwiftEnumRawType(rawTypeString) {
+ return .rawValueEnum(swiftCallName, rawType)
+ } else {
+ let hasAnyCases = enumDecl.memberBlock.members.contains { member in
+ member.decl.is(EnumCaseDeclSyntax.self)
+ }
+ if !hasAnyCases {
+ return .namespaceEnum(swiftCallName)
+ }
+ let hasAssociatedValues =
+ enumDecl.memberBlock.members.contains { member in
+ guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { return false }
+ return caseDecl.elements.contains { element in
+ if let params = element.parameterClause?.parameters {
+ return !params.isEmpty
+ }
+ return false
}
- return false
}
+ if hasAssociatedValues {
+ return .associatedValueEnum(swiftCallName)
+ } else {
+ return .caseEnum(swiftCallName)
}
- if hasAssociatedValues {
- return .associatedValueEnum(swiftCallName)
- } else {
- return .caseEnum(swiftCallName)
}
}
- }
- if let structDecl = typeDecl.as(StructDeclSyntax.self) {
- let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: structDecl, itemName: structDecl.name.text)
- if structDecl.attributes.hasAttribute(name: "JSClass") {
+ if let structDecl = typeDecl.as(StructDeclSyntax.self) {
+ let swiftCallName = SwiftToSkeleton.computeSwiftCallName(
+ for: structDecl,
+ itemName: structDecl.name.text
+ )
+ if structDecl.attributes.hasAttribute(name: "JSClass") {
+ return .jsObject(swiftCallName)
+ }
+ return .swiftStruct(swiftCallName)
+ }
+
+ guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else {
+ return nil
+ }
+ let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: typeDecl, itemName: typeDecl.name.text)
+
+ // A type annotated with @JSClass is a JavaScript object wrapper (imported),
+ // even if it is declared as a Swift class.
+ if let classDecl = typeDecl.as(ClassDeclSyntax.self), classDecl.attributes.hasAttribute(name: "JSClass") {
+ return .jsObject(swiftCallName)
+ }
+ if let actorDecl = typeDecl.as(ActorDeclSyntax.self), actorDecl.attributes.hasAttribute(name: "JSClass") {
return .jsObject(swiftCallName)
}
- return .swiftStruct(swiftCallName)
+
+ return .swiftHeapObject(swiftCallName)
+ }
+
+ if let externalType = resolveExternal(for: type, errors: &errors) {
+ return externalType
}
- guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else {
+ errors.append(
+ DiagnosticError(
+ node: type,
+ message: "Unsupported type '\(type.trimmedDescription)'.",
+ hint:
+ "Only primitive types, types defined in the same module, and "
+ + "`@JS` types from dependency targets that apply the BridgeJS plugin are allowed"
+ )
+ )
+ return nil
+ }
+
+ private func resolveExternal(for type: TypeSyntax, errors: inout [DiagnosticError]) -> BridgeType? {
+ guard
+ !externalModuleIndex.isEmpty,
+ var components = typeDeclResolver.qualifiedComponents(from: type)
+ else {
return nil
}
- let swiftCallName = SwiftToSkeleton.computeSwiftCallName(for: typeDecl, itemName: typeDecl.name.text)
- // A type annotated with @JSClass is a JavaScript object wrapper (imported),
- // even if it is declared as a Swift class.
- if let classDecl = typeDecl.as(ClassDeclSyntax.self), classDecl.attributes.hasAttribute(name: "JSClass") {
- return .jsObject(swiftCallName)
+ var scopedModule: String? = nil
+ if components.count >= 2, externalModuleIndex.isKnownModule(components[0]) {
+ scopedModule = components[0]
+ components.removeFirst()
}
- if let actorDecl = typeDecl.as(ActorDeclSyntax.self), actorDecl.attributes.hasAttribute(name: "JSClass") {
- return .jsObject(swiftCallName)
+
+ let dotPath = components.joined(separator: ".")
+ let lookupResult: ExternalModuleIndex.LookupResult?
+ if let moduleName = scopedModule {
+ lookupResult = externalModuleIndex.lookup(dotPath: dotPath, module: moduleName)
+ } else {
+ lookupResult = externalModuleIndex.lookup(dotPath: dotPath)
}
- return .swiftHeapObject(swiftCallName)
+ guard let lookupResult else { return nil }
+ switch lookupResult {
+ case .unique(let externalType):
+ usedExternalModules.insert(externalType.moduleName)
+ return externalType.bridgeType
+ case .ambiguous(let candidates):
+ let moduleNames = candidates.map(\.moduleName).sorted().joined(separator: ", ")
+ errors.append(
+ DiagnosticError(
+ node: type,
+ message: "ambiguous use of '\(type.trimmedDescription)'",
+ hint:
+ "'\(dotPath)' is exported by multiple dependency modules: \(moduleNames). "
+ + "Qualify with a module name (e.g. '.\(dotPath)') to disambiguate."
+ )
+ )
+ return nil
+ }
}
fileprivate static func parseUnsafePointerType(_ type: TypeSyntax) -> UnsafePointerType? {
diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift
index 975c0c9dc..ec04421aa 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift
@@ -161,7 +161,7 @@ class TypeDeclResolver {
return nil
}
- private func qualifiedComponents(from type: TypeSyntax) -> QualifiedName? {
+ func qualifiedComponents(from type: TypeSyntax) -> QualifiedName? {
if let m = type.as(MemberTypeSyntax.self) {
guard let base = qualifiedComponents(from: TypeSyntax(m.baseType)) else { return nil }
return base + [m.name.text]
diff --git a/Plugins/BridgeJS/Sources/BridgeJSPluginUtilities/PluginPaths.swift b/Plugins/BridgeJS/Sources/BridgeJSPluginUtilities/PluginPaths.swift
new file mode 100644
index 000000000..f95d1adaa
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSPluginUtilities/PluginPaths.swift
@@ -0,0 +1,74 @@
+#if canImport(PackagePlugin)
+import struct Foundation.URL
+
+enum BridgeJSPluginPaths {
+ static func skeletonURL(
+ targetName: String,
+ packageID: String,
+ buildPluginWorkDirectoryURL workDirectoryURL: URL
+ ) -> URL {
+ let pluginsOutputsRootURL =
+ workDirectoryURL
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ return bridgeJSDirectoryURL(
+ targetName: targetName,
+ packageID: packageID,
+ pluginsOutputsRootURL: pluginsOutputsRootURL
+ )
+ .appending(path: "JavaScript/BridgeJS.json")
+ }
+
+ static func skeletonURL(
+ targetName: String,
+ packageID: String,
+ commandPluginWorkDirectoryURL workDirectoryURL: URL
+ ) -> URL {
+ // workDirectoryURL: ".build/plugins/PackageToJS/outputs/"
+ // .build/plugins/outputs/[package]/[target]/destination/BridgeJS/JavaScript/BridgeJS.json
+ let pluginsOutputsRootURL =
+ workDirectoryURL
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .appending(path: "outputs")
+ return bridgeJSDirectoryURL(
+ targetName: targetName,
+ packageID: packageID,
+ pluginsOutputsRootURL: pluginsOutputsRootURL
+ )
+ .appending(path: "JavaScript/BridgeJS.json")
+ }
+
+ static func bridgeJSSwiftURL(
+ targetName: String,
+ packageID: String,
+ buildPluginWorkDirectoryURL workDirectoryURL: URL
+ ) -> URL {
+ let pluginsOutputsRootURL =
+ workDirectoryURL
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ return bridgeJSDirectoryURL(
+ targetName: targetName,
+ packageID: packageID,
+ pluginsOutputsRootURL: pluginsOutputsRootURL
+ )
+ .appending(path: "BridgeJS.swift")
+ }
+
+ private static func bridgeJSDirectoryURL(
+ targetName: String,
+ packageID: String,
+ pluginsOutputsRootURL: URL
+ ) -> URL {
+ pluginsOutputsRootURL
+ .appending(path: packageID)
+ .appending(path: targetName)
+ .appending(path: "destination/BridgeJS")
+ }
+}
+#endif
diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
index 03e6d676a..baf6debd9 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
@@ -1239,11 +1239,18 @@ public struct BridgeJSSkeleton: Codable {
public let moduleName: String
public let exported: ExportedSkeleton?
public let imported: ImportedModuleSkeleton?
+ public let usedExternalModules: [String]
- public init(moduleName: String, exported: ExportedSkeleton? = nil, imported: ImportedModuleSkeleton? = nil) {
+ public init(
+ moduleName: String,
+ exported: ExportedSkeleton? = nil,
+ imported: ImportedModuleSkeleton? = nil,
+ usedExternalModules: [String] = []
+ ) {
self.moduleName = moduleName
self.exported = exported
self.imported = imported
+ self.usedExternalModules = usedExternalModules
}
}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
index 3e3f27ea1..005af04a8 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
@@ -112,10 +112,18 @@ import BridgeJSUtilities
help: "The path to the TypeScript project configuration file (required for .d.ts files)",
required: false
),
+ "dependency-skeleton": OptionRule(
+ help: "Path to a dependency module's BridgeJS.json, as '='. Repeatable.",
+ required: false,
+ repeatable: true
+ ),
]
)
- let (positionalArguments, _, doubleDashOptions) = try parser.parse(
- arguments: Array(arguments.dropFirst())
+ let parsedArguments = try parser.parse(arguments: Array(arguments.dropFirst()))
+ let positionalArguments = parsedArguments.positionalArguments
+ let doubleDashOptions = parsedArguments.doubleDashOptions
+ let dependencySkeletons = try loadDependencySkeletons(
+ parsedArguments.repeatedDoubleDashOptions["dependency-skeleton", default: []]
)
let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true")
let moduleName = doubleDashOptions["module-name"]!
@@ -162,10 +170,12 @@ import BridgeJSUtilities
inputFiles.append(macrosPath)
}
}
+ let externalModuleIndex = ExternalModuleIndex(dependencies: dependencySkeletons)
let swiftToSkeleton = SwiftToSkeleton(
progress: progress,
moduleName: moduleName,
exposeToGlobal: config.exposeToGlobal,
+ externalModuleIndex: externalModuleIndex,
identityMode: config.identityMode
)
for inputFile in inputFiles.sorted() {
@@ -224,7 +234,10 @@ import BridgeJSUtilities
// Combine and write unified Swift output
let outputSwiftURL = outputDirectory.appending(path: "BridgeJS.swift")
let combinedSwift = [closureSupport, exportResult, importResult].compactMap { $0 }
- let outputSwift = combineGeneratedSwift(combinedSwift)
+ let outputSwift = combineGeneratedSwift(
+ combinedSwift,
+ importingExternalModules: skeleton.usedExternalModules
+ )
let shouldWrite = doubleDashOptions["always-write"] == "true" || !outputSwift.isEmpty
if shouldWrite {
try withSpan("Writing output Swift") {
@@ -237,7 +250,14 @@ import BridgeJSUtilities
}
}
- // Write unified skeleton
+ // Write unified skeleton.
+ // Note that for the build system to sequence the BridgeJSBuildPlugin correctly,
+ // the skeleton-to-Swift-output mapping must be injective, i.e. any change to
+ // the skeleton must produce a change in the Swift output. This is because we
+ // can’t use the BridgeJS.json file as an outputFile, since it would then be
+ // treated as a resource and thus included in the generated bundle. The
+ // invariant currently holds, but if this ever changes the BridgeJS.swift file
+ // could include a hash of the skeleton to maintain it.
let outputSkeletonURL = outputDirectory.appending(path: "JavaScript/BridgeJS.json")
try withSpan("Writing output skeleton") {
try FileManager.default.createDirectory(
@@ -302,14 +322,45 @@ private func writeIfChanged(_ data: Data, to url: URL) throws {
try data.write(to: url)
}
-private func combineGeneratedSwift(_ pieces: [String]) -> String {
+private func loadDependencySkeletons(
+ _ rawArguments: [String]
+) throws -> [(moduleName: String, skeleton: BridgeJSSkeleton)] {
+ var loadedSkeletons: [(moduleName: String, skeleton: BridgeJSSkeleton)] = []
+ let decoder = JSONDecoder()
+ for rawArgument in rawArguments {
+ guard let equalIndex = rawArgument.firstIndex(of: "="), equalIndex != rawArgument.startIndex else {
+ throw BridgeJSToolError(
+ "--dependency-skeleton expects '='; got invalid value '\(rawArgument)'"
+ )
+ }
+ let moduleName = String(rawArgument[.. String {
let trimmedPieces =
pieces
.map { $0.trimmingCharacters(in: .newlines) }
.filter { !$0.isEmpty }
guard !trimmedPieces.isEmpty else { return "" }
- return ([BridgeJSGeneratedFile.swiftPreamble] + trimmedPieces).joined(separator: "\n\n")
+ let imports = BridgeJSGeneratedFile.swiftImports(["JavaScriptKit"] + externalModules)
+ return ([BridgeJSGeneratedFile.swiftHeader, imports] + trimmedPieces).joined(separator: "\n\n")
}
private func recursivelyCollectSwiftFiles(from directory: URL) -> [URL] {
@@ -365,6 +416,7 @@ extension Profiling {
struct OptionRule {
var help: String
var required: Bool = false
+ var repeatable: Bool = false
}
struct ArgumentParser {
@@ -377,11 +429,12 @@ struct ArgumentParser {
self.doubleDashOptions = doubleDashOptions
}
- typealias ParsedArguments = (
- positionalArguments: [String],
- singleDashOptions: [String: String],
- doubleDashOptions: [String: String]
- )
+ struct ParsedArguments {
+ var positionalArguments: [String]
+ var singleDashOptions: [String: String]
+ var doubleDashOptions: [String: String]
+ var repeatedDoubleDashOptions: [String: [String]]
+ }
func help() -> String {
var help = "Usage: \(CommandLine.arguments.first ?? "bridge-js-tool") [options] \n\n"
@@ -404,6 +457,7 @@ struct ArgumentParser {
var positionalArguments: [String] = []
var singleDashOptions: [String: String] = [:]
var doubleDashOptions: [String: String] = [:]
+ var repeatedDoubleDashOptions: [String: [String]] = [:]
var arguments = arguments.makeIterator()
@@ -412,7 +466,12 @@ struct ArgumentParser {
if arg.starts(with: "--") {
let key = String(arg.dropFirst(2))
let value = arguments.next()
- doubleDashOptions[key] = value
+ if self.doubleDashOptions[key]?.repeatable ?? false {
+ guard let value else { continue }
+ repeatedDoubleDashOptions[key, default: []].append(value)
+ } else {
+ doubleDashOptions[key] = value
+ }
} else {
let key = String(arg.dropFirst(1))
let value = arguments.next()
@@ -424,11 +483,16 @@ struct ArgumentParser {
}
for (key, rule) in self.doubleDashOptions {
- if rule.required, doubleDashOptions[key] == nil {
+ if rule.required, doubleDashOptions[key] == nil, repeatedDoubleDashOptions[key] == nil {
throw BridgeJSToolError("Option --\(key) is required")
}
}
- return (positionalArguments, singleDashOptions, doubleDashOptions)
+ return ParsedArguments(
+ positionalArguments: positionalArguments,
+ singleDashOptions: singleDashOptions,
+ doubleDashOptions: doubleDashOptions,
+ repeatedDoubleDashOptions: repeatedDoubleDashOptions
+ )
}
}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift b/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift
index 8a6a6b7b4..f4de24093 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSToolInternal/BridgeJSToolInternal.swift
@@ -48,7 +48,8 @@ import ArgumentParser
let swiftToSkeleton = SwiftToSkeleton(
progress: ProgressReporting(verbose: false),
moduleName: "InternalModule",
- exposeToGlobal: false
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
)
for inputFile in inputFiles.sorted() {
let content = try String(decoding: readData(from: inputFile), as: UTF8.self)
diff --git a/Plugins/BridgeJS/Sources/BridgeJSUtilities/Utilities.swift b/Plugins/BridgeJS/Sources/BridgeJSUtilities/Utilities.swift
index 68c07f225..8a4171718 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSUtilities/Utilities.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSUtilities/Utilities.swift
@@ -13,7 +13,7 @@ public enum BridgeJSGeneratedFile {
content.starts(with: skipLine + "\n")
}
- public static var swiftPreamble: String {
+ public static var swiftHeader: String {
// The generated Swift file itself should not be processed by BridgeJS again.
"""
\(skipLine)
@@ -22,10 +22,12 @@ public enum BridgeJSGeneratedFile {
//
// To update this file, just rebuild your project or run
// `swift package bridge-js`.
-
- @_spi(BridgeJS) import JavaScriptKit
"""
}
+
+ public static func swiftImports(_ moduleNames: [String]) -> String {
+ moduleNames.map { "@_spi(BridgeJS) import \($0)" }.joined(separator: "\n")
+ }
}
/// A printer for code fragments.
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift
index dd0ce5d03..8b8e8b8a2 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift
@@ -77,7 +77,12 @@ import Testing
let url = Self.inputsDirectory.appendingPathComponent(input)
let name = url.deletingPathExtension().lastPathComponent
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
swiftAPI.addSourceFile(sourceFile, inputFilePath: input)
let skeleton = try swiftAPI.finalize()
try snapshotCodegen(skeleton: skeleton, name: name)
@@ -93,7 +98,12 @@ import Testing
let url = Self.inputsDirectory.appendingPathComponent(input)
let name = url.deletingPathExtension().lastPathComponent
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: true)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: true,
+ externalModuleIndex: .empty
+ )
swiftAPI.addSourceFile(sourceFile, inputFilePath: input)
let skeleton = try swiftAPI.finalize()
try snapshotCodegen(skeleton: skeleton, name: name + ".Global")
@@ -101,7 +111,12 @@ import Testing
@Test
func codegenCrossFileTypeResolution() throws {
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
let classBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassB.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: classBURL, encoding: .utf8)),
@@ -118,7 +133,12 @@ import Testing
@Test
func codegenCrossFileTypeResolutionReverseOrder() throws {
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
let classAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassA.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: classAURL, encoding: .utf8)),
@@ -135,7 +155,12 @@ import Testing
@Test
func codegenCrossFileFunctionTypes() throws {
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
let functionBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionB.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: functionBURL, encoding: .utf8)),
@@ -152,7 +177,12 @@ import Testing
@Test
func codegenCrossFileFunctionTypesReverseOrder() throws {
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
let functionAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionA.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: functionAURL, encoding: .utf8)),
@@ -169,7 +199,12 @@ import Testing
@Test
func codegenCrossFileExtension() throws {
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
let classURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileExtensionClass.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: classURL, encoding: .utf8)),
@@ -186,7 +221,12 @@ import Testing
@Test
func codegenSkipsEmptySkeletons() throws {
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
let importedURL = Self.multifileInputsDirectory.appendingPathComponent("ImportedFunctions.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: importedURL, encoding: .utf8)),
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
index 64c9ae535..2f3f46fdb 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
@@ -49,7 +49,12 @@ import Testing
let name = url.deletingPathExtension().lastPathComponent
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
- let importSwift = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
+ let importSwift = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
importSwift.addSourceFile(sourceFile, inputFilePath: "\(name).swift")
let importResult = try importSwift.finalize()
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
@@ -69,7 +74,12 @@ import Testing
func snapshotExportWithGlobal(inputFile: String) throws {
let url = Self.inputsDirectory.appendingPathComponent(inputFile)
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
- let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: true)
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "TestModule",
+ exposeToGlobal: true,
+ externalModuleIndex: .empty
+ )
swiftAPI.addSourceFile(sourceFile, inputFilePath: inputFile)
let name = url.deletingPathExtension().lastPathComponent
let outputSkeleton = try swiftAPI.finalize()
@@ -86,13 +96,23 @@ import Testing
func snapshotMixedModuleExposure() throws {
let globalURL = Self.inputsDirectory.appendingPathComponent("MixedGlobal.swift")
let globalSourceFile = Parser.parse(source: try String(contentsOf: globalURL, encoding: .utf8))
- let globalAPI = SwiftToSkeleton(progress: .silent, moduleName: "GlobalModule", exposeToGlobal: true)
+ let globalAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "GlobalModule",
+ exposeToGlobal: true,
+ externalModuleIndex: .empty
+ )
globalAPI.addSourceFile(globalSourceFile, inputFilePath: "MixedGlobal.swift")
let globalSkeleton = try globalAPI.finalize()
let privateURL = Self.inputsDirectory.appendingPathComponent("MixedPrivate.swift")
let privateSourceFile = Parser.parse(source: try String(contentsOf: privateURL, encoding: .utf8))
- let privateAPI = SwiftToSkeleton(progress: .silent, moduleName: "PrivateModule", exposeToGlobal: false)
+ let privateAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "PrivateModule",
+ exposeToGlobal: false,
+ externalModuleIndex: .empty
+ )
privateAPI.addSourceFile(privateSourceFile, inputFilePath: "MixedPrivate.swift")
let privateSkeleton = try privateAPI.finalize()
@@ -114,6 +134,7 @@ import Testing
progress: .silent,
moduleName: "TestModule",
exposeToGlobal: false,
+ externalModuleIndex: .empty,
identityMode: nil // no config default
)
swiftAPI.addSourceFile(sourceFile, inputFilePath: "IdentityModeClass.swift")
@@ -140,6 +161,7 @@ import Testing
progress: .silent,
moduleName: "TestModule",
exposeToGlobal: false,
+ externalModuleIndex: .empty,
identityMode: "pointer" // config says pointer for all classes
)
swiftAPI.addSourceFile(sourceFile, inputFilePath: "IdentityModeClass.swift")
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift
new file mode 100644
index 000000000..4f12a88fa
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift
@@ -0,0 +1,491 @@
+import Foundation
+import SwiftParser
+import SwiftSyntax
+import Testing
+
+@testable import BridgeJSCore
+@testable import BridgeJSSkeleton
+
+@Suite struct CrossModuleResolutionTests {
+ @Test
+ func resolvesTopLevelExternalStruct() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: """
+ @JS public struct Vector3D {
+ public let x: Double
+ public let y: Double
+ public let z: Double
+ @JS public init(x: Double, y: Double, z: Double) {
+ self.x = x; self.y = y; self.z = z
+ }
+ }
+ """
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func currentVelocity() -> Vector3D {
+ Vector3D(x: 0, y: 0, z: 0)
+ }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ #expect(app.usedExternalModules == ["Core"])
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "currentVelocity" }))
+ #expect(function.returnType == .swiftStruct("Vector3D"))
+ }
+
+ @Test
+ func resolvesTopLevelExternalClass() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: """
+ @JS public class Emitter {
+ @JS public init() {}
+ @JS public func emit() -> String { "" }
+ }
+ """
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func makeEmitter() -> Emitter { Emitter() }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ #expect(app.usedExternalModules == ["Core"])
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "makeEmitter" }))
+ #expect(function.returnType == .swiftHeapObject("Emitter"))
+ }
+
+ @Test
+ func resolvesNestedExternalStruct() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: """
+ @JS public enum Geometry {
+ @JS public struct BoundingBox {
+ public let side: Double
+ @JS public init(side: Double) { self.side = side }
+ }
+ }
+ """
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func unitBox() -> Geometry.BoundingBox {
+ Geometry.BoundingBox(side: 1)
+ }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ #expect(app.usedExternalModules == ["Core"])
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "unitBox" }))
+ #expect(function.returnType == .swiftStruct("Geometry.BoundingBox"))
+ }
+
+ @Test
+ func resolvesExplicitModuleQualifier() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: """
+ @JS public struct Vector3D {
+ @JS public init() {}
+ }
+ """
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func fromCore() -> Core.Vector3D { Core.Vector3D() }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "fromCore" }))
+ #expect(function.returnType == .swiftStruct("Vector3D"))
+ }
+
+ @Test
+ func resolvesExternalTypeInArrayAndOptional() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: """
+ @JS public struct Point {
+ @JS public init() {}
+ }
+ """
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func scatter(points: [Point?]) -> Point? { nil }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "scatter" }))
+ #expect(function.returnType == .nullable(.swiftStruct("Point"), .null))
+ #expect(function.parameters.first?.type == .array(.nullable(.swiftStruct("Point"), .null)))
+ }
+
+ // MARK: - Diagnostics
+
+ @Test
+ func ambiguousExternalNameProducesDiagnostic() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let graphics = try buildDependencySkeleton(
+ moduleName: "Graphics",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+
+ do {
+ _ = try resolveApp(
+ source: """
+ import Core
+ import Graphics
+ @JS public func ambiguous() -> Vector3D { fatalError() }
+ """,
+ dependencies: [
+ (moduleName: "Core", skeleton: core),
+ (moduleName: "Graphics", skeleton: graphics),
+ ]
+ )
+ Issue.record("Expected ambiguity diagnostic, but resolution succeeded")
+ } catch let error as BridgeJSCoreDiagnosticError {
+ let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
+ #expect(combined.contains("ambiguous use of 'Vector3D'"))
+ let combinedHints = error.diagnostics.compactMap(\.diagnostic.hint).joined(separator: "\n")
+ #expect(combinedHints.contains("Core"))
+ #expect(combinedHints.contains("Graphics"))
+ }
+ }
+
+ @Test
+ func explicitQualifierResolvesAmbiguity() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let graphics = try buildDependencySkeleton(
+ moduleName: "Graphics",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ import Graphics
+ @JS public func fromCore() -> Core.Vector3D { Core.Vector3D() }
+ """,
+ dependencies: [
+ (moduleName: "Core", skeleton: core),
+ (moduleName: "Graphics", skeleton: graphics),
+ ]
+ )
+ #expect(app.usedExternalModules == ["Core"])
+ }
+
+ @Test
+ func localDeclarationShadowsExternalSameName() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public struct Vector3D {
+ public let id: Int
+ @JS public init(id: Int) { self.id = id }
+ }
+ @JS public func makeLocal() -> Vector3D { Vector3D(id: 42) }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ // Local declaration wins.
+ #expect(app.usedExternalModules == [])
+ let exported = try #require(app.exported)
+ #expect(exported.structs.contains(where: { $0.name == "Vector3D" }))
+ }
+
+ @Test
+ func unknownTypeEmitsHintMentioningDependencyTargets() throws {
+ do {
+ _ = try resolveApp(
+ source: """
+ @JS public func use() -> MissingType { fatalError() }
+ """,
+ dependencies: []
+ )
+ Issue.record("Expected an unsupported-type diagnostic")
+ } catch let error as BridgeJSCoreDiagnosticError {
+ let combinedHints = error.diagnostics.compactMap(\.diagnostic.hint).joined(separator: "\n")
+ #expect(combinedHints.contains("dependency targets"))
+ }
+ }
+
+ // MARK: - Coverage across type categories
+
+ @Test
+ func resolvesExternalSimpleEnum() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public enum Direction { case north, south }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func opposite(_ d: Direction) -> Direction { d }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "opposite" }))
+ #expect(function.returnType == .caseEnum("Direction"))
+ #expect(function.parameters.first?.type == .caseEnum("Direction"))
+ }
+
+ @Test
+ func resolvesExternalRawValueEnum() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public enum HTTPMethod: String { case get, post }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func describe(_ m: HTTPMethod) -> String { "" }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "describe" }))
+ #expect(function.parameters.first?.type == .rawValueEnum("HTTPMethod", .string))
+ }
+
+ @Test
+ func resolvesExternalAssociatedValueEnum() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public enum Shape { case point, circle(radius: Double) }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func area(_ s: Shape) -> Double { 0 }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "area" }))
+ #expect(function.parameters.first?.type == .associatedValueEnum("Shape"))
+ }
+
+ @Test
+ func resolvesExternalNamespaceEnum() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: """
+ @JS public enum Utils {
+ @JS public static func hello() -> String { "hi" }
+ }
+ """
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func dummy(_ u: Utils?) -> Utils? { u }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "dummy" }))
+ #expect(function.returnType == .nullable(.namespaceEnum("Utils"), .null))
+ }
+
+ // MARK: - Structural positions
+
+ @Test
+ func resolvesExternalInDictionaryValuePosition() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func names(_ map: [String: Vector3D]) -> [String] { [] }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "names" }))
+ #expect(function.parameters.first?.type == .dictionary(.swiftStruct("Vector3D")))
+ }
+
+ @Test
+ func resolvesExternalInStructPropertyType() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public struct Particle {
+ public let position: Vector3D
+ @JS public init(position: Vector3D) { self.position = position }
+ }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let particle = try #require(app.exported?.structs.first(where: { $0.name == "Particle" }))
+ let positionProperty = try #require(particle.properties.first(where: { $0.name == "position" }))
+ #expect(positionProperty.type == .swiftStruct("Vector3D"))
+ #expect(app.usedExternalModules == ["Core"])
+ }
+
+ // MARK: - Multi-module scenarios
+
+ @Test
+ func tracksMultipleExternalModulesInSortedOrder() throws {
+ let alpha = try buildDependencySkeleton(
+ moduleName: "Alpha",
+ source: "@JS public struct A { @JS public init() {} }"
+ )
+ let beta = try buildDependencySkeleton(
+ moduleName: "Beta",
+ source: "@JS public struct B { @JS public init() {} }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Alpha
+ import Beta
+ @JS public func both(_ a: A, _ b: B) -> String { "" }
+ """,
+ dependencies: [
+ (moduleName: "Beta", skeleton: beta),
+ (moduleName: "Alpha", skeleton: alpha),
+ ]
+ )
+ #expect(app.usedExternalModules == ["Alpha", "Beta"])
+ }
+
+ @Test
+ func transitiveDependencyTypesResolve() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let domain = try buildDependencySkeleton(
+ moduleName: "Domain",
+ source: """
+ @JS public struct Particle {
+ public let position: Vector3D
+ @JS public init(position: Vector3D) { self.position = position }
+ }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ #expect(domain.usedExternalModules == ["Core"])
+ // App can still reference Vector3D through Domain’s transitive dependency on Core.
+ let app = try resolveApp(
+ source: """
+ import Core
+ import Domain
+ @JS public func position(of p: Particle) -> Vector3D { p.position }
+ """,
+ dependencies: [
+ (moduleName: "Core", skeleton: core),
+ (moduleName: "Domain", skeleton: domain),
+ ]
+ )
+ let function = try #require(app.exported?.functions.first(where: { $0.name == "position" }))
+ #expect(function.returnType == .swiftStruct("Vector3D"))
+ #expect(function.parameters.first?.type == .swiftStruct("Particle"))
+ #expect(app.usedExternalModules == ["Core", "Domain"])
+ }
+
+ // MARK: - Skeleton serialisation round-trip
+
+ @Test
+ func skeletonRoundTripsUsedExternalModules() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ let app = try resolveApp(
+ source: """
+ import Core
+ @JS public func origin() -> Vector3D { Vector3D() }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.sortedKeys]
+ let data = try encoder.encode(app)
+ let decoded = try JSONDecoder().decode(BridgeJSSkeleton.self, from: data)
+ #expect(decoded.usedExternalModules == app.usedExternalModules)
+ #expect(decoded.moduleName == app.moduleName)
+ }
+
+ @Test
+ func externalModuleIndexSkipsDependenciesWithoutExportedTypes() throws {
+ let empty = BridgeJSSkeleton(moduleName: "Empty")
+ let index = ExternalModuleIndex(dependencies: [(moduleName: "Empty", skeleton: empty)])
+ #expect(index.isEmpty)
+ #expect(!index.isKnownModule("Empty"))
+ #expect(index.lookup(dotPath: "Whatever") == nil)
+ }
+
+ @Test
+ func moduleQualifierRejectsUnknownModule() throws {
+ let core = try buildDependencySkeleton(
+ moduleName: "Core",
+ source: "@JS public struct Vector3D { @JS public init() {} }"
+ )
+ do {
+ _ = try resolveApp(
+ source: """
+ import Core
+ @JS public func useFoundation() -> Foundation.URL { fatalError() }
+ """,
+ dependencies: [(moduleName: "Core", skeleton: core)]
+ )
+ Issue.record("Expected unsupported-type diagnostic for Foundation.URL")
+ } catch let error as BridgeJSCoreDiagnosticError {
+ let combinedMessages = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
+ #expect(combinedMessages.contains("Foundation.URL"))
+ }
+ }
+
+ // MARK: - Utillites
+
+ private func resolveApp(
+ source appSource: String,
+ dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)]
+ ) throws -> BridgeJSSkeleton {
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: "App",
+ exposeToGlobal: false,
+ externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
+ )
+ let sourceFile = Parser.parse(source: appSource)
+ swiftAPI.addSourceFile(sourceFile, inputFilePath: "App.swift")
+ return try swiftAPI.finalize()
+ }
+
+ private func buildDependencySkeleton(
+ moduleName: String,
+ source: String,
+ dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)] = []
+ ) throws -> BridgeJSSkeleton {
+ let swiftAPI = SwiftToSkeleton(
+ progress: .silent,
+ moduleName: moduleName,
+ exposeToGlobal: false,
+ externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
+ )
+ swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
+ return try swiftAPI.finalize()
+ }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ArrayTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ArrayTypes.json
index d071d8c52..89b64c157 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ArrayTypes.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ArrayTypes.json
@@ -1531,5 +1531,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json
index a2b95cc24..27ba89aca 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json
@@ -189,5 +189,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncImport.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncImport.json
index 263578d20..616a0bdbd 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncImport.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncImport.json
@@ -147,5 +147,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncStaticImport.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncStaticImport.json
index 972a532c6..e6f4c2395 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncStaticImport.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AsyncStaticImport.json
@@ -63,5 +63,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json
index f77d39ad9..4c8d575b0 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json
@@ -78,5 +78,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.ReverseOrder.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.ReverseOrder.json
index 9b056b650..6c589de87 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.ReverseOrder.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.ReverseOrder.json
@@ -148,5 +148,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.json
index d76a1622d..9bec040f1 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileFunctionTypes.json
@@ -148,5 +148,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileSkipsEmptySkeletons.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileSkipsEmptySkeletons.json
index a0c2c80c6..3bb67f13c 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileSkipsEmptySkeletons.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileSkipsEmptySkeletons.json
@@ -29,5 +29,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.ReverseOrder.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.ReverseOrder.json
index 59fb8484a..edf8177c1 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.ReverseOrder.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.ReverseOrder.json
@@ -61,5 +61,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.json
index cc10331a4..58bfadab7 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileTypeResolution.json
@@ -61,5 +61,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DefaultParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DefaultParameters.json
index f8a23c33d..e7874c072 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DefaultParameters.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DefaultParameters.json
@@ -1312,5 +1312,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json
index e18586e1c..c52f3e82f 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json
@@ -337,5 +337,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json
index b92cee954..873c5c49f 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json
@@ -1455,5 +1455,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumCase.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumCase.json
index ea32ad739..c4095b502 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumCase.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumCase.json
@@ -323,5 +323,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.Global.json
index 46dbe8917..103a67999 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.Global.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.Global.json
@@ -639,5 +639,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.json
index a57703b09..f9890d36b 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumNamespace.json
@@ -639,5 +639,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumRawType.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumRawType.json
index fc4a7ae52..b51940be9 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumRawType.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumRawType.json
@@ -1573,5 +1573,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/FixedWidthIntegers.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/FixedWidthIntegers.json
index 15a20f72e..4b7e5ceb2 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/FixedWidthIntegers.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/FixedWidthIntegers.json
@@ -507,5 +507,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalGetter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalGetter.json
index f750fc6a5..bafea5d81 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalGetter.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalGetter.json
@@ -57,5 +57,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalThisImports.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalThisImports.json
index 809a9ad99..5acb585fe 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalThisImports.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalThisImports.json
@@ -125,5 +125,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json
index d7a9064dc..f4a4440c6 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json
@@ -144,5 +144,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportArray.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportArray.json
index 3f9cb8e32..7ae3363db 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportArray.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportArray.json
@@ -74,5 +74,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportedTypeInExportedInterface.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportedTypeInExportedInterface.json
index f57e77d21..aa71b40b9 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportedTypeInExportedInterface.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportedTypeInExportedInterface.json
@@ -198,5 +198,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/InvalidPropertyNames.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/InvalidPropertyNames.json
index 1ad99f397..6edbc671c 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/InvalidPropertyNames.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/InvalidPropertyNames.json
@@ -267,5 +267,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClass.json
index ef8eba9ba..b12f099b6 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClass.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClass.json
@@ -181,5 +181,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClassStaticFunctions.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClassStaticFunctions.json
index 18f7cfaac..c0d885b89 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClassStaticFunctions.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSClassStaticFunctions.json
@@ -160,5 +160,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSValue.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSValue.json
index fb8601ae7..05e5b9d3b 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSValue.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSValue.json
@@ -381,5 +381,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedGlobal.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedGlobal.json
index f140fd007..0d30063ee 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedGlobal.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedGlobal.json
@@ -75,5 +75,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedPrivate.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedPrivate.json
index 9e9dc445e..e6bcf2e5c 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedPrivate.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/MixedPrivate.json
@@ -75,5 +75,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json
index 080f9f959..4b6b720f1 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.Global.json
@@ -289,5 +289,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json
index d471eeaca..3c07b7dcf 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Namespaces.json
@@ -289,5 +289,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Optionals.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Optionals.json
index 3e6d6c60c..26479bf1d 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Optionals.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Optionals.json
@@ -1557,5 +1557,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveParameters.json
index cf76f3878..539f8132a 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveParameters.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveParameters.json
@@ -125,5 +125,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveReturn.json
index b0398c161..1cdce90a6 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveReturn.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PrimitiveReturn.json
@@ -150,5 +150,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PropertyTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PropertyTypes.json
index 24e3f44cd..281538dd6 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PropertyTypes.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/PropertyTypes.json
@@ -377,5 +377,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.json
index b46d1125e..feca4615b 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.json
@@ -970,5 +970,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.json
index 36d6941d3..70273f8b3 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.json
@@ -282,5 +282,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json
index e20af8a3b..800018440 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json
@@ -483,5 +483,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json
index ded6f7602..36110488c 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json
@@ -483,5 +483,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.Global.json
index d14f9b0a3..1cbe44619 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.Global.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.Global.json
@@ -351,5 +351,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.json
index 35d740dff..8fc0667b0 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticProperties.json
@@ -351,5 +351,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringParameter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringParameter.json
index 75462af81..3ffadf65d 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringParameter.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringParameter.json
@@ -131,5 +131,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringReturn.json
index 1088a5cab..63ee8e54b 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringReturn.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StringReturn.json
@@ -60,5 +60,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json
index ceda64904..f25e1bda7 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json
@@ -275,5 +275,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json
index 41662e48b..abca80150 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json
@@ -1875,5 +1875,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosureImports.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosureImports.json
index a78b1bf5d..9187f5574 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosureImports.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosureImports.json
@@ -123,5 +123,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json
index 4c1ef582b..bfde01318 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json
@@ -712,5 +712,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.json
index ccd3043ac..6c8f9a33f 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.json
@@ -107,5 +107,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Throws.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Throws.json
index 02796479f..942e5fb45 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Throws.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Throws.json
@@ -33,5 +33,8 @@
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/UnsafePointer.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/UnsafePointer.json
index 1eb9e47ec..a382778e9 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/UnsafePointer.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/UnsafePointer.json
@@ -412,5 +412,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/VoidParameterVoidReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/VoidParameterVoidReturn.json
index 7f19c18bf..fd2e4c565 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/VoidParameterVoidReturn.json
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/VoidParameterVoidReturn.json
@@ -60,5 +60,8 @@
}
]
},
- "moduleName" : "TestModule"
+ "moduleName" : "TestModule",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Plugins/PackageToJS/Sources/BridgeJSPluginUtilities b/Plugins/PackageToJS/Sources/BridgeJSPluginUtilities
new file mode 120000
index 000000000..97aa4a1cc
--- /dev/null
+++ b/Plugins/PackageToJS/Sources/BridgeJSPluginUtilities
@@ -0,0 +1 @@
+../../BridgeJS/Sources/BridgeJSPluginUtilities
\ No newline at end of file
diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
index 7e335c68e..7686372f9 100644
--- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
+++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
@@ -748,15 +748,15 @@ class SkeletonCollector {
}
}
if let target = target as? SwiftSourceModuleTarget {
- let directories = [
- target.directoryURL.appending(path: "Generated/JavaScript"),
- // context.pluginWorkDirectoryURL: ".build/plugins/PackageToJS/outputs/"
- // .build/plugins/outputs/[package]/[target]/destination/BridgeJS/JavaScript/BridgeJS.json
- context.pluginWorkDirectoryURL.deletingLastPathComponent().deletingLastPathComponent()
- .appending(path: "outputs/\(package.id)/\(target.name)/destination/BridgeJS/JavaScript"),
+ let candidates = [
+ target.directoryURL.appending(path: "Generated/JavaScript").appending(path: skeletonFile),
+ BridgeJSPluginPaths.skeletonURL(
+ targetName: target.name,
+ packageID: package.id,
+ commandPluginWorkDirectoryURL: context.pluginWorkDirectoryURL
+ ),
]
- for directory in directories {
- let skeletonURL = directory.appending(path: skeletonFile)
+ for skeletonURL in candidates {
if FileManager.default.fileExists(atPath: skeletonURL.path) {
skeletons.append(skeletonURL)
}
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md
index 0bd69aa5b..b8856ac30 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md
@@ -12,6 +12,8 @@ The configuration system supports two complementary files:
- `bridge-js.config.json` - Base configuration (checked into version control)
- `bridge-js.config.local.json` - Local overrides (intended to be ignored by git, for developer-specific settings)
+> Note: The presence of a configuration file, even if empty, is required to expose `@JS` types to other modules in the package. See `Examples/MultiModule/` for an example.
+
## Configuration Loading
### File Locations
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md
index 99a3d5b1c..5b9616105 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md
@@ -58,3 +58,7 @@ For package layout and how to consume the output from JavaScript, see .
+
+## Multiple targets in one package
+
+A single package can have multiple targets that use `@JS`. Apply the BridgeJS plugin to every target that contains `@JS` declarations. To make a target's `@JS` types visible to other targets in the same package, also add a `bridge-js.config.json` file (`{}` is enough) to that target’s source directory. See `Examples/MultiModule/` for an example.
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Unsupported-Features.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Unsupported-Features.md
index 7ba25f7ae..83213aca1 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Unsupported-Features.md
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Unsupported-Features.md
@@ -8,13 +8,11 @@ BridgeJS generates glue code per Swift target (module). Some patterns that are v
## Type usage crossing module boundary
-BridgeJS does **not** support using a type across module boundaries in the following situations.
+### Exporting Swift: extending types from another Swift module
-### Exporting Swift: types from another Swift module
+If you have multiple Swift targets (e.g. a library and an app), you **cannot** extend a type defined in one target with a `@JS` exported API in another target.
-If you have multiple Swift targets (e.g. a library and an app), you **cannot** use a type defined in one target in an exported API of another target.
-
-**Unsupported example:** Module `App` exports a function that takes or returns a type defined in module `Lib`:
+**Unsupported example:** Module `App` extends a type defined in module `Lib`:
```swift
// In module Lib
@@ -24,5 +22,15 @@ If you have multiple Swift targets (e.g. a library and an app), you **cannot** u
}
// In module App (depends on Lib) - unsupported
-@JS public func transform(_ p: LibPoint) -> LibPoint { ... }
+extension LibPoint {
+ @JS public func transformed() -> LibPoint { ... }
+}
```
+
+### Exporting Swift: non-`@JS` types from another Swift module
+
+While using `@JS` types from another Swift module is supported, it is not possible to use non-`@JS` types defined in other modules: this will fail at type lookup.
+
+### Exporting Swift: types from another Swift package
+
+Types defined in a separate Swift package cannot yet be referenced from `@JS` declarations in your package.
diff --git a/Tests/BridgeJSGlobalTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSGlobalTests/Generated/JavaScript/BridgeJS.json
index f57f91936..5e9626840 100644
--- a/Tests/BridgeJSGlobalTests/Generated/JavaScript/BridgeJS.json
+++ b/Tests/BridgeJSGlobalTests/Generated/JavaScript/BridgeJS.json
@@ -559,5 +559,8 @@
]
},
- "moduleName" : "BridgeJSGlobalTests"
+ "moduleName" : "BridgeJSGlobalTests",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json
index d30ca00f8..93e2401e5 100644
--- a/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json
+++ b/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json
@@ -443,5 +443,8 @@
}
]
},
- "moduleName" : "BridgeJSIdentityTests"
+ "moduleName" : "BridgeJSIdentityTests",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file
diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json
index dd4362fc1..e396662b9 100644
--- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json
+++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json
@@ -20481,5 +20481,8 @@
}
]
},
- "moduleName" : "BridgeJSRuntimeTests"
+ "moduleName" : "BridgeJSRuntimeTests",
+ "usedExternalModules" : [
+
+ ]
}
\ No newline at end of file