diff --git a/Package.swift b/Package.swift index ac0f503..c5770a2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,49 +1,46 @@ -// swift-tools-version:6.4 +// swift-tools-version:5.5 import PackageDescription let package = Package( name: "JavaScriptCore", - platforms: [ - .macOS(.v26), - ], products: [ .library( name: "SJavaScriptCore", - targets: ["SJavaScriptCore"] - ), + targets: ["SJavaScriptCore"]), ], dependencies: [ .package(name: "JavaScript"), + .package(name: "Test"), ], targets: [ .target( - name: "SJavaScriptCore", - dependencies: [ - .target( - name: "CJavaScriptCore", - condition: .when(platforms: [.linux]) - ), - .product( - name: "JavaScript", - package: "javascript" - ), - ], - path: "./Sources/JavaScriptCore" - ), - .systemLibrary( name: "CJavaScriptCore", - pkgConfig: "javascriptcoregtk-4.1", - providers: [.aptItem(["libjavascriptcoregtk-4.1-dev"])] - ), - .testTarget( - name: "Tests", - dependencies: [ - "SJavaScriptCore" - ] - ) + dependencies: []), + .target( + name: "SJavaScriptCore", + dependencies: ["CJavaScriptCore", "JavaScript"]) ] ) +// MARK: - tests + +testTarget("SJavaScriptCore") { test in + test("JavaScript") + test("JSValue") +} + +func testTarget(_ target: String, task: ((String) -> Void) -> Void) { + task { test in addTest(target: target, name: test) } +} + +func addTest(target: String, name: String) { + package.targets.append( + .executableTarget( + name: "Tests/\(target)/\(name)", + dependencies: ["Test", "SJavaScriptCore"], + path: "Tests/\(target)/\(name)")) +} + // MARK: - custom package source #if canImport(ObjectiveC) @@ -60,7 +57,7 @@ extension Package.Dependency { var baseUrl: String { switch self { - case .local: return "../../swiftstack/" + case .local: return "../" case .remote: return "https://swiftstack.io/" case .github: return "https://github.com/swiftstack/" } @@ -86,6 +83,6 @@ extension Package.Dependency { static func package(name: String, source: Source) -> Package.Dependency { return source == .local ? .package(name: name, path: source.url(for: name)) - : .package(url: source.url(for: name), branch: "dev") + : .package(name: name, url: source.url(for: name), .branch("dev")) } } diff --git a/README.md b/README.md index 95fa1c9..5179f60 100644 --- a/README.md +++ b/README.md @@ -30,5 +30,7 @@ Just works ### Linux ```bash -apt install -y libjavascriptcoregtk-4.1-dev +apt install -y libjavascriptcoregtk-4.0-dev +swift build -Xcc -I/usr/include/webkitgtk-4.0 +swift test -Xcc -I/usr/include/webkitgtk-4.0 ``` diff --git a/Sources/CJavaScriptCore/blank.c b/Sources/CJavaScriptCore/blank.c new file mode 100644 index 0000000..e69de29 diff --git a/Sources/CJavaScriptCore/include/include.h b/Sources/CJavaScriptCore/include/include.h new file mode 100644 index 0000000..2f77237 --- /dev/null +++ b/Sources/CJavaScriptCore/include/include.h @@ -0,0 +1,7 @@ + +#ifndef __JAVASCRIPTCORE_H__ +#define __JAVASCRIPTCORE_H__ + +#include + +#endif diff --git a/Sources/CJavaScriptCore/include/module.modulemap b/Sources/CJavaScriptCore/include/module.modulemap new file mode 100644 index 0000000..88dda56 --- /dev/null +++ b/Sources/CJavaScriptCore/include/module.modulemap @@ -0,0 +1,6 @@ + +module CJavaScriptCore [system] { + header "include.h" + link "javascriptcoregtk-4.0" + export * +} diff --git a/Sources/CJavaScriptCore/module.modulemap b/Sources/CJavaScriptCore/module.modulemap deleted file mode 100644 index 654928b..0000000 --- a/Sources/CJavaScriptCore/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -module CJavaScriptCore [system] { - header "/usr/include/webkitgtk-4.1/JavaScriptCore/JavaScript.h" - link "javascriptcoregtk-4.1" - export * -} diff --git a/Sources/JavaScriptCore/JSContext+closure.swift b/Sources/SJavaScriptCore/JSContext+closure.swift similarity index 68% rename from Sources/JavaScriptCore/JSContext+closure.swift rename to Sources/SJavaScriptCore/JSContext+closure.swift index 2957649..44bd883 100644 --- a/Sources/JavaScriptCore/JSContext+closure.swift +++ b/Sources/SJavaScriptCore/JSContext+closure.swift @@ -1,37 +1,31 @@ + #if os(Linux) import CJavaScriptCore #else import JavaScriptCore #endif -import Synchronization - @_exported import JavaScript -private -let functions: Mutex<[OpaquePointer: ([JSValue]) throws -> Value]> = .init([:]) - -extension JSObjectRef: @unchecked @retroactive Sendable {} +private var functions: [OpaquePointer: ([JSValue]) throws -> Value] = [:] extension JSContext { public func createFunction( name: String, - _ body: @escaping @Sendable ([JSValue]) throws -> Value - ) throws { + _ body: @escaping ([JSValue]) throws -> Value) throws + { let function = try createFunction(name: name, callback: wrapper) - functions.withLock { $0[function] = body } + functions[function] = body } public func createFunction( name: String, - _ body: @escaping @Sendable ([JSValue]) throws -> Void - ) throws { + _ body: @escaping ([JSValue]) throws -> Void) throws + { let function = try createFunction(name: name, callback: wrapper) - functions.withLock { - $0[function] = { arguments in - try body(arguments) - return .undefined - } + functions[function] = { arguments in + try body(arguments) + return .undefined } } } @@ -39,8 +33,8 @@ extension JSContext { extension JSContext { public func createFunction( name: String, - _ body: @escaping @Sendable () throws -> Value - ) throws { + _ body: @escaping () throws -> Value) throws + { return try createFunction(name: name) { _ in return try body() } @@ -48,8 +42,8 @@ extension JSContext { public func createFunction( name: String, - _ body: @escaping @Sendable () throws -> Void - ) throws { + _ body: @escaping () throws -> Void) throws + { try createFunction(name: name) { _ in try body() } @@ -62,9 +56,9 @@ func wrapper( thisObject: JSObjectRef!, argumentCount: Int, arguments: UnsafePointer?, - exception: UnsafeMutablePointer? -) -> JSValueRef? { - guard let body = functions.withLock({ $0[function] }) else { + exception: UnsafeMutablePointer?) -> JSValueRef? +{ + guard let body = functions[function] else { if let exception = exception { let error = "swift error: unregistered function" exception.pointee = JSValue(string: error, in: ctx).pointer diff --git a/Sources/JavaScriptCore/JSContext.swift b/Sources/SJavaScriptCore/JSContext.swift similarity index 97% rename from Sources/JavaScriptCore/JSContext.swift rename to Sources/SJavaScriptCore/JSContext.swift index 47e5872..964e449 100644 --- a/Sources/JavaScriptCore/JSContext.swift +++ b/Sources/SJavaScriptCore/JSContext.swift @@ -1,3 +1,4 @@ + #if os(Linux) import CJavaScriptCore #else @@ -7,7 +8,7 @@ import JavaScriptCore public class JSContext { let group: JSContextGroupRef let context: JSGlobalContextRef - var exception: JSObjectRef? + var exception: JSObjectRef? = nil var global: JSObjectRef { return JSContextGetGlobalObject(context)! diff --git a/Sources/JavaScriptCore/JSError.swift b/Sources/SJavaScriptCore/JSError.swift similarity index 99% rename from Sources/JavaScriptCore/JSError.swift rename to Sources/SJavaScriptCore/JSError.swift index 09cda29..ee8e9e8 100644 --- a/Sources/JavaScriptCore/JSError.swift +++ b/Sources/SJavaScriptCore/JSError.swift @@ -1,3 +1,4 @@ + #if os(Linux) import CJavaScriptCore #else diff --git a/Sources/JavaScriptCore/JSValue.swift b/Sources/SJavaScriptCore/JSValue.swift similarity index 98% rename from Sources/JavaScriptCore/JSValue.swift rename to Sources/SJavaScriptCore/JSValue.swift index cf95f16..a94a045 100644 --- a/Sources/JavaScriptCore/JSValue.swift +++ b/Sources/SJavaScriptCore/JSValue.swift @@ -1,3 +1,4 @@ + #if os(Linux) import CJavaScriptCore #else @@ -78,7 +79,7 @@ extension JSValue { let property = JSStringCreateWithCharacters(bytes, bytes.count) defer { JSStringRelease(property) } - var exception: JSValueRef? + var exception: JSValueRef? = nil let result = JSObjectGetProperty(context, pointer, property, &exception) if exception != nil { @@ -88,6 +89,7 @@ extension JSValue { } } + extension JSValue { public var isNull: Bool { return JSValueIsNull(context, pointer) diff --git a/Sources/JavaScriptCore/shims.swift b/Sources/SJavaScriptCore/shims.swift similarity index 72% rename from Sources/JavaScriptCore/shims.swift rename to Sources/SJavaScriptCore/shims.swift index 2418ab5..909cdd5 100644 --- a/Sources/JavaScriptCore/shims.swift +++ b/Sources/SJavaScriptCore/shims.swift @@ -1,3 +1,4 @@ + #if os(Linux) import CJavaScriptCore #else @@ -8,7 +9,7 @@ public func JSValueToStringCopy( _ ctx: JSContextRef, _ value: JSValueRef ) throws -> JSStringRef { - var exception: JSValueRef? + var exception: JSValueRef? = nil let result = JSValueToStringCopy(ctx, value, &exception) if let exception = exception { throw JSError(context: ctx, pointer: exception) @@ -20,7 +21,7 @@ public func JSValueToNumber( _ ctx: JSContextRef, _ value: JSValueRef ) throws -> Double { - var exception: JSValueRef? + var exception: JSValueRef? = nil let result = JSValueToNumber(ctx, value, &exception) if let exception = exception { throw JSError(context: ctx, pointer: exception) @@ -36,7 +37,7 @@ public func JSEvaluateScript( _ sourceURL: JSStringRef!, _ startingLineNumber: Int32 ) throws -> JSValueRef { - var exception: JSValueRef? + var exception: JSValueRef? = nil let result = JSEvaluateScript( ctx, script, thisObject, sourceURL, startingLineNumber, &exception) if let exception = exception { @@ -46,7 +47,7 @@ public func JSEvaluateScript( return result! } -public struct JSPropertyAttributes: OptionSet, Sendable { +public struct JSPropertyAttributes: OptionSet { public let rawValue: UInt32 public init(rawValue: UInt32) { @@ -54,18 +55,13 @@ public struct JSPropertyAttributes: OptionSet, Sendable { } /// Specifies that a property has no special attributes. - static let none = JSPropertyAttributes( - rawValue: UInt32(kJSPropertyAttributeNone)) + static let none = JSPropertyAttributes(rawValue: UInt32(kJSPropertyAttributeNone)) /// Specifies that a property is read-only. - static let readOnly = JSPropertyAttributes( - rawValue: UInt32(kJSPropertyAttributeNone)) - /// Specifies that a property should not be enumerated by - /// JSPropertyEnumerators and JavaScript for...in loops. - static let dontEnum = JSPropertyAttributes( - rawValue: UInt32(kJSPropertyAttributeNone)) + static let readOnly = JSPropertyAttributes(rawValue: UInt32(kJSPropertyAttributeNone)) + /// Specifies that a property should not be enumerated by JSPropertyEnumerators and JavaScript for...in loops. + static let dontEnum = JSPropertyAttributes(rawValue: UInt32(kJSPropertyAttributeNone)) /// Specifies that the delete operation should fail on a property. - static let dontDelete = JSPropertyAttributes( - rawValue: UInt32(kJSPropertyAttributeNone)) + static let dontDelete = JSPropertyAttributes(rawValue: UInt32(kJSPropertyAttributeNone)) } public func JSObjectSetProperty( @@ -75,7 +71,7 @@ public func JSObjectSetProperty( _ value: JSValueRef!, _ attributes: JSPropertyAttributes ) throws { - var exception: JSValueRef? + var exception: JSValueRef? = nil JSObjectSetProperty( ctx, object, propertyName, value, attributes.rawValue, &exception) if let exception = exception { diff --git a/Tests/SJavaScriptCore/JSValue.swift b/Tests/SJavaScriptCore/JSValue.swift deleted file mode 100644 index 9045f01..0000000 --- a/Tests/SJavaScriptCore/JSValue.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Testing -@testable import SJavaScriptCore - -@Test func isUndefined() async throws { - let context = JSContext() - - let result = try context.evaluate("undefined") - #expect(result.isUndefined) - #expect(!result.isNull) - #expect(!result.isBool) - #expect(!result.isNumber) - #expect(!result.isString) - #expect(try result.toString() == "undefined") -} - -@Test func isNull() async throws { - let context = JSContext() - let result = try context.evaluate("null") - #expect(!result.isUndefined) - #expect(result.isNull) - #expect(!result.isBool) - #expect(!result.isNumber) - #expect(!result.isString) - #expect(try result.toString() == "null") -} - -@Test func isBool() async throws { - let context = JSContext() - let result = try context.evaluate("true") - #expect(!result.isUndefined) - #expect(!result.isNull) - #expect(result.isBool) - #expect(!result.isNumber) - #expect(!result.isString) - #expect(try result.toString() == "true") - #expect(result.toBool() == true) -} - -@Test func isNumber() async throws { - let context = JSContext() - let result = try context.evaluate("3.14") - #expect(!result.isUndefined) - #expect(!result.isNull) - #expect(!result.isBool) - #expect(result.isNumber) - #expect(!result.isString) - #expect(try result.toString() == "3.14") - #expect(try result.toDouble() == 3.14) -} - -@Test func isString() async throws { - let context = JSContext() - let result = try context.evaluate("'success'") - #expect(!result.isUndefined) - #expect(!result.isNull) - #expect(!result.isBool) - #expect(!result.isNumber) - #expect(result.isString) - #expect(try result.toString() == "success") -} - -@Test func toInt() async throws { - let context = JSContext() - let result = try context.evaluate("40 + 2") - #expect(try result.toInt() == 42) -} - -@Test func toString() async throws { - let context = JSContext() - let result = try context.evaluate("40 + 2") - #expect(try result.toString() == "42") -} - -@Test func property() async throws { - let context = JSContext() - let result = try context.evaluate(""" - (function(){ - return { property: 'test' } - })() - """) - - #expect(try result["property"]?.toString() == "test") -} diff --git a/Tests/SJavaScriptCore/JSValue/main.swift b/Tests/SJavaScriptCore/JSValue/main.swift new file mode 100644 index 0000000..ccdc31e --- /dev/null +++ b/Tests/SJavaScriptCore/JSValue/main.swift @@ -0,0 +1,85 @@ +import Test +@testable import SJavaScriptCore + +test.case("isUndefined") { + let context = JSContext() + + let result = try context.evaluate("undefined") + expect(result.isUndefined) + expect(!result.isNull) + expect(!result.isBool) + expect(!result.isNumber) + expect(!result.isString) + expect(try result.toString() == "undefined") +} + +test.case("isNull") { + let context = JSContext() + let result = try context.evaluate("null") + expect(!result.isUndefined) + expect(result.isNull) + expect(!result.isBool) + expect(!result.isNumber) + expect(!result.isString) + expect(try result.toString() == "null") +} + +test.case("isBool") { + let context = JSContext() + let result = try context.evaluate("true") + expect(!result.isUndefined) + expect(!result.isNull) + expect(result.isBool) + expect(!result.isNumber) + expect(!result.isString) + expect(try result.toString() == "true") + expect(result.toBool() == true) +} + +test.case("isNumber") { + let context = JSContext() + let result = try context.evaluate("3.14") + expect(!result.isUndefined) + expect(!result.isNull) + expect(!result.isBool) + expect(result.isNumber) + expect(!result.isString) + expect(try result.toString() == "3.14") + expect(try result.toDouble() == 3.14) +} + +test.case("isString") { + let context = JSContext() + let result = try context.evaluate("'success'") + expect(!result.isUndefined) + expect(!result.isNull) + expect(!result.isBool) + expect(!result.isNumber) + expect(result.isString) + expect(try result.toString() == "success") +} + +test.case("toInt()") { + let context = JSContext() + let result = try context.evaluate("40 + 2") + expect(try result.toInt() == 42) +} + +test.case("toString()") { + let context = JSContext() + let result = try context.evaluate("40 + 2") + expect(try result.toString() == "42") +} + +test.case("property") { + let context = JSContext() + let result = try context.evaluate(""" + (function(){ + return { property: 'test' } + })() + """) + + expect(try result["property"]?.toString() == "test") +} + +test.run() diff --git a/Tests/SJavaScriptCore/JavaScript.swift b/Tests/SJavaScriptCore/JavaScript/main.swift similarity index 58% rename from Tests/SJavaScriptCore/JavaScript.swift rename to Tests/SJavaScriptCore/JavaScript/main.swift index 1300bf7..efdc8a5 100644 --- a/Tests/SJavaScriptCore/JavaScript.swift +++ b/Tests/SJavaScriptCore/JavaScript/main.swift @@ -1,112 +1,112 @@ -import Testing +import Test @testable import SJavaScriptCore -@Test func evaluate() async throws { +test.case("evaluate") { let context = JSContext() _ = try context.evaluate("40 + 2") } -@Test func exception() async throws { +test.case("exception") { let context = JSContext() - #expect(throws: JSError("Can't find variable: x")) { + expect(throws: JSError("Can't find variable: x")) { try context.evaluate("x()") } - #expect(throws: JSError("Unexpected end of script")) { + expect(throws: JSError("Unexpected end of script")) { try context.evaluate("{") } } -@Test func function() async throws { +test.case("function") { let context = JSContext() try context.createFunction(name: "test") { (_) -> Value in return .string("success") } let result = try context.evaluate("test()") - #expect(try result.toString() == "success") + expect(try result.toString() == "success") } -@Test func closure() async throws { +test.case("closure") { let context = JSContext() try context.createFunction(name: "testUndefined") { return .undefined } let undefinedResult = try context.evaluate("testUndefined()") - #expect(undefinedResult.isUndefined) + expect(undefinedResult.isUndefined) try context.createFunction(name: "testNull") { return .null } let nullResult = try context.evaluate("testNull()") - #expect(nullResult.isNull) + expect(nullResult.isNull) try context.createFunction(name: "testBool") { return .bool(true) } let boolResult = try context.evaluate("testBool()") - #expect(boolResult.isBool) + expect(boolResult.isBool) try context.createFunction(name: "testNumber") { return .number(3.14) } let numberResult = try context.evaluate("testNumber()") - #expect(numberResult.isNumber) + expect(numberResult.isNumber) try context.createFunction(name: "testString") { return .string("success") } let stringResult = try context.evaluate("testString()") - #expect(stringResult.isString) + expect(stringResult.isString) } -@Test func capture() async throws { +test.case("capture") { let context = JSContext() - final class Box: @unchecked Sendable { - var captured = false - } - let box = Box() + + var captured = false try context.createFunction(name: "test") { (_) -> Value in - box.captured = true + captured = true return .string("captured") } let result = try context.evaluate("test()") - #expect(box.captured == true) - #expect("\(result)" == "captured") + expect(captured) + expect("\(result)" == "captured") } -@Test func arguments() async throws { +test.case("arguments") { let context = JSContext() try context.createFunction(name: "test") { (arguments) -> Void in - #expect(arguments.count == 2) - try #expect(arguments.first?.toString() == "one") - try #expect(arguments.last?.toInt() == 42) + expect(arguments.count == 2) + expect(try arguments.first?.toString() == "one") + expect(try arguments.last?.toInt() == 42) } try context.evaluate("test('one', 42)") } -@Test func `persistent context`() async throws { +test.case("persistent context") { let context = JSContext() try context.evaluate("result = 'success'") - #expect(try context.evaluate("result").toString() == "success") + expect(try context.evaluate("result").toString() == "success") - try context.createFunction(name: "test") { (_) -> Value in + try context.createFunction(name: "test") { (arguments) -> Value in return .string("test ok") } - #expect(try context.evaluate("result").toString() == "success") + expect(try context.evaluate("result").toString() == "success") } -@Test func sandbox() async throws { +test.case("sandbox") { try { let context = JSContext() try context.evaluate("test = 'hello'") let result = try context.evaluate("test") - #expect(try result.toString() == "hello") + expect(try result.toString() == "hello") }() let context = JSContext() - #expect(throws: JSError("Can\'t find variable: test")) { + expect(throws: JSError("Can\'t find variable: test")) { try context.evaluate("test") } } + +test.run() diff --git a/run_tests b/run_tests new file mode 100755 index 0000000..ff5b535 --- /dev/null +++ b/run_tests @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +swift build + +export DYLD_LIBRARY_PATH=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/lib/swift/macosx + +.build/debug/Tests/SJavaScriptCore/JavaScript +.build/debug/Tests/SJavaScriptCore/JSValue