diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3cdb46c..666b62d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,6 +71,18 @@ Tests for `PackageToJS` plugin: swift test --package-path ./Plugins/PackageToJS ``` +Tests for `BridgeJS` plugin: + +```bash +swift test --package-path ./Plugins/BridgeJS +``` + +To update snapshot test files when expected output changes: + +```bash +UPDATE_SNAPSHOTS=1 swift test --package-path ./Plugins/BridgeJS +``` + ### Editing `./Runtime` directory The `./Runtime` directory contains the JavaScript runtime that interacts with the JavaScript environment and Swift code. diff --git a/Examples/ExportSwift/index.js b/Examples/ExportSwift/index.js index 4c5576b2..fcf7c983 100644 --- a/Examples/ExportSwift/index.js +++ b/Examples/ExportSwift/index.js @@ -2,7 +2,7 @@ import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; const { exports } = await init({}); const Greeter = exports.Greeter; -const greeter = new Greeter("World"); +const greeter = Greeter.init("World"); const circle = exports.renderCircleSVG(100); // Display the results diff --git a/Examples/PlayBridgeJS/Sources/JavaScript/app.js b/Examples/PlayBridgeJS/Sources/JavaScript/app.js index b14db79b..30179791 100644 --- a/Examples/PlayBridgeJS/Sources/JavaScript/app.js +++ b/Examples/PlayBridgeJS/Sources/JavaScript/app.js @@ -52,7 +52,7 @@ export class BridgeJSPlayground { createTS2Skeleton: this.createTS2Skeleton } }); - this.playBridgeJS = new exports.PlayBridgeJS(); + this.playBridgeJS = exports.PlayBridgeJS.init(); console.log('BridgeJS initialized successfully'); } catch (error) { console.error('Failed to initialize BridgeJS:', error); @@ -162,4 +162,4 @@ export class BridgeJSPlayground { hideError() { this.errorDisplay.classList.remove('show'); } -} \ No newline at end of file +} diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 6693c815..7dbe3cbb 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -43,6 +43,10 @@ struct BridgeJSLink { let swiftHeapObjectClassJs = """ /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { + static __construct(ptr, deinit) { + return new SwiftHeapObject(ptr, deinit); + } + constructor(pointer, deinit) { this.pointer = pointer; this.hasReleased = false; @@ -401,7 +405,7 @@ struct BridgeJSLink { bodyLines.append("swift.memory.release(retId);") returnExpr = "ret" case .swiftHeapObject(let name): - bodyLines.append("const ret = new \(name)(\(call));") + bodyLines.append("const ret = \(name).__construct(\(call));") returnExpr = "ret" } return returnExpr @@ -484,23 +488,37 @@ struct BridgeJSLink { dtsExportEntryLines.append("\(klass.name): {") jsLines.append("class \(klass.name) extends SwiftHeapObject {") + // Always add __construct and constructor methods for all classes + var constructorLines: [String] = [] + constructorLines.append("static __construct(ptr) {") + constructorLines.append( + "return new \(klass.name)(ptr, instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4) + ) + constructorLines.append("}") + constructorLines.append("") + constructorLines.append("constructor(pointer, deinit) {") + constructorLines.append("super(pointer, deinit);".indent(count: 4)) + constructorLines.append("}") + jsLines.append(contentsOf: constructorLines.map { $0.indent(count: 4) }) + if let constructor: ExportedConstructor = klass.constructor { let thunkBuilder = ExportedThunkBuilder(effects: constructor.effects) for param in constructor.parameters { thunkBuilder.lowerParameter(param: param) } var funcLines: [String] = [] - funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {") + funcLines.append("") + funcLines.append("static init(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {") let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) }) funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) }) funcLines.append(contentsOf: thunkBuilder.checkExceptionLines().map { $0.indent(count: 4) }) - funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)) + funcLines.append("return \(klass.name).__construct(\(returnExpr));".indent(count: 4)) funcLines.append("}") jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) }) dtsExportEntryLines.append( - "new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));" + "init\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));" .indent(count: 4) ) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts index b2ccecc4..65b9360e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts @@ -51,10 +51,10 @@ export interface UUID extends SwiftHeapObject { } export type Exports = { Greeter: { - new(name: string): Greeter; + init(name: string): Greeter; } Converter: { - new(): Converter; + init(): Converter; } UUID: { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js index dce99393..a9678497 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js @@ -60,6 +60,10 @@ export async function createInstantiator(options, swift) { const js = swift.memory.heap; /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { + static __construct(ptr, deinit) { + return new SwiftHeapObject(ptr, deinit); + } + constructor(pointer, deinit) { this.pointer = pointer; this.hasReleased = false; @@ -76,12 +80,20 @@ export async function createInstantiator(options, swift) { } } class Greeter extends SwiftHeapObject { - constructor(name) { + static __construct(ptr) { + return new Greeter(ptr, instance.exports.bjs_Greeter_deinit); + } + + constructor(pointer, deinit) { + super(pointer, deinit); + } + + static init(name) { const nameBytes = textEncoder.encode(name); const nameId = swift.memory.retain(nameBytes); const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length); swift.memory.release(nameId); - super(ret, instance.exports.bjs_Greeter_deinit); + return Greeter.__construct(ret); } greet() { instance.exports.bjs_Greeter_greet(this.pointer); @@ -91,9 +103,17 @@ export async function createInstantiator(options, swift) { } } class Converter extends SwiftHeapObject { - constructor() { + static __construct(ptr) { + return new Converter(ptr, instance.exports.bjs_Converter_deinit); + } + + constructor(pointer, deinit) { + super(pointer, deinit); + } + + static init() { const ret = instance.exports.bjs_Converter_init(); - super(ret, instance.exports.bjs_Converter_deinit); + return Converter.__construct(ret); } toString(value) { instance.exports.bjs_Converter_toString(this.pointer, value); @@ -103,6 +123,13 @@ export async function createInstantiator(options, swift) { } } class UUID extends SwiftHeapObject { + static __construct(ptr) { + return new UUID(ptr, instance.exports.bjs_UUID_deinit); + } + + constructor(pointer, deinit) { + super(pointer, deinit); + } uuidString() { instance.exports.bjs_UUID_uuidString(this.pointer); const ret = tmpRetString; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts index fd376d57..65391433 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts @@ -17,7 +17,7 @@ export interface Greeter extends SwiftHeapObject { } export type Exports = { Greeter: { - new(name: string): Greeter; + init(name: string): Greeter; } takeGreeter(greeter: Greeter): void; } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js index 7a5938a1..9f79f20f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js @@ -60,6 +60,10 @@ export async function createInstantiator(options, swift) { const js = swift.memory.heap; /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { + static __construct(ptr, deinit) { + return new SwiftHeapObject(ptr, deinit); + } + constructor(pointer, deinit) { this.pointer = pointer; this.hasReleased = false; @@ -76,12 +80,20 @@ export async function createInstantiator(options, swift) { } } class Greeter extends SwiftHeapObject { - constructor(name) { + static __construct(ptr) { + return new Greeter(ptr, instance.exports.bjs_Greeter_deinit); + } + + constructor(pointer, deinit) { + super(pointer, deinit); + } + + static init(name) { const nameBytes = textEncoder.encode(name); const nameId = swift.memory.retain(nameBytes); const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length); swift.memory.release(nameId); - super(ret, instance.exports.bjs_Greeter_deinit); + return Greeter.__construct(ret); } greet() { instance.exports.bjs_Greeter_greet(this.pointer); diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md index 6ce30772..6de7db0b 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md @@ -133,7 +133,7 @@ In JavaScript: import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; const { exports } = await init({}); -const cart = new exports.ShoppingCart(); +const cart = exports.ShoppingCart.init(); cart.addItem("Laptop", 999.99, 1); cart.addItem("Mouse", 24.99, 2); console.log(`Items in cart: ${cart.getItemCount()}`); @@ -158,7 +158,7 @@ export interface ShoppingCart extends SwiftHeapObject { export type Exports = { ShoppingCart: { - new(): ShoppingCart; + init(): ShoppingCart; } } ``` @@ -175,8 +175,8 @@ You can export functions to specific namespaces by providing a namespace paramet import JavaScriptKit // Export a function to a custom namespace -@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String { - return "namespaced" +@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String { + return "namespaced" } ``` diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 2b78b96b..7b561f25 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -74,13 +74,47 @@ struct TestError: Error { g.changeName(name: name) } +// Test class without @JS init constructor +@JS class Calculator { + nonisolated(unsafe) static var onDeinit: () -> Void = {} + + @JS func square(value: Int) -> Int { + return value * value + } + + @JS func add(a: Int, b: Int) -> Int { + return a + b + } + + deinit { + Self.onDeinit() + } +} + +@JS func createCalculator() -> Calculator { + return Calculator() +} + +@JS func useCalculator(calc: Calculator, x: Int, y: Int) -> Int { + return calc.add(a: calc.square(value: x), b: y) +} + class ExportAPITests: XCTestCase { func testAll() { var hasDeinitGreeter = false + var hasDeinitCalculator = false + Greeter.onDeinit = { hasDeinitGreeter = true } + + Calculator.onDeinit = { + hasDeinitCalculator = true + } + runJsWorks() - XCTAssertTrue(hasDeinitGreeter) + + XCTAssertTrue(hasDeinitGreeter, "Greeter (with @JS init) should have been deinitialized") + XCTAssertTrue(hasDeinitCalculator, "Calculator (without @JS init) should have been deinitialized") } } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift index 2a91da9f..41f0d882 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift @@ -314,6 +314,28 @@ public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameL #endif } +@_expose(wasm, "bjs_createCalculator") +@_cdecl("bjs_createCalculator") +public func _bjs_createCalculator() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = createCalculator() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_useCalculator") +@_cdecl("bjs_useCalculator") +public func _bjs_useCalculator(calc: UnsafeMutableRawPointer, x: Int32, y: Int32) -> Int32 { + #if arch(wasm32) + let ret = useCalculator(calc: Unmanaged.fromOpaque(calc).takeUnretainedValue(), x: Int(x), y: Int(y)) + return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_init") @_cdecl("bjs_Greeter_init") public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer { @@ -360,4 +382,32 @@ public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: I @_cdecl("bjs_Greeter_deinit") public func _bjs_Greeter_deinit(pointer: UnsafeMutableRawPointer) { Unmanaged.fromOpaque(pointer).release() +} + +@_expose(wasm, "bjs_Calculator_square") +@_cdecl("bjs_Calculator_square") +public func _bjs_Calculator_square(_self: UnsafeMutableRawPointer, value: Int32) -> Int32 { + #if arch(wasm32) + let ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().square(value: Int(value)) + return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Calculator_add") +@_cdecl("bjs_Calculator_add") +public func _bjs_Calculator_add(_self: UnsafeMutableRawPointer, a: Int32, b: Int32) -> Int32 { + #if arch(wasm32) + let ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().add(a: Int(a), b: Int(b)) + return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Calculator_deinit") +@_cdecl("bjs_Calculator_deinit") +public func _bjs_Calculator_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json index 7a467cc3..ad759cec 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -62,6 +62,68 @@ } ], "name" : "Greeter" + }, + { + "methods" : [ + { + "abiName" : "bjs_Calculator_square", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "square", + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_Calculator_add", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "add", + "parameters" : [ + { + "label" : "a", + "name" : "a", + "type" : { + "int" : { + + } + } + }, + { + "label" : "b", + "name" : "b", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + } + } + ], + "name" : "Calculator" } ], "functions" : [ @@ -415,6 +477,64 @@ "returnType" : { "void" : { + } + } + }, + { + "abiName" : "bjs_createCalculator", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "createCalculator", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "Calculator" + } + } + }, + { + "abiName" : "bjs_useCalculator", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "useCalculator", + "parameters" : [ + { + "label" : "calc", + "name" : "calc", + "type" : { + "swiftHeapObject" : { + "_0" : "Calculator" + } + } + }, + { + "label" : "x", + "name" : "x", + "type" : { + "int" : { + + } + } + }, + { + "label" : "y", + "name" : "y", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + } } } diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index c6ac428a..ddb232d2 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -115,17 +115,27 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.roundTripString(v), v); } - const g = new exports.Greeter("John"); - const g2 = exports.roundTripSwiftHeapObject(g) - g2.release(); - + const g = exports.Greeter.init("John"); assert.equal(g.greet(), "Hello, John!"); g.changeName("Jane"); assert.equal(g.greet(), "Hello, Jane!"); exports.takeGreeter(g, "Jay"); assert.equal(g.greet(), "Hello, Jay!"); + + const g2 = exports.roundTripSwiftHeapObject(g) + assert.equal(g2.greet(), "Hello, Jay!"); + g2.release(); + g.release(); + // Test class without @JS init constructor + const calc = exports.createCalculator(); + assert.equal(calc.square(5), 25); + assert.equal(calc.add(3, 4), 7); + assert.equal(exports.useCalculator(calc, 3, 10), 19); // 3^2 + 10 = 19 + + calc.release(); + const anyObject = {}; assert.equal(exports.roundTripJSObject(anyObject), anyObject);