Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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