Skip to content

Commit c672d76

Browse files
Introduce BridgeJS, a declarative JS interop system
1 parent f4d5219 commit c672d76

File tree

111 files changed

+8387
-102
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+8387
-102
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ xcuserdata/
1010
Examples/*/Bundle
1111
Examples/*/package-lock.json
1212
Package.resolved
13+
Plugins/BridgeJS/Sources/JavaScript/package-lock.json

Examples/ExportSwift/Package.swift

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version:6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "MyApp",
7+
platforms: [
8+
.macOS(.v14)
9+
],
10+
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
11+
targets: [
12+
.executableTarget(
13+
name: "MyApp",
14+
dependencies: [
15+
"JavaScriptKit"
16+
],
17+
swiftSettings: [
18+
.enableExperimentalFeature("Extern")
19+
],
20+
plugins: [
21+
.plugin(name: "BridgeJS", package: "JavaScriptKit")
22+
]
23+
)
24+
]
25+
)
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import JavaScriptKit
2+
3+
// Mark functions you want to export to JavaScript with the @JS attribute
4+
// This function will be available as `renderCircleSVG(size)` in JavaScript
5+
@JS public func renderCircleSVG(size: Int) -> String {
6+
let strokeWidth = 3
7+
let strokeColor = "black"
8+
let fillColor = "red"
9+
let cx = size / 2
10+
let cy = size / 2
11+
let r = (size / 2) - strokeWidth
12+
var svg = "<svg width=\"\(size)px\" height=\"\(size)px\">"
13+
svg +=
14+
"<circle cx=\"\(cx)\" cy=\"\(cy)\" r=\"\(r)\" stroke=\"\(strokeColor)\" stroke-width=\"\(strokeWidth)\" fill=\"\(fillColor)\" />"
15+
svg += "</svg>"
16+
return svg
17+
}
18+
19+
// Classes can also be exported using the @JS attribute
20+
// This class will be available as a constructor in JavaScript: new Greeter("name")
21+
@JS class Greeter {
22+
var name: String
23+
24+
// Use @JS for initializers you want to expose
25+
@JS init(name: String) {
26+
self.name = name
27+
}
28+
29+
// Methods need the @JS attribute to be accessible from JavaScript
30+
// This method will be available as greeter.greet() in JavaScript
31+
@JS public func greet() -> String {
32+
"Hello, \(name)!"
33+
}
34+
}

Examples/ExportSwift/index.html

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>Getting Started</title>
6+
</head>
7+
8+
<body>
9+
<script type="module" src="index.js"></script>
10+
</body>
11+
12+
</html>

Examples/ExportSwift/index.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
2+
const { exports } = await init({});
3+
4+
const Greeter = exports.Greeter;
5+
const greeter = new Greeter("World");
6+
const circle = exports.renderCircleSVG(100);
7+
8+
// Display the results
9+
const textOutput = document.createElement("div");
10+
textOutput.innerText = greeter.greet()
11+
document.body.appendChild(textOutput);
12+
const circleOutput = document.createElement("div");
13+
circleOutput.innerHTML = circle;
14+
document.body.appendChild(circleOutput);

Examples/ImportTS/Package.swift

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// swift-tools-version:6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "MyApp",
7+
platforms: [
8+
.macOS(.v10_15),
9+
.iOS(.v13),
10+
.tvOS(.v13),
11+
.watchOS(.v6),
12+
.macCatalyst(.v13),
13+
],
14+
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
15+
targets: [
16+
.executableTarget(
17+
name: "MyApp",
18+
dependencies: [
19+
"JavaScriptKit"
20+
],
21+
swiftSettings: [
22+
.enableExperimentalFeature("Extern")
23+
],
24+
plugins: [
25+
.plugin(name: "BridgeJS", package: "JavaScriptKit")
26+
]
27+
)
28+
]
29+
)

Examples/ImportTS/Sources/bridge.d.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Function definition to expose console.log to Swift
2+
// Will be imported as a Swift function: consoleLog(message: String)
3+
export function consoleLog(message: string): void
4+
5+
// TypeScript interface types are converted to Swift structs
6+
// This defines a subset of the browser's HTMLElement interface
7+
type HTMLElement = Pick<globalThis.HTMLElement, "innerText"> & {
8+
// Methods with object parameters are properly handled
9+
appendChild(child: HTMLElement): void
10+
}
11+
12+
// TypeScript object type with read-only properties
13+
// Properties will become Swift properties with appropriate access level
14+
type Document = {
15+
// Regular property - will be read/write in Swift
16+
title: string
17+
// Read-only property - will be read-only in Swift
18+
readonly body: HTMLElement
19+
// Method returning an object - will become a Swift method returning an HTMLElement
20+
createElement(tagName: string): HTMLElement
21+
}
22+
// Function returning a complex object
23+
// Will be imported as a Swift function: getDocument() -> Document
24+
export function getDocument(): Document

Examples/ImportTS/Sources/main.swift

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import JavaScriptKit
2+
3+
// This function is automatically generated by the @JS plugin
4+
// It demonstrates how to use TypeScript functions and types imported from bridge.d.ts
5+
@JS public func run() {
6+
// Call the imported consoleLog function defined in bridge.d.ts
7+
consoleLog("Hello, World!")
8+
9+
// Get the document object - this comes from the imported getDocument() function
10+
let document = getDocument()
11+
12+
// Access and modify properties - the title property is read/write
13+
document.title = "Hello, World!"
14+
15+
// Access read-only properties - body is defined as readonly in TypeScript
16+
let body = document.body
17+
18+
// Create a new element using the document.createElement method
19+
let h1 = document.createElement("h1")
20+
21+
// Set properties on the created element
22+
h1.innerText = "Hello, World!"
23+
24+
// Call methods on objects - appendChild is defined in the HTMLElement interface
25+
body.appendChild(h1)
26+
}

Examples/ImportTS/index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>Getting Started</title>
6+
</head>
7+
8+
<body>
9+
<script type="module" src="index.js"></script>
10+
11+
<div id="exports-result"></div>
12+
<div id="imports-result"></div>
13+
<pre id="code"></pre>
14+
</body>
15+
16+
</html>

Examples/ImportTS/index.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
2+
const { exports } = await init({
3+
imports: {
4+
consoleLog: (message) => {
5+
console.log(message);
6+
},
7+
getDocument: () => {
8+
return document;
9+
},
10+
}
11+
});
12+
13+
exports.run()

Examples/Multithreading/Package.resolved

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"originHash" : "e66f4c272838a860049b7e3528f1db03ee6ae99c2b21c3b6ea58a293be4db41b",
2+
"originHash" : "072d03a6e24e01bd372682a6090adb80cf29dea39421e065de6ff8853de704c9",
33
"pins" : [
44
{
55
"identity" : "chibi-ray",
@@ -8,6 +8,15 @@
88
"state" : {
99
"revision" : "c8cab621a3338dd2f8e817d3785362409d3b8cf1"
1010
}
11+
},
12+
{
13+
"identity" : "swift-syntax",
14+
"kind" : "remoteSourceControl",
15+
"location" : "https://github.com/swiftlang/swift-syntax",
16+
"state" : {
17+
"revision" : "0687f71944021d616d34d922343dcef086855920",
18+
"version" : "600.0.1"
19+
}
1120
}
1221
],
1322
"version" : 3

Package.swift

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// swift-tools-version:6.0
22

3+
import CompilerPluginSupport
34
import PackageDescription
45

56
// NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits
@@ -9,12 +10,24 @@ let useLegacyResourceBundling =
910

1011
let package = Package(
1112
name: "JavaScriptKit",
13+
platforms: [
14+
.macOS(.v10_15),
15+
.iOS(.v13),
16+
.tvOS(.v13),
17+
.watchOS(.v6),
18+
.macCatalyst(.v13),
19+
],
1220
products: [
1321
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
1422
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
1523
.library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]),
1624
.library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]),
1725
.plugin(name: "PackageToJS", targets: ["PackageToJS"]),
26+
.plugin(name: "BridgeJS", targets: ["BridgeJS"]),
27+
.plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]),
28+
],
29+
dependencies: [
30+
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"601.0.0")
1831
],
1932
targets: [
2033
.target(
@@ -98,7 +111,40 @@ let package = Package(
98111
capability: .command(
99112
intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package")
100113
),
101-
sources: ["Sources"]
114+
path: "Plugins/PackageToJS/Sources"
115+
),
116+
.plugin(
117+
name: "BridgeJS",
118+
capability: .buildTool(),
119+
dependencies: ["BridgeJSTool"],
120+
path: "Plugins/BridgeJS/Sources/BridgeJSBuildPlugin"
121+
),
122+
.plugin(
123+
name: "BridgeJSCommandPlugin",
124+
capability: .command(
125+
intent: .custom(verb: "bridge-js", description: "Generate bridging code"),
126+
permissions: [.writeToPackageDirectory(reason: "Generate bridging code")]
127+
),
128+
dependencies: ["BridgeJSTool"],
129+
path: "Plugins/BridgeJS/Sources/BridgeJSCommandPlugin"
130+
),
131+
.executableTarget(
132+
name: "BridgeJSTool",
133+
dependencies: [
134+
.product(name: "SwiftParser", package: "swift-syntax"),
135+
.product(name: "SwiftSyntax", package: "swift-syntax"),
136+
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
137+
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
138+
],
139+
path: "Plugins/BridgeJS/Sources/BridgeJSTool"
140+
),
141+
.testTarget(
142+
name: "BridgeJSRuntimeTests",
143+
dependencies: ["JavaScriptKit"],
144+
exclude: ["Generated/JavaScript"],
145+
swiftSettings: [
146+
.enableExperimentalFeature("Extern")
147+
]
102148
),
103149
]
104150
)

Plugins/BridgeJS/Package.swift

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "BridgeJS",
7+
platforms: [.macOS(.v13)],
8+
dependencies: [
9+
.package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1")
10+
],
11+
targets: [
12+
.target(name: "BridgeJSBuildPlugin"),
13+
.target(name: "BridgeJSLink"),
14+
.executableTarget(
15+
name: "BridgeJSTool",
16+
dependencies: [
17+
.product(name: "SwiftParser", package: "swift-syntax"),
18+
.product(name: "SwiftSyntax", package: "swift-syntax"),
19+
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
20+
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
21+
]
22+
),
23+
.testTarget(
24+
name: "BridgeJSToolTests",
25+
dependencies: ["BridgeJSTool", "BridgeJSLink"],
26+
exclude: ["__Snapshots__", "Inputs"]
27+
),
28+
]
29+
)

0 commit comments

Comments
 (0)