Skip to content

Add JavaScriptFoundationCompat module to provide utilities to interact Foundation types #378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2025
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/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ let package = Package(
targets: [
.executableTarget(
name: "Benchmarks",
dependencies: ["JavaScriptKit"],
dependencies: [
"JavaScriptKit",
.product(name: "JavaScriptFoundationCompat", package: "JavaScriptKit"),
],
exclude: ["Generated/JavaScript", "bridge-js.d.ts"],
swiftSettings: [
.enableExperimentalFeature("Extern")
Expand Down
20 changes: 20 additions & 0 deletions Benchmarks/Sources/Benchmarks.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import JavaScriptKit
import JavaScriptFoundationCompat
import Foundation

class Benchmark {
init(_ title: String) {
Expand Down Expand Up @@ -75,4 +77,22 @@ class Benchmark {
}
}
}

do {
let conversion = Benchmark("Conversion")
let data = Data(repeating: 0, count: 10_000)
conversion.testSuite("Data to JSTypedArray") {
for _ in 0..<1_000_000 {
_ = data.jsTypedArray
}
}

let uint8Array = data.jsTypedArray

conversion.testSuite("JSTypedArray to Data") {
for _ in 0..<1_000_000 {
_ = Data.construct(from: uint8Array)
}
}
}
}
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let package = Package(
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
.library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]),
.library(name: "JavaScriptFoundationCompat", targets: ["JavaScriptFoundationCompat"]),
.library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]),
.plugin(name: "PackageToJS", targets: ["PackageToJS"]),
.plugin(name: "BridgeJS", targets: ["BridgeJS"]),
Expand Down Expand Up @@ -106,6 +107,18 @@ let package = Package(
"JavaScriptEventLoopTestSupport",
]
),
.target(
name: "JavaScriptFoundationCompat",
dependencies: [
"JavaScriptKit"
]
),
.testTarget(
name: "JavaScriptFoundationCompatTests",
dependencies: [
"JavaScriptFoundationCompat"
]
),
.plugin(
name: "PackageToJS",
capability: .command(
Expand Down
42 changes: 42 additions & 0 deletions Sources/JavaScriptFoundationCompat/Data+JSValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
import JavaScriptKit

/// Data <-> Uint8Array conversion. The conversion is lossless and copies the bytes at most once per conversion
extension Data: ConvertibleToJSValue, ConstructibleFromJSValue {
/// Convert a Data to a JSTypedArray<UInt8>.
///
/// - Returns: A Uint8Array that contains the bytes of the Data.
public var jsTypedArray: JSTypedArray<UInt8> {
self.withUnsafeBytes { buffer in
return JSTypedArray<UInt8>(buffer: buffer.bindMemory(to: UInt8.self))
}
}

/// Convert a Data to a JSValue.
///
/// - Returns: A JSValue that contains the bytes of the Data as a Uint8Array.
public var jsValue: JSValue { jsTypedArray.jsValue }

/// Construct a Data from a JSTypedArray<UInt8>.
public static func construct(from uint8Array: JSTypedArray<UInt8>) -> Data? {
// First, allocate the data storage
var data = Data(count: uint8Array.lengthInBytes)
// Then, copy the byte contents into the Data buffer
data.withUnsafeMutableBytes { destinationBuffer in
uint8Array.copyMemory(to: destinationBuffer.bindMemory(to: UInt8.self))
}
return data
}

/// Construct a Data from a JSValue.
///
/// - Parameter jsValue: The JSValue to construct a Data from.
/// - Returns: A Data, if the JSValue is a Uint8Array.
public static func construct(from jsValue: JSValue) -> Data? {
guard let uint8Array = JSTypedArray<UInt8>(from: jsValue) else {
// If the JSValue is not a Uint8Array, fail.
return nil
}
return construct(from: uint8Array)
}
}
55 changes: 55 additions & 0 deletions Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import XCTest
import Foundation
import JavaScriptFoundationCompat
import JavaScriptKit

final class DataJSValueTests: XCTestCase {
func testDataToJSValue() {
let data = Data([0x00, 0x01, 0x02, 0x03])
let jsValue = data.jsValue

let uint8Array = JSTypedArray<UInt8>(from: jsValue)
XCTAssertEqual(uint8Array?.lengthInBytes, 4)
XCTAssertEqual(uint8Array?[0], 0x00)
XCTAssertEqual(uint8Array?[1], 0x01)
XCTAssertEqual(uint8Array?[2], 0x02)
XCTAssertEqual(uint8Array?[3], 0x03)
}

func testJSValueToData() {
let jsValue = JSTypedArray<UInt8>([0x00, 0x01, 0x02, 0x03]).jsValue
let data = Data.construct(from: jsValue)
XCTAssertEqual(data, Data([0x00, 0x01, 0x02, 0x03]))
}

func testDataToJSValue_withLargeData() {
let data = Data(repeating: 0x00, count: 1024 * 1024)
let jsValue = data.jsValue
let uint8Array = JSTypedArray<UInt8>(from: jsValue)
XCTAssertEqual(uint8Array?.lengthInBytes, 1024 * 1024)
}

func testJSValueToData_withLargeData() {
let jsValue = JSTypedArray<UInt8>(Array(repeating: 0x00, count: 1024 * 1024)).jsValue
let data = Data.construct(from: jsValue)
XCTAssertEqual(data?.count, 1024 * 1024)
}

func testDataToJSValue_withEmptyData() {
let data = Data()
let jsValue = data.jsValue
let uint8Array = JSTypedArray<UInt8>(from: jsValue)
XCTAssertEqual(uint8Array?.lengthInBytes, 0)
}

func testJSValueToData_withEmptyData() {
let jsValue = JSTypedArray<UInt8>([]).jsValue
let data = Data.construct(from: jsValue)
XCTAssertEqual(data, Data())
}

func testJSValueToData_withInvalidJSValue() {
let data = Data.construct(from: JSObject().jsValue)
XCTAssertNil(data)
}
}