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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Benchmarks/Sources/Generated/JavaScript/BridgeJS.json
Original file line number Diff line number Diff line change
Expand Up @@ -3415,5 +3415,8 @@
}
]
},
"moduleName" : "Benchmarks"
"moduleName" : "Benchmarks",
"usedExternalModules" : [

]
}
38 changes: 38 additions & 0 deletions Examples/MultiModule/Package.swift
Original file line number Diff line number Diff line change
@@ -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")
]
),
]
)
17 changes: 17 additions & 0 deletions Examples/MultiModule/README.md
Original file line number Diff line number Diff line change
@@ -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`.
17 changes: 17 additions & 0 deletions Examples/MultiModule/Sources/Core/Vector3D.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions Examples/MultiModule/Sources/Core/bridge-js.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
6 changes: 6 additions & 0 deletions Examples/MultiModule/Sources/MultiModule/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Core
import JavaScriptKit

@JS public func currentVelocity() -> Vector3D {
Vector3D(x: 0.1, y: 0.2, z: 0.3)
}
12 changes: 12 additions & 0 deletions Examples/MultiModule/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>

<head>
<title>MultiModule Example</title>
</head>

<body>
<script type="module" src="index.js"></script>
</body>

</html>
10 changes: 10 additions & 0 deletions Examples/MultiModule/index.js
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -300,5 +300,8 @@
}
]
},
"moduleName" : "PlayBridgeJS"
"moduleName" : "PlayBridgeJS",
"usedExternalModules" : [

]
}
7 changes: 6 additions & 1 deletion Examples/PlayBridgeJS/Sources/PlayBridgeJS/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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<String>()
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()
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)...")
Expand Down Expand Up @@ -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 + "/")
Expand Down Expand Up @@ -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: {
Expand Down
Loading
Loading