From 86f07ab426383d4661c0897b2c560391f8a7c7e5 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Fri, 6 Mar 2026 14:03:33 +1100 Subject: [PATCH 1/5] BridgeJS: Correctly emit @JS methods in extensions --- .../BridgeJSCore/SwiftToSkeleton.swift | 60 +++++++++++++- .../BridgeJSCodegenTests.swift | 18 ++++ .../Multifile/CrossFileExtension.swift | 5 ++ .../Multifile/CrossFileExtensionClass.swift | 4 + .../Inputs/MacroSwift/StaticFunctions.swift | 12 +++ .../Inputs/MacroSwift/SwiftClass.swift | 6 ++ .../Inputs/MacroSwift/SwiftStruct.swift | 6 ++ .../CrossFileExtension.json | 82 +++++++++++++++++++ .../CrossFileExtension.swift | 60 ++++++++++++++ .../StaticFunctions.Global.json | 69 ++++++++++++++++ .../StaticFunctions.Global.swift | 22 +++++ .../BridgeJSCodegenTests/StaticFunctions.json | 69 ++++++++++++++++ .../StaticFunctions.swift | 22 +++++ .../BridgeJSCodegenTests/SwiftClass.json | 17 ++++ .../BridgeJSCodegenTests/SwiftClass.swift | 11 +++ .../BridgeJSCodegenTests/SwiftStruct.json | 16 ++++ .../BridgeJSCodegenTests/SwiftStruct.swift | 11 +++ .../BridgeJSLinkTests/SwiftClass.d.ts | 1 + .../BridgeJSLinkTests/SwiftClass.js | 6 ++ 19 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtension.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtensionClass.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.swift diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index 8f1b3fa35..2ff5cecba 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -43,12 +43,14 @@ public final class SwiftToSkeleton { var perSourceErrors: [(inputFilePath: String, errors: [DiagnosticError])] = [] var importedFiles: [ImportedFileSkeleton] = [] var exported = ExportedSkeleton(functions: [], classes: [], enums: [], exposeToGlobal: exposeToGlobal) + var exportCollectors: [ExportSwiftAPICollector] = [] for (sourceFile, inputFilePath) in sourceFiles { progress.print("Processing \(inputFilePath)") let exportCollector = ExportSwiftAPICollector(parent: self) exportCollector.walk(sourceFile) + exportCollectors.append(exportCollector) let typeNameCollector = ImportSwiftMacrosJSImportTypeNameCollector(viewMode: .sourceAccurate) typeNameCollector.walk(sourceFile) @@ -74,7 +76,15 @@ public final class SwiftToSkeleton { if !importedFile.isEmpty { importedFiles.append(importedFile) } - exportCollector.finalize(&exported) + } + + // Resolve extensions against all collectors. This needs to happen at this point so we can resolve both same file and cross file extensions. + for source in exportCollectors { + source.resolveDeferredExtensions(against: exportCollectors) + } + + for collector in exportCollectors { + collector.finalize(&exported) } if !perSourceErrors.isEmpty { @@ -486,6 +496,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { var exportedStructNames: [String] = [] var exportedStructByName: [String: ExportedStruct] = [:] var errors: [DiagnosticError] = [] + /// Extensions collected during the walk, to be resolved after all files have been walked + var deferredExtensions: [ExtensionDeclSyntax] = [] func finalize(_ result: inout ExportedSkeleton) { result.functions.append(contentsOf: exportedFunctions) @@ -1388,6 +1400,52 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { } } + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + // Defer until all type declarations in the module have been collected. + deferredExtensions.append(node) + return .skipChildren + } + + func resolveDeferredExtensions(against collectors: [ExportSwiftAPICollector]) { + for ext in deferredExtensions { + var resolved = false + for collector in collectors { + if collector.resolveExtension(ext) { + resolved = true + break + } + } + if !resolved { + diagnose( + node: ext.extendedType, + message: "Unsupported type '\(ext.extendedType.trimmedDescription)'.", + hint: "You can only extend `@JS` annotated types defined in the same module" + ) + } + } + } + + /// Walks extension members under the matching type’s state, returning whether the type was found + func resolveExtension(_ ext: ExtensionDeclSyntax) -> Bool { + let name = ext.extendedType.trimmedDescription + let state: State + if let entry = exportedClassByName.first(where: { $0.value.name == name }) { + state = .classBody(name: name, key: entry.key) + } else if let entry = exportedStructByName.first(where: { $0.value.name == name }) { + state = .structBody(name: name, key: entry.key) + } else if let entry = exportedEnumByName.first(where: { $0.value.name == name }) { + state = .enumBody(name: name, key: entry.key) + } else { + return false + } + stateStack.push(state: state) + for member in ext.memberBlock.members { + walk(member) + } + stateStack.pop() + return true + } + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { guard let jsAttribute = node.attributes.firstJSAttribute else { return .skipChildren diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift index 9754fbced..1b1f95f76 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift @@ -167,6 +167,24 @@ import Testing try snapshotCodegen(skeleton: skeleton, name: "CrossFileFunctionTypes.ReverseOrder") } + @Test + func codegenCrossFileExtension() throws { + let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false) + let classURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileExtensionClass.swift") + swiftAPI.addSourceFile( + Parser.parse(source: try String(contentsOf: classURL, encoding: .utf8)), + inputFilePath: "CrossFileExtensionClass.swift" + ) + let extensionURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileExtension.swift") + swiftAPI.addSourceFile( + Parser.parse(source: try String(contentsOf: extensionURL, encoding: .utf8)), + inputFilePath: "CrossFileExtension.swift" + ) + let skeleton = try swiftAPI.finalize() + try snapshotCodegen(skeleton: skeleton, name: "CrossFileExtension") + } + + @Test func codegenSkipsEmptySkeletons() throws { let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtension.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtension.swift new file mode 100644 index 000000000..ce9e8e2b0 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtension.swift @@ -0,0 +1,5 @@ +extension Greeter { + @JS func greetFormally() -> String { + return "Good day, " + self.name + "." + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtensionClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtensionClass.swift new file mode 100644 index 000000000..48625d42a --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Multifile/CrossFileExtensionClass.swift @@ -0,0 +1,4 @@ +@JS class Greeter { + @JS init(name: String) {} + @JS func greet() -> String { return "" } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift index 1d42cf415..25196d773 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift @@ -38,3 +38,15 @@ enum APIResult { } } } + +extension MathUtils { + @JS static func divide(a: Int, b: Int) -> Int { + return a / b + } +} + +extension Calculator { + @JS static func cube(value: Int) -> Int { + return value * value * value + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift index d7b5a5b8e..12004ffa8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift @@ -12,6 +12,12 @@ } } +extension Greeter { + @JS func greetEnthusiastically() -> String { + return "Hey, " + self.name + "!!!" + } +} + @JS func takeGreeter(greeter: Greeter) { print(greeter.greet()) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift index 0d84f4736..d42b2e202 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift @@ -60,3 +60,9 @@ } @JS func roundtripContainer(_ container: Container) -> Container + +extension DataPoint { + @JS func distanceFromOrigin() -> Double { + return (x * x + y * y).squareRoot() + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json new file mode 100644 index 000000000..f77d39ad9 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.json @@ -0,0 +1,82 @@ +{ + "exported" : { + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_Greeter_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_Greeter_greet", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "greet", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_Greeter_greetFormally", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "greetFormally", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "Greeter", + "properties" : [ + + ], + "swiftCallName" : "Greeter" + } + ], + "enums" : [ + + ], + "exposeToGlobal" : false, + "functions" : [ + + ], + "protocols" : [ + + ], + "structs" : [ + + ] + }, + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.swift new file mode 100644 index 000000000..ab73df508 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/CrossFileExtension.swift @@ -0,0 +1,60 @@ +@_expose(wasm, "bjs_Greeter_init") +@_cdecl("bjs_Greeter_init") +public func _bjs_Greeter_init(_ nameBytes: Int32, _ nameLength: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Greeter(name: String.bridgeJSLiftParameter(nameBytes, nameLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_greet") +@_cdecl("bjs_Greeter_greet") +public func _bjs_Greeter_greet(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).greet() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_greetFormally") +@_cdecl("bjs_Greeter_greetFormally") +public func _bjs_Greeter_greetFormally(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).greetFormally() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_deinit") +@_cdecl("bjs_Greeter_deinit") +public func _bjs_Greeter_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension Greeter: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_Greeter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_Greeter_wrap") +fileprivate func _bjs_Greeter_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_Greeter_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_Greeter_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_Greeter_wrap_extern(pointer) +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json index b0eac3313..a5fc4e7e1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json @@ -125,6 +125,45 @@ } } + }, + { + "abiName" : "bjs_MathUtils_static_divide", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "divide", + "parameters" : [ + { + "label" : "a", + "name" : "a", + "type" : { + "int" : { + + } + } + }, + { + "label" : "b", + "name" : "b", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + }, + "staticContext" : { + "className" : { + "_0" : "MathUtils" + } + } } ], "name" : "MathUtils", @@ -182,6 +221,36 @@ "_0" : "Calculator" } } + }, + { + "abiName" : "bjs_Calculator_static_cube", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "cube", + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + }, + "staticContext" : { + "enumName" : { + "_0" : "Calculator" + } + } } ], "staticProperties" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift index b6d35a215..3478eb883 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift @@ -44,6 +44,17 @@ public func _bjs_Calculator_static_square(_ value: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_Calculator_static_cube") +@_cdecl("bjs_Calculator_static_cube") +public func _bjs_Calculator_static_cube(_ value: Int32) -> Int32 { + #if arch(wasm32) + let ret = Calculator.cube(value: Int.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension APIResult: _BridgedSwiftAssociatedValueEnum { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPopPayload(_ caseId: Int32) -> APIResult { switch caseId { @@ -134,6 +145,17 @@ public func _bjs_MathUtils_multiply(_ _self: UnsafeMutableRawPointer, _ x: Int32 #endif } +@_expose(wasm, "bjs_MathUtils_static_divide") +@_cdecl("bjs_MathUtils_static_divide") +public func _bjs_MathUtils_static_divide(_ a: Int32, _ b: Int32) -> Int32 { + #if arch(wasm32) + let ret = MathUtils.divide(a: Int.bridgeJSLiftParameter(a), b: Int.bridgeJSLiftParameter(b)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_MathUtils_deinit") @_cdecl("bjs_MathUtils_deinit") public func _bjs_MathUtils_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json index e4ec22855..b620fb0e2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json @@ -125,6 +125,45 @@ } } + }, + { + "abiName" : "bjs_MathUtils_static_divide", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "divide", + "parameters" : [ + { + "label" : "a", + "name" : "a", + "type" : { + "int" : { + + } + } + }, + { + "label" : "b", + "name" : "b", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + }, + "staticContext" : { + "className" : { + "_0" : "MathUtils" + } + } } ], "name" : "MathUtils", @@ -182,6 +221,36 @@ "_0" : "Calculator" } } + }, + { + "abiName" : "bjs_Calculator_static_cube", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "cube", + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + }, + "staticContext" : { + "enumName" : { + "_0" : "Calculator" + } + } } ], "staticProperties" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift index b6d35a215..3478eb883 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift @@ -44,6 +44,17 @@ public func _bjs_Calculator_static_square(_ value: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_Calculator_static_cube") +@_cdecl("bjs_Calculator_static_cube") +public func _bjs_Calculator_static_cube(_ value: Int32) -> Int32 { + #if arch(wasm32) + let ret = Calculator.cube(value: Int.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension APIResult: _BridgedSwiftAssociatedValueEnum { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPopPayload(_ caseId: Int32) -> APIResult { switch caseId { @@ -134,6 +145,17 @@ public func _bjs_MathUtils_multiply(_ _self: UnsafeMutableRawPointer, _ x: Int32 #endif } +@_expose(wasm, "bjs_MathUtils_static_divide") +@_cdecl("bjs_MathUtils_static_divide") +public func _bjs_MathUtils_static_divide(_ a: Int32, _ b: Int32) -> Int32 { + #if arch(wasm32) + let ret = MathUtils.divide(a: Int.bridgeJSLiftParameter(a), b: Int.bridgeJSLiftParameter(b)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_MathUtils_deinit") @_cdecl("bjs_MathUtils_deinit") public func _bjs_MathUtils_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json index 7cebdd5e6..0245bf208 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json @@ -61,6 +61,23 @@ "returnType" : { "void" : { + } + } + }, + { + "abiName" : "bjs_Greeter_greetEnthusiastically", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "greetEnthusiastically", + "parameters" : [ + + ], + "returnType" : { + "string" : { + } } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift index 0e9434832..9f927da13 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift @@ -40,6 +40,17 @@ public func _bjs_Greeter_changeName(_ _self: UnsafeMutableRawPointer, _ nameByte #endif } +@_expose(wasm, "bjs_Greeter_greetEnthusiastically") +@_cdecl("bjs_Greeter_greetEnthusiastically") +public func _bjs_Greeter_greetEnthusiastically(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).greetEnthusiastically() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_name_get") @_cdecl("bjs_Greeter_name_get") public func _bjs_Greeter_name_get(_ _self: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json index 00c6af5cb..d216f10c6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json @@ -211,7 +211,23 @@ ] }, "methods" : [ + { + "abiName" : "bjs_DataPoint_distanceFromOrigin", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "distanceFromOrigin", + "parameters" : [ + ], + "returnType" : { + "double" : { + + } + } + } ], "name" : "DataPoint", "properties" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift index 6fccb3280..5e867f179 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift @@ -66,6 +66,17 @@ public func _bjs_DataPoint_init(_ x: Float64, _ y: Float64, _ labelBytes: Int32, #endif } +@_expose(wasm, "bjs_DataPoint_distanceFromOrigin") +@_cdecl("bjs_DataPoint_distanceFromOrigin") +public func _bjs_DataPoint_distanceFromOrigin() -> Float64 { + #if arch(wasm32) + let ret = DataPoint.bridgeJSLiftParameter().distanceFromOrigin() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension Address: _BridgedSwiftStruct { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> Address { let zipCode = Optional.bridgeJSStackPop() diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts index 05fc97fee..72f816bd5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts @@ -14,6 +14,7 @@ export interface SwiftHeapObject { export interface Greeter extends SwiftHeapObject { greet(): string; changeName(name: string): void; + greetEnthusiastically(): string; name: string; } export interface PublicGreeter extends SwiftHeapObject { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js index 9acf70de2..2596b539a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js @@ -282,6 +282,12 @@ export async function createInstantiator(options, swift) { const nameId = swift.memory.retain(nameBytes); instance.exports.bjs_Greeter_changeName(this.pointer, nameId, nameBytes.length); } + greetEnthusiastically() { + instance.exports.bjs_Greeter_greetEnthusiastically(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } get name() { instance.exports.bjs_Greeter_name_get(this.pointer); const ret = tmpRetString; From 6762c6b030795956d229544035c554ffec6dc877 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Tue, 10 Mar 2026 13:57:04 +1100 Subject: [PATCH 2/5] BridgeJS: Improve test coverage for @JS methods and properties in extensions --- .../Inputs/MacroSwift/StaticFunctions.swift | 4 + .../Inputs/MacroSwift/SwiftClass.swift | 8 + .../Inputs/MacroSwift/SwiftStruct.swift | 6 + .../StaticFunctions.Global.json | 28 +++ .../StaticFunctions.Global.swift | 22 +++ .../BridgeJSCodegenTests/StaticFunctions.json | 28 +++ .../StaticFunctions.swift | 22 +++ .../BridgeJSCodegenTests/SwiftClass.json | 47 +++++ .../BridgeJSCodegenTests/SwiftClass.swift | 33 ++++ .../BridgeJSCodegenTests/SwiftStruct.json | 37 ++++ .../BridgeJSCodegenTests/SwiftStruct.swift | 22 +++ .../StaticFunctions.Global.d.ts | 4 + .../StaticFunctions.Global.js | 18 ++ .../BridgeJSLinkTests/StaticFunctions.d.ts | 4 + .../BridgeJSLinkTests/StaticFunctions.js | 18 ++ .../BridgeJSLinkTests/SwiftClass.d.ts | 3 + .../BridgeJSLinkTests/SwiftClass.js | 16 ++ .../BridgeJSLinkTests/SwiftStruct.d.ts | 3 + .../BridgeJSLinkTests/SwiftStruct.js | 17 +- .../BridgeJSRuntimeTests/ExportAPITests.swift | 22 +++ .../Generated/BridgeJS.swift | 99 +++++++++++ .../Generated/JavaScript/BridgeJS.json | 161 ++++++++++++++++++ Tests/BridgeJSRuntimeTests/StructAPIs.swift | 12 ++ Tests/prelude.mjs | 19 +++ 24 files changed, 652 insertions(+), 1 deletion(-) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift index 25196d773..4f6296d2e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/StaticFunctions.swift @@ -43,10 +43,14 @@ extension MathUtils { @JS static func divide(a: Int, b: Int) -> Int { return a / b } + + @JS static var pi: Double { 3.14159 } } extension Calculator { @JS static func cube(value: Int) -> Int { return value * value * value } + + @JS static var version: String { "1.0" } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift index 12004ffa8..2fb050eeb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift @@ -16,6 +16,14 @@ extension Greeter { @JS func greetEnthusiastically() -> String { return "Hey, " + self.name + "!!!" } + + @JS var nameCount: Int { name.count } + + @JS static func greetAnonymously() -> String { + return "Hello." + } + + @JS static var defaultGreeting: String { "Hello, world!" } } @JS func takeGreeter(greeter: Greeter) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift index d42b2e202..4ec6525e3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift @@ -65,4 +65,10 @@ extension DataPoint { @JS func distanceFromOrigin() -> Double { return (x * x + y * y).squareRoot() } + + @JS static func origin() -> DataPoint { + return DataPoint(x: 0, y: 0, label: "origin", optCount: nil, optFlag: nil) + } + + @JS static var dimensions: Int { 2 } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json index a5fc4e7e1..3e07317ac 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.json @@ -168,7 +168,21 @@ ], "name" : "MathUtils", "properties" : [ + { + "isReadonly" : true, + "isStatic" : true, + "name" : "pi", + "staticContext" : { + "className" : { + "_0" : "MathUtils" + } + }, + "type" : { + "double" : { + } + } + } ], "swiftCallName" : "MathUtils" } @@ -254,7 +268,21 @@ } ], "staticProperties" : [ + { + "isReadonly" : true, + "isStatic" : true, + "name" : "version", + "staticContext" : { + "enumName" : { + "_0" : "Calculator" + } + }, + "type" : { + "string" : { + } + } + } ], "swiftCallName" : "Calculator", "tsFullPath" : "Calculator" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift index 3478eb883..aa7af111f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.Global.swift @@ -55,6 +55,17 @@ public func _bjs_Calculator_static_cube(_ value: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_Calculator_static_version_get") +@_cdecl("bjs_Calculator_static_version_get") +public func _bjs_Calculator_static_version_get() -> Void { + #if arch(wasm32) + let ret = Calculator.version + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension APIResult: _BridgedSwiftAssociatedValueEnum { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPopPayload(_ caseId: Int32) -> APIResult { switch caseId { @@ -156,6 +167,17 @@ public func _bjs_MathUtils_static_divide(_ a: Int32, _ b: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_MathUtils_static_pi_get") +@_cdecl("bjs_MathUtils_static_pi_get") +public func _bjs_MathUtils_static_pi_get() -> Float64 { + #if arch(wasm32) + let ret = MathUtils.pi + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_MathUtils_deinit") @_cdecl("bjs_MathUtils_deinit") public func _bjs_MathUtils_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json index b620fb0e2..a6540015f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.json @@ -168,7 +168,21 @@ ], "name" : "MathUtils", "properties" : [ + { + "isReadonly" : true, + "isStatic" : true, + "name" : "pi", + "staticContext" : { + "className" : { + "_0" : "MathUtils" + } + }, + "type" : { + "double" : { + } + } + } ], "swiftCallName" : "MathUtils" } @@ -254,7 +268,21 @@ } ], "staticProperties" : [ + { + "isReadonly" : true, + "isStatic" : true, + "name" : "version", + "staticContext" : { + "enumName" : { + "_0" : "Calculator" + } + }, + "type" : { + "string" : { + } + } + } ], "swiftCallName" : "Calculator", "tsFullPath" : "Calculator" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift index 3478eb883..aa7af111f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/StaticFunctions.swift @@ -55,6 +55,17 @@ public func _bjs_Calculator_static_cube(_ value: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_Calculator_static_version_get") +@_cdecl("bjs_Calculator_static_version_get") +public func _bjs_Calculator_static_version_get() -> Void { + #if arch(wasm32) + let ret = Calculator.version + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension APIResult: _BridgedSwiftAssociatedValueEnum { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPopPayload(_ caseId: Int32) -> APIResult { switch caseId { @@ -156,6 +167,17 @@ public func _bjs_MathUtils_static_divide(_ a: Int32, _ b: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_MathUtils_static_pi_get") +@_cdecl("bjs_MathUtils_static_pi_get") +public func _bjs_MathUtils_static_pi_get() -> Float64 { + #if arch(wasm32) + let ret = MathUtils.pi + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_MathUtils_deinit") @_cdecl("bjs_MathUtils_deinit") public func _bjs_MathUtils_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json index 0245bf208..cf5156d8d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json @@ -80,6 +80,28 @@ } } + }, + { + "abiName" : "bjs_Greeter_static_greetAnonymously", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "greetAnonymously", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + }, + "staticContext" : { + "className" : { + "_0" : "Greeter" + } + } } ], "name" : "Greeter", @@ -91,6 +113,31 @@ "type" : { "string" : { + } + } + }, + { + "isReadonly" : true, + "isStatic" : false, + "name" : "nameCount", + "type" : { + "int" : { + + } + } + }, + { + "isReadonly" : true, + "isStatic" : true, + "name" : "defaultGreeting", + "staticContext" : { + "className" : { + "_0" : "Greeter" + } + }, + "type" : { + "string" : { + } } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift index 9f927da13..cb61e263d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift @@ -51,6 +51,17 @@ public func _bjs_Greeter_greetEnthusiastically(_ _self: UnsafeMutableRawPointer) #endif } +@_expose(wasm, "bjs_Greeter_static_greetAnonymously") +@_cdecl("bjs_Greeter_static_greetAnonymously") +public func _bjs_Greeter_static_greetAnonymously() -> Void { + #if arch(wasm32) + let ret = Greeter.greetAnonymously() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_name_get") @_cdecl("bjs_Greeter_name_get") public func _bjs_Greeter_name_get(_ _self: UnsafeMutableRawPointer) -> Void { @@ -72,6 +83,28 @@ public func _bjs_Greeter_name_set(_ _self: UnsafeMutableRawPointer, _ valueBytes #endif } +@_expose(wasm, "bjs_Greeter_nameCount_get") +@_cdecl("bjs_Greeter_nameCount_get") +public func _bjs_Greeter_nameCount_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).nameCount + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_static_defaultGreeting_get") +@_cdecl("bjs_Greeter_static_defaultGreeting_get") +public func _bjs_Greeter_static_defaultGreeting_get() -> Void { + #if arch(wasm32) + let ret = Greeter.defaultGreeting + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_deinit") @_cdecl("bjs_Greeter_deinit") public func _bjs_Greeter_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json index d216f10c6..7a6421668 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json @@ -227,6 +227,28 @@ } } + }, + { + "abiName" : "bjs_DataPoint_static_origin", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "origin", + "parameters" : [ + + ], + "returnType" : { + "swiftStruct" : { + "_0" : "DataPoint" + } + }, + "staticContext" : { + "structName" : { + "_0" : "DataPoint" + } + } } ], "name" : "DataPoint", @@ -290,6 +312,21 @@ "_1" : "null" } } + }, + { + "isReadonly" : true, + "isStatic" : true, + "name" : "dimensions", + "staticContext" : { + "structName" : { + "_0" : "DataPoint" + } + }, + "type" : { + "int" : { + + } + } } ], "swiftCallName" : "DataPoint" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift index 5e867f179..5e17b6db2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift @@ -66,6 +66,17 @@ public func _bjs_DataPoint_init(_ x: Float64, _ y: Float64, _ labelBytes: Int32, #endif } +@_expose(wasm, "bjs_DataPoint_static_dimensions_get") +@_cdecl("bjs_DataPoint_static_dimensions_get") +public func _bjs_DataPoint_static_dimensions_get() -> Int32 { + #if arch(wasm32) + let ret = DataPoint.dimensions + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_DataPoint_distanceFromOrigin") @_cdecl("bjs_DataPoint_distanceFromOrigin") public func _bjs_DataPoint_distanceFromOrigin() -> Float64 { @@ -77,6 +88,17 @@ public func _bjs_DataPoint_distanceFromOrigin() -> Float64 { #endif } +@_expose(wasm, "bjs_DataPoint_static_origin") +@_cdecl("bjs_DataPoint_static_origin") +public func _bjs_DataPoint_static_origin() -> Void { + #if arch(wasm32) + let ret = DataPoint.origin() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension Address: _BridgedSwiftStruct { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> Address { let zipCode = Optional.bridgeJSStackPop() diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.d.ts index a2f1c7d6d..5916e1648 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.d.ts @@ -22,6 +22,8 @@ export type APIResultTag = export type CalculatorObject = typeof CalculatorValues & { square(value: number): number; + cube(value: number): number; + readonly version: string; }; export type APIResultObject = typeof APIResultValues & { @@ -53,6 +55,8 @@ export type Exports = { new(): MathUtils; subtract(a: number, b: number): number; add(a: number, b: number): number; + divide(a: number, b: number): number; + readonly pi: number; } Calculator: CalculatorObject APIResult: APIResultObject diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js index 800b07107..df2a34ff1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js @@ -297,6 +297,14 @@ export async function createInstantiator(options, swift) { const ret = instance.exports.bjs_MathUtils_multiply(this.pointer, x, y); return ret; } + static divide(a, b) { + const ret = instance.exports.bjs_MathUtils_static_divide(a, b); + return ret; + } + static get pi() { + const ret = instance.exports.bjs_MathUtils_static_pi_get(); + return ret; + } } const APIResultHelpers = __bjs_createAPIResultValuesHelpers(); enumHelpers.APIResult = APIResultHelpers; @@ -314,6 +322,16 @@ export async function createInstantiator(options, swift) { square: function(value) { const ret = instance.exports.bjs_Calculator_static_square(value); return ret; + }, + cube: function(value) { + const ret = instance.exports.bjs_Calculator_static_cube(value); + return ret; + }, + get version() { + instance.exports.bjs_Calculator_static_version_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; } }, APIResult: { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.d.ts index e938ddb9a..c9cb26910 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.d.ts @@ -22,6 +22,8 @@ export type APIResultTag = export type CalculatorObject = typeof CalculatorValues & { square(value: number): number; + cube(value: number): number; + readonly version: string; }; export type APIResultObject = typeof APIResultValues & { @@ -43,6 +45,8 @@ export type Exports = { new(): MathUtils; subtract(a: number, b: number): number; add(a: number, b: number): number; + divide(a: number, b: number): number; + readonly pi: number; } Calculator: CalculatorObject APIResult: APIResultObject diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js index a4290f828..2e5b6e7f1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js @@ -297,6 +297,14 @@ export async function createInstantiator(options, swift) { const ret = instance.exports.bjs_MathUtils_multiply(this.pointer, x, y); return ret; } + static divide(a, b) { + const ret = instance.exports.bjs_MathUtils_static_divide(a, b); + return ret; + } + static get pi() { + const ret = instance.exports.bjs_MathUtils_static_pi_get(); + return ret; + } } const APIResultHelpers = __bjs_createAPIResultValuesHelpers(); enumHelpers.APIResult = APIResultHelpers; @@ -308,6 +316,16 @@ export async function createInstantiator(options, swift) { square: function(value) { const ret = instance.exports.bjs_Calculator_static_square(value); return ret; + }, + cube: function(value) { + const ret = instance.exports.bjs_Calculator_static_cube(value); + return ret; + }, + get version() { + instance.exports.bjs_Calculator_static_version_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; } }, APIResult: { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts index 72f816bd5..6d590950c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts @@ -16,6 +16,7 @@ export interface Greeter extends SwiftHeapObject { changeName(name: string): void; greetEnthusiastically(): string; name: string; + readonly nameCount: number; } export interface PublicGreeter extends SwiftHeapObject { } @@ -24,6 +25,8 @@ export interface PackageGreeter extends SwiftHeapObject { export type Exports = { Greeter: { new(name: string): Greeter; + greetAnonymously(): string; + readonly defaultGreeting: string; } PublicGreeter: { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js index 2596b539a..f6293f12a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js @@ -288,6 +288,12 @@ export async function createInstantiator(options, swift) { tmpRetString = undefined; return ret; } + static greetAnonymously() { + instance.exports.bjs_Greeter_static_greetAnonymously(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } get name() { instance.exports.bjs_Greeter_name_get(this.pointer); const ret = tmpRetString; @@ -299,6 +305,16 @@ export async function createInstantiator(options, swift) { const valueId = swift.memory.retain(valueBytes); instance.exports.bjs_Greeter_name_set(this.pointer, valueId, valueBytes.length); } + get nameCount() { + const ret = instance.exports.bjs_Greeter_nameCount_get(this.pointer); + return ret; + } + static get defaultGreeting() { + instance.exports.bjs_Greeter_static_defaultGreeting_get(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } } class PublicGreeter extends SwiftHeapObject { static __construct(ptr) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts index 4a61a26e3..211661d5f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts @@ -16,6 +16,7 @@ export interface DataPoint { label: string; optCount: number | null; optFlag: boolean | null; + distanceFromOrigin(): number; } export interface Address { street: string; @@ -65,6 +66,8 @@ export type Exports = { Precision: PrecisionObject DataPoint: { init(x: number, y: number, label: string, optCount: number | null, optFlag: boolean | null): DataPoint; + readonly dimensions: number; + origin(): DataPoint; } ConfigStruct: { readonly maxRetries: number; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js index cd2d396df..099f4ccc3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js @@ -73,7 +73,13 @@ export async function createInstantiator(options, swift) { const string = strStack.pop(); const f64 = f64Stack.pop(); const f641 = f64Stack.pop(); - return { x: f641, y: f64, label: string, optCount: optValue1, optFlag: optValue }; + const instance1 = { x: f641, y: f64, label: string, optCount: optValue1, optFlag: optValue }; + instance1.distanceFromOrigin = function() { + structHelpers.DataPoint.lower(this); + const ret = instance.exports.bjs_DataPoint_distanceFromOrigin(); + return ret; + }.bind(instance1); + return instance1; } }); const __bjs_createAddressHelpers = () => ({ @@ -546,6 +552,15 @@ export async function createInstantiator(options, swift) { const structValue = structHelpers.DataPoint.lift(); return structValue; }, + get dimensions() { + const ret = instance.exports.bjs_DataPoint_static_dimensions_get(); + return ret; + }, + origin: function() { + instance.exports.bjs_DataPoint_static_origin(); + const structValue = structHelpers.DataPoint.lift(); + return structValue; + }, }, ConfigStruct: { get maxRetries() { diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index c8ebbc8ce..6997cfd3b 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -150,6 +150,20 @@ struct TestError: Error { } } +extension Greeter { + @JS func greetEnthusiastically() -> String { + return "Hey, \(name)!!!" + } + + @JS var nameCount: Int { name.count } + + @JS static func greetAnonymously() -> String { + return "Hello." + } + + @JS static var defaultGreeting: String { "Hello, world!" } +} + @JS func takeGreeter(g: Greeter, name: String) { g.changeName(name: name) } @@ -250,6 +264,14 @@ struct TestError: Error { case auto = "auto" } +extension StaticCalculator { + @JS static func doubleValue(_ value: Int) -> Int { + return value * 2 + } + + @JS static var version: String { "1.0" } +} + @JS func setDirection(_ direction: Direction) -> Direction { return direction } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 13aab1455..cde1afc59 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -3418,6 +3418,28 @@ public func _bjs_StaticCalculator_static_roundtrip(_ value: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_StaticCalculator_static_doubleValue") +@_cdecl("bjs_StaticCalculator_static_doubleValue") +public func _bjs_StaticCalculator_static_doubleValue(_ value: Int32) -> Int32 { + #if arch(wasm32) + let ret = StaticCalculator.doubleValue(_: Int.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_StaticCalculator_static_version_get") +@_cdecl("bjs_StaticCalculator_static_version_get") +public func _bjs_StaticCalculator_static_version_get() -> Void { + #if arch(wasm32) + let ret = StaticCalculator.version + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_StaticUtils_Nested_static_roundtrip") @_cdecl("bjs_StaticUtils_Nested_static_roundtrip") public func _bjs_StaticUtils_Nested_static_roundtrip(_ valueBytes: Int32, _ valueLength: Int32) -> Void { @@ -4233,6 +4255,39 @@ public func _bjs_DataPoint_init(_ x: Float64, _ y: Float64, _ labelBytes: Int32, #endif } +@_expose(wasm, "bjs_DataPoint_static_dimensions_get") +@_cdecl("bjs_DataPoint_static_dimensions_get") +public func _bjs_DataPoint_static_dimensions_get() -> Int32 { + #if arch(wasm32) + let ret = DataPoint.dimensions + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_DataPoint_distanceFromOrigin") +@_cdecl("bjs_DataPoint_distanceFromOrigin") +public func _bjs_DataPoint_distanceFromOrigin() -> Float64 { + #if arch(wasm32) + let ret = DataPoint.bridgeJSLiftParameter().distanceFromOrigin() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_DataPoint_static_origin") +@_cdecl("bjs_DataPoint_static_origin") +public func _bjs_DataPoint_static_origin() -> Void { + #if arch(wasm32) + let ret = DataPoint.origin() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension PublicPoint: _BridgedSwiftStruct { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> PublicPoint { let y = Int.bridgeJSStackPop() @@ -6934,6 +6989,28 @@ public func _bjs_Greeter_makeCustomGreeter(_ _self: UnsafeMutableRawPointer) -> #endif } +@_expose(wasm, "bjs_Greeter_greetEnthusiastically") +@_cdecl("bjs_Greeter_greetEnthusiastically") +public func _bjs_Greeter_greetEnthusiastically(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).greetEnthusiastically() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_static_greetAnonymously") +@_cdecl("bjs_Greeter_static_greetAnonymously") +public func _bjs_Greeter_static_greetAnonymously() -> Void { + #if arch(wasm32) + let ret = Greeter.greetAnonymously() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_name_get") @_cdecl("bjs_Greeter_name_get") public func _bjs_Greeter_name_get(_ _self: UnsafeMutableRawPointer) -> Void { @@ -6966,6 +7043,28 @@ public func _bjs_Greeter_prefix_get(_ _self: UnsafeMutableRawPointer) -> Void { #endif } +@_expose(wasm, "bjs_Greeter_nameCount_get") +@_cdecl("bjs_Greeter_nameCount_get") +public func _bjs_Greeter_nameCount_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = Greeter.bridgeJSLiftParameter(_self).nameCount + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Greeter_static_defaultGreeting_get") +@_cdecl("bjs_Greeter_static_defaultGreeting_get") +public func _bjs_Greeter_static_defaultGreeting_get() -> Void { + #if arch(wasm32) + let ret = Greeter.defaultGreeting + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_deinit") @_cdecl("bjs_Greeter_deinit") public func _bjs_Greeter_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 746e0cd3c..137fb6087 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -705,6 +705,45 @@ "useJSTypedClosure" : false } } + }, + { + "abiName" : "bjs_Greeter_greetEnthusiastically", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "greetEnthusiastically", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_Greeter_static_greetAnonymously", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "greetAnonymously", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + }, + "staticContext" : { + "className" : { + "_0" : "Greeter" + } + } } ], "name" : "Greeter", @@ -726,6 +765,31 @@ "type" : { "string" : { + } + } + }, + { + "isReadonly" : true, + "isStatic" : false, + "name" : "nameCount", + "type" : { + "int" : { + + } + } + }, + { + "isReadonly" : true, + "isStatic" : true, + "name" : "defaultGreeting", + "staticContext" : { + "className" : { + "_0" : "Greeter" + } + }, + "type" : { + "string" : { + } } } @@ -7620,10 +7684,54 @@ "_0" : "StaticCalculator" } } + }, + { + "abiName" : "bjs_StaticCalculator_static_doubleValue", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "doubleValue", + "parameters" : [ + { + "label" : "_", + "name" : "value", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + }, + "staticContext" : { + "enumName" : { + "_0" : "StaticCalculator" + } + } } ], "staticProperties" : [ + { + "isReadonly" : true, + "isStatic" : true, + "name" : "version", + "staticContext" : { + "enumName" : { + "_0" : "StaticCalculator" + } + }, + "type" : { + "string" : { + } + } + } ], "swiftCallName" : "StaticCalculator", "tsFullPath" : "StaticCalculator" @@ -13342,7 +13450,45 @@ ] }, "methods" : [ + { + "abiName" : "bjs_DataPoint_distanceFromOrigin", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "distanceFromOrigin", + "parameters" : [ + + ], + "returnType" : { + "double" : { + } + } + }, + { + "abiName" : "bjs_DataPoint_static_origin", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "origin", + "parameters" : [ + + ], + "returnType" : { + "swiftStruct" : { + "_0" : "DataPoint" + } + }, + "staticContext" : { + "structName" : { + "_0" : "DataPoint" + } + } + } ], "name" : "DataPoint", "properties" : [ @@ -13405,6 +13551,21 @@ "_1" : "null" } } + }, + { + "isReadonly" : true, + "isStatic" : true, + "name" : "dimensions", + "staticContext" : { + "structName" : { + "_0" : "DataPoint" + } + }, + "type" : { + "int" : { + + } + } } ], "swiftCallName" : "DataPoint" diff --git a/Tests/BridgeJSRuntimeTests/StructAPIs.swift b/Tests/BridgeJSRuntimeTests/StructAPIs.swift index 0a05a517d..af925cb77 100644 --- a/Tests/BridgeJSRuntimeTests/StructAPIs.swift +++ b/Tests/BridgeJSRuntimeTests/StructAPIs.swift @@ -174,6 +174,18 @@ import JavaScriptKit } } +extension DataPoint { + @JS func distanceFromOrigin() -> Double { + return (x * x + y * y).squareRoot() + } + + @JS static func origin() -> DataPoint { + return DataPoint(x: 0, y: 0, label: "origin", optCount: nil, optFlag: nil) + } + + @JS static var dimensions: Int { 2 } +} + @JS func roundTripDataPoint(_ data: DataPoint) -> DataPoint { return data } diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 4c211e91c..94d33057d 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -269,6 +269,12 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(g.name, "Bob"); assert.equal(g.greet(), "Hello, Bob!"); + // Test class extension members + assert.equal(g.greetEnthusiastically(), "Hey, Bob!!!"); + assert.equal(g.nameCount, 3); + assert.equal(exports.Greeter.greetAnonymously(), "Hello."); + assert.equal(exports.Greeter.defaultGreeting, "Hello, world!"); + const g2 = exports.roundTripSwiftHeapObject(g) assert.equal(g2.greet(), "Hello, Bob!"); assert.equal(g2.name, "Bob"); @@ -761,6 +767,10 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(StaticCalculatorValues.Basic, 1); assert.equal(StaticCalculatorValues.Scientific, exports.StaticCalculator.Scientific); assert.equal(StaticCalculatorValues.Basic, exports.StaticCalculator.Basic); + + // Test enum extension static members + assert.equal(exports.StaticCalculator.doubleValue(21), 42); + assert.equal(exports.StaticCalculator.version, "1.0"); assert.equal(exports.StaticUtils.Nested.roundtrip("hello world"), "hello world"); assert.equal(exports.StaticUtils.Nested.roundtrip("test"), "test"); @@ -779,6 +789,15 @@ function testStructSupport(exports) { const data2 = { x: 0.0, y: 0.0, label: "", optCount: null, optFlag: null }; assert.deepEqual(exports.roundTripDataPoint(data2), data2); + // Test struct extension members + const data3 = { x: 3.0, y: 4.0, label: "Test", optCount: null, optFlag: null }; + assert.equal(exports.DataPoint.distanceFromOrigin(data3), 5.0); + const origin = exports.DataPoint.origin(); + assert.equal(origin.x, 0.0); + assert.equal(origin.y, 0.0); + assert.equal(origin.label, "origin"); + assert.equal(exports.DataPoint.dimensions, 2); + const publicPoint = { x: 9, y: -3 }; assert.deepEqual(exports.roundTripPublicPoint(publicPoint), publicPoint); From 825f6950b2ed32de29764f131c306ec75e82f008 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 11 Mar 2026 10:58:14 +1100 Subject: [PATCH 3/5] Fix formatting --- .../Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift | 1 - Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift index 1b1f95f76..dd0ce5d03 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift @@ -184,7 +184,6 @@ import Testing try snapshotCodegen(skeleton: skeleton, name: "CrossFileExtension") } - @Test func codegenSkipsEmptySkeletons() throws { let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false) diff --git a/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift b/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift index db093e549..e3c19a8e4 100644 --- a/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift @@ -72,7 +72,7 @@ class JSClosureAsyncTests: XCTestCase { )!.value() XCTAssertEqual(result, 42.0) } - + func testAsyncOneshotClosureWithPriority() async throws { let priority = UnsafeSendableBox(nil) let closure = JSOneshotClosure.async(priority: .high) { _ in @@ -83,7 +83,7 @@ class JSClosureAsyncTests: XCTestCase { XCTAssertEqual(result, 42.0) XCTAssertEqual(priority.value, .high) } - + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func testAsyncOneshotClosureWithTaskExecutor() async throws { let executor = AnyTaskExecutor() @@ -93,7 +93,7 @@ class JSClosureAsyncTests: XCTestCase { let result = try await JSPromise(from: closure.function!())!.value() XCTAssertEqual(result, 42.0) } - + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func testAsyncOneshotClosureWithTaskExecutorPreference() async throws { let executor = AnyTaskExecutor() From a93a6869918f5610547caf6d924555f5f17dc32f Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 11 Mar 2026 11:53:04 +1100 Subject: [PATCH 4/5] Update test code to avoid accidentally introduced failure --- .../Inputs/MacroSwift/SwiftStruct.swift | 17 ++- .../BridgeJSCodegenTests/SwiftStruct.json | 87 ++++++++++--- .../BridgeJSCodegenTests/SwiftStruct.swift | 81 ++++++++++-- .../Generated/BridgeJS.swift | 92 ++++++++++++-- .../Generated/JavaScript/BridgeJS.json | 115 +++++++++++++++--- Tests/BridgeJSRuntimeTests/StructAPIs.swift | 24 +++- Tests/prelude.mjs | 11 +- 7 files changed, 361 insertions(+), 66 deletions(-) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift index 4ec6525e3..63bb0ff8d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftStruct.swift @@ -61,11 +61,22 @@ @JS func roundtripContainer(_ container: Container) -> Container -extension DataPoint { - @JS func distanceFromOrigin() -> Double { - return (x * x + y * y).squareRoot() +@JS struct Vector2D { + var dx: Double + var dy: Double +} + +extension Vector2D { + @JS func magnitude() -> Double { + return (dx * dx + dy * dy).squareRoot() } + @JS func scaled(by factor: Double) -> Vector2D { + return Vector2D(dx: dx * factor, dy: dy * factor) + } +} + +extension DataPoint { @JS static func origin() -> DataPoint { return DataPoint(x: 0, y: 0, label: "origin", optCount: nil, optFlag: nil) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json index 7a6421668..617124701 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.json @@ -211,23 +211,6 @@ ] }, "methods" : [ - { - "abiName" : "bjs_DataPoint_distanceFromOrigin", - "effects" : { - "isAsync" : false, - "isStatic" : false, - "isThrows" : false - }, - "name" : "distanceFromOrigin", - "parameters" : [ - - ], - "returnType" : { - "double" : { - - } - } - }, { "abiName" : "bjs_DataPoint_static_origin", "effects" : { @@ -635,6 +618,76 @@ } ], "swiftCallName" : "Container" + }, + { + "methods" : [ + { + "abiName" : "bjs_Vector2D_magnitude", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "magnitude", + "parameters" : [ + + ], + "returnType" : { + "double" : { + + } + } + }, + { + "abiName" : "bjs_Vector2D_scaled", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "scaled", + "parameters" : [ + { + "label" : "by", + "name" : "factor", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + } + } + ], + "name" : "Vector2D", + "properties" : [ + { + "isReadonly" : true, + "isStatic" : false, + "name" : "dx", + "type" : { + "double" : { + + } + } + }, + { + "isReadonly" : true, + "isStatic" : false, + "name" : "dy", + "type" : { + "double" : { + + } + } + } + ], + "swiftCallName" : "Vector2D" } ] }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift index 5e17b6db2..e4cd409ae 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStruct.swift @@ -77,17 +77,6 @@ public func _bjs_DataPoint_static_dimensions_get() -> Int32 { #endif } -@_expose(wasm, "bjs_DataPoint_distanceFromOrigin") -@_cdecl("bjs_DataPoint_distanceFromOrigin") -public func _bjs_DataPoint_distanceFromOrigin() -> Float64 { - #if arch(wasm32) - let ret = DataPoint.bridgeJSLiftParameter().distanceFromOrigin() - return ret.bridgeJSLowerReturn() - #else - fatalError("Only available on WebAssembly") - #endif -} - @_expose(wasm, "bjs_DataPoint_static_origin") @_cdecl("bjs_DataPoint_static_origin") public func _bjs_DataPoint_static_origin() -> Void { @@ -466,6 +455,76 @@ fileprivate func _bjs_struct_lift_Container_extern() -> Int32 { return _bjs_struct_lift_Container_extern() } +extension Vector2D: _BridgedSwiftStruct { + @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> Vector2D { + let dy = Double.bridgeJSStackPop() + let dx = Double.bridgeJSStackPop() + return Vector2D(dx: dx, dy: dy) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSStackPush() { + self.dx.bridgeJSStackPush() + self.dy.bridgeJSStackPush() + } + + init(unsafelyCopying jsObject: JSObject) { + _bjs_struct_lower_Vector2D(jsObject.bridgeJSLowerParameter()) + self = Self.bridgeJSStackPop() + } + + func toJSObject() -> JSObject { + let __bjs_self = self + __bjs_self.bridgeJSStackPush() + return JSObject(id: UInt32(bitPattern: _bjs_struct_lift_Vector2D())) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Vector2D") +fileprivate func _bjs_struct_lower_Vector2D_extern(_ objectId: Int32) -> Void +#else +fileprivate func _bjs_struct_lower_Vector2D_extern(_ objectId: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lower_Vector2D(_ objectId: Int32) -> Void { + return _bjs_struct_lower_Vector2D_extern(objectId) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lift_Vector2D") +fileprivate func _bjs_struct_lift_Vector2D_extern() -> Int32 +#else +fileprivate func _bjs_struct_lift_Vector2D_extern() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lift_Vector2D() -> Int32 { + return _bjs_struct_lift_Vector2D_extern() +} + +@_expose(wasm, "bjs_Vector2D_magnitude") +@_cdecl("bjs_Vector2D_magnitude") +public func _bjs_Vector2D_magnitude() -> Float64 { + #if arch(wasm32) + let ret = Vector2D.bridgeJSLiftParameter().magnitude() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Vector2D_scaled") +@_cdecl("bjs_Vector2D_scaled") +public func _bjs_Vector2D_scaled(_ factor: Float64) -> Void { + #if arch(wasm32) + let ret = Vector2D.bridgeJSLiftParameter().scaled(by: Double.bridgeJSLiftParameter(factor)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_roundtrip") @_cdecl("bjs_roundtrip") public func _bjs_roundtrip() -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index cde1afc59..c85dec679 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -4266,17 +4266,6 @@ public func _bjs_DataPoint_static_dimensions_get() -> Int32 { #endif } -@_expose(wasm, "bjs_DataPoint_distanceFromOrigin") -@_cdecl("bjs_DataPoint_distanceFromOrigin") -public func _bjs_DataPoint_distanceFromOrigin() -> Float64 { - #if arch(wasm32) - let ret = DataPoint.bridgeJSLiftParameter().distanceFromOrigin() - return ret.bridgeJSLowerReturn() - #else - fatalError("Only available on WebAssembly") - #endif -} - @_expose(wasm, "bjs_DataPoint_static_origin") @_cdecl("bjs_DataPoint_static_origin") public func _bjs_DataPoint_static_origin() -> Void { @@ -5089,6 +5078,87 @@ public func _bjs_ConfigStruct_static_computedSetting_get() -> Void { #endif } +extension Vector2D: _BridgedSwiftStruct { + @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> Vector2D { + let dy = Double.bridgeJSStackPop() + let dx = Double.bridgeJSStackPop() + return Vector2D(dx: dx, dy: dy) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSStackPush() { + self.dx.bridgeJSStackPush() + self.dy.bridgeJSStackPush() + } + + init(unsafelyCopying jsObject: JSObject) { + _bjs_struct_lower_Vector2D(jsObject.bridgeJSLowerParameter()) + self = Self.bridgeJSStackPop() + } + + func toJSObject() -> JSObject { + let __bjs_self = self + __bjs_self.bridgeJSStackPush() + return JSObject(id: UInt32(bitPattern: _bjs_struct_lift_Vector2D())) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Vector2D") +fileprivate func _bjs_struct_lower_Vector2D_extern(_ objectId: Int32) -> Void +#else +fileprivate func _bjs_struct_lower_Vector2D_extern(_ objectId: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lower_Vector2D(_ objectId: Int32) -> Void { + return _bjs_struct_lower_Vector2D_extern(objectId) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lift_Vector2D") +fileprivate func _bjs_struct_lift_Vector2D_extern() -> Int32 +#else +fileprivate func _bjs_struct_lift_Vector2D_extern() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lift_Vector2D() -> Int32 { + return _bjs_struct_lift_Vector2D_extern() +} + +@_expose(wasm, "bjs_Vector2D_init") +@_cdecl("bjs_Vector2D_init") +public func _bjs_Vector2D_init(_ dx: Float64, _ dy: Float64) -> Void { + #if arch(wasm32) + let ret = Vector2D(dx: Double.bridgeJSLiftParameter(dx), dy: Double.bridgeJSLiftParameter(dy)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Vector2D_magnitude") +@_cdecl("bjs_Vector2D_magnitude") +public func _bjs_Vector2D_magnitude() -> Float64 { + #if arch(wasm32) + let ret = Vector2D.bridgeJSLiftParameter().magnitude() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Vector2D_scaled") +@_cdecl("bjs_Vector2D_scaled") +public func _bjs_Vector2D_scaled(_ factor: Float64) -> Void { + #if arch(wasm32) + let ret = Vector2D.bridgeJSLiftParameter().scaled(by: Double.bridgeJSLiftParameter(factor)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension JSObjectContainer: _BridgedSwiftStruct { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> JSObjectContainer { let optionalObject = Optional.bridgeJSStackPop() diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 137fb6087..14cdc5dee 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -13450,23 +13450,6 @@ ] }, "methods" : [ - { - "abiName" : "bjs_DataPoint_distanceFromOrigin", - "effects" : { - "isAsync" : false, - "isStatic" : false, - "isThrows" : false - }, - "name" : "distanceFromOrigin", - "parameters" : [ - - ], - "returnType" : { - "double" : { - - } - } - }, { "abiName" : "bjs_DataPoint_static_origin", "effects" : { @@ -14500,6 +14483,104 @@ ], "swiftCallName" : "ConfigStruct" }, + { + "constructor" : { + "abiName" : "bjs_Vector2D_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "dx", + "name" : "dx", + "type" : { + "double" : { + + } + } + }, + { + "label" : "dy", + "name" : "dy", + "type" : { + "double" : { + + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_Vector2D_magnitude", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "magnitude", + "parameters" : [ + + ], + "returnType" : { + "double" : { + + } + } + }, + { + "abiName" : "bjs_Vector2D_scaled", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "scaled", + "parameters" : [ + { + "label" : "by", + "name" : "factor", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + } + } + ], + "name" : "Vector2D", + "properties" : [ + { + "isReadonly" : true, + "isStatic" : false, + "name" : "dx", + "type" : { + "double" : { + + } + } + }, + { + "isReadonly" : true, + "isStatic" : false, + "name" : "dy", + "type" : { + "double" : { + + } + } + } + ], + "swiftCallName" : "Vector2D" + }, { "methods" : [ diff --git a/Tests/BridgeJSRuntimeTests/StructAPIs.swift b/Tests/BridgeJSRuntimeTests/StructAPIs.swift index af925cb77..daa7ad1e2 100644 --- a/Tests/BridgeJSRuntimeTests/StructAPIs.swift +++ b/Tests/BridgeJSRuntimeTests/StructAPIs.swift @@ -175,10 +175,6 @@ import JavaScriptKit } extension DataPoint { - @JS func distanceFromOrigin() -> Double { - return (x * x + y * y).squareRoot() - } - @JS static func origin() -> DataPoint { return DataPoint(x: 0, y: 0, label: "origin", optCount: nil, optFlag: nil) } @@ -186,6 +182,26 @@ extension DataPoint { @JS static var dimensions: Int { 2 } } +@JS struct Vector2D { + var dx: Double + var dy: Double + + @JS init(dx: Double, dy: Double) { + self.dx = dx + self.dy = dy + } +} + +extension Vector2D { + @JS func magnitude() -> Double { + return (dx * dx + dy * dy).squareRoot() + } + + @JS func scaled(by factor: Double) -> Vector2D { + return Vector2D(dx: dx * factor, dy: dy * factor) + } +} + @JS func roundTripDataPoint(_ data: DataPoint) -> DataPoint { return data } diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 94d33057d..c5da33196 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -789,15 +789,20 @@ function testStructSupport(exports) { const data2 = { x: 0.0, y: 0.0, label: "", optCount: null, optFlag: null }; assert.deepEqual(exports.roundTripDataPoint(data2), data2); - // Test struct extension members - const data3 = { x: 3.0, y: 4.0, label: "Test", optCount: null, optFlag: null }; - assert.equal(exports.DataPoint.distanceFromOrigin(data3), 5.0); + // Test struct extension static members const origin = exports.DataPoint.origin(); assert.equal(origin.x, 0.0); assert.equal(origin.y, 0.0); assert.equal(origin.label, "origin"); assert.equal(exports.DataPoint.dimensions, 2); + // Test struct extension instance methods + const vec = new exports.Vector2D(3.0, 4.0); + assert.equal(vec.magnitude(), 5.0); + const scaled = vec.scaled(2.0); + assert.equal(scaled.dx, 6.0); + assert.equal(scaled.dy, 8.0); + const publicPoint = { x: 9, y: -3 }; assert.deepEqual(exports.roundTripPublicPoint(publicPoint), publicPoint); From 4c1c32caa352f512142ef62b1015d506acf726fb Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Wed, 11 Mar 2026 19:52:52 +0800 Subject: [PATCH 5/5] Fix CI: update snapshots, formatting, runtime test, add docs and review feedback --- .../BridgeJSCore/SwiftToSkeleton.swift | 14 +++++- .../BridgeJSLinkTests/SwiftStruct.d.ts | 7 ++- .../BridgeJSLinkTests/SwiftStruct.js | 41 +++++++++++++--- .../Exporting-Swift/Exporting-Swift-Class.md | 48 +++++++++++++++++++ .../Exporting-Swift/Exporting-Swift-Enum.md | 1 + .../Exporting-Swift/Exporting-Swift-Struct.md | 1 + .../JSClosure+AsyncTests.swift | 6 +-- Tests/prelude.mjs | 2 +- 8 files changed, 107 insertions(+), 13 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index 2ff5cecba..a28192d4b 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1425,7 +1425,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { } } - /// Walks extension members under the matching type’s state, returning whether the type was found + /// Walks extension members under the matching type’s state, returning whether the type was found. + /// + /// Note: The lookup scans dictionaries keyed by `makeKey(name:namespace:)`, matching only by + /// plain name. If two types share a name but differ by namespace, `.first(where:)` picks + /// whichever comes first. This is acceptable today since namespace collisions are unlikely, + /// but may need refinement if namespace-qualified extension resolution is added. func resolveExtension(_ ext: ExtensionDeclSyntax) -> Bool { let name = ext.extendedType.trimmedDescription let state: State @@ -1435,6 +1440,13 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { state = .structBody(name: name, key: entry.key) } else if let entry = exportedEnumByName.first(where: { $0.value.name == name }) { state = .enumBody(name: name, key: entry.key) + } else if exportedProtocolByName.values.contains(where: { $0.name == name }) { + diagnose( + node: ext.extendedType, + message: "Protocol extensions are not supported by BridgeJS.", + hint: "You cannot extend `@JS` protocol '\(name)' with additional members" + ) + return true } else { return false } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts index 211661d5f..bf4ebc71f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.d.ts @@ -16,7 +16,6 @@ export interface DataPoint { label: string; optCount: number | null; optFlag: boolean | null; - distanceFromOrigin(): number; } export interface Address { street: string; @@ -44,6 +43,12 @@ export interface Container { object: any; optionalObject: any | null; } +export interface Vector2D { + dx: number; + dy: number; + magnitude(): number; + scaled(factor: number): Vector2D; +} export type PrecisionObject = typeof PrecisionValues; /// Represents a Swift heap object like a class instance or an actor instance. diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js index 099f4ccc3..b9889f156 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js @@ -73,13 +73,7 @@ export async function createInstantiator(options, swift) { const string = strStack.pop(); const f64 = f64Stack.pop(); const f641 = f64Stack.pop(); - const instance1 = { x: f641, y: f64, label: string, optCount: optValue1, optFlag: optValue }; - instance1.distanceFromOrigin = function() { - structHelpers.DataPoint.lower(this); - const ret = instance.exports.bjs_DataPoint_distanceFromOrigin(); - return ret; - }.bind(instance1); - return instance1; + return { x: f641, y: f64, label: string, optCount: optValue1, optFlag: optValue }; } }); const __bjs_createAddressHelpers = () => ({ @@ -225,6 +219,29 @@ export async function createInstantiator(options, swift) { return { object: value, optionalObject: optValue }; } }); + const __bjs_createVector2DHelpers = () => ({ + lower: (value) => { + f64Stack.push(value.dx); + f64Stack.push(value.dy); + }, + lift: () => { + const f64 = f64Stack.pop(); + const f641 = f64Stack.pop(); + const instance1 = { dx: f641, dy: f64 }; + instance1.magnitude = function() { + structHelpers.Vector2D.lower(this); + const ret = instance.exports.bjs_Vector2D_magnitude(); + return ret; + }.bind(instance1); + instance1.scaled = function(factor) { + structHelpers.Vector2D.lower(this); + const ret = instance.exports.bjs_Vector2D_scaled(factor); + const structValue = structHelpers.Vector2D.lift(); + return structValue; + }.bind(instance1); + return instance1; + } + }); return { /** @@ -336,6 +353,13 @@ export async function createInstantiator(options, swift) { const value = structHelpers.Container.lift(); return swift.memory.retain(value); } + bjs["swift_js_struct_lower_Vector2D"] = function(objectId) { + structHelpers.Vector2D.lower(swift.memory.getObject(objectId)); + } + bjs["swift_js_struct_lift_Vector2D"] = function() { + const value = structHelpers.Vector2D.lift(); + return swift.memory.retain(value); + } bjs["swift_js_return_optional_bool"] = function(isSome, value) { if (isSome === 0) { tmpRetOptionalBool = null; @@ -527,6 +551,9 @@ export async function createInstantiator(options, swift) { const ContainerHelpers = __bjs_createContainerHelpers(); structHelpers.Container = ContainerHelpers; + const Vector2DHelpers = __bjs_createVector2DHelpers(); + structHelpers.Vector2D = Vector2DHelpers; + const exports = { Greeter, roundtrip: function bjs_roundtrip(session) { diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md index 9cd4a2224..a16c81286 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md @@ -77,6 +77,53 @@ export type Exports = { } ``` +## Adding Members via Extensions + +You can add exported methods, computed properties, and static members to a `@JS` class using extensions. The extension block itself does not need `@JS` - only the individual members do: + +```swift +@JS class Greeter { + @JS var name: String + + @JS init(name: String) { + self.name = name + } + + @JS func greet() -> String { + return "Hello, " + self.name + "!" + } +} + +extension Greeter { + @JS func greetEnthusiastically() -> String { + return "Hey, " + self.name + "!!!" + } + + @JS var nameCount: Int { name.count } + + @JS static func greetAnonymously() -> String { + return "Hello." + } + + @JS static var defaultGreeting: String { "Hello, world!" } +} +``` + +This also works across files within the same module: + +```swift +// GreeterExtension.swift +extension Greeter { + @JS func greetFormally() -> String { + return "Good day, " + self.name + "." + } +} +``` + +All `@JS`-annotated members in extensions are merged into the same generated TypeScript interface as the original class declaration. + +> Note: Extensions must target `@JS`-annotated types from the same module. + ## How It Works Classes use **reference semantics** when crossing the Swift/JavaScript boundary: @@ -103,5 +150,6 @@ This differs from structs, which use copy semantics and transfer data by value. | Static / class properties: `static var`, `class let` | ✅ (See )| | Methods: `func` | ✅ (See ) | | Static/class methods: `static func`, `class func` | ✅ (See ) | +| Extension methods/properties | ✅ | | Subscripts: `subscript()` | ❌ | | Generics | ❌ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md index 2220d457c..68996b27b 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md @@ -514,4 +514,5 @@ This differs from classes, which use reference semantics and share state across | Associated values: `JSObject` | ✅ | | Associated values: Arrays | ✅ | | Associated values: Optionals of all supported types | ✅ | +| Extension static functions/properties | ✅ | | Generics | ❌ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md index 32bb79ed3..c4a9524d9 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md @@ -165,6 +165,7 @@ This differs from classes, which use reference semantics and share state across | Instance methods | ✅ | | Static methods | ✅ | | Static properties | ✅ | +| Extension methods/properties | ✅ | | Property observers (`willSet`, `didSet`) | ❌ | | Generics | ❌ | | Conformances | ❌ | diff --git a/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift b/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift index e3c19a8e4..db093e549 100644 --- a/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift @@ -72,7 +72,7 @@ class JSClosureAsyncTests: XCTestCase { )!.value() XCTAssertEqual(result, 42.0) } - + func testAsyncOneshotClosureWithPriority() async throws { let priority = UnsafeSendableBox(nil) let closure = JSOneshotClosure.async(priority: .high) { _ in @@ -83,7 +83,7 @@ class JSClosureAsyncTests: XCTestCase { XCTAssertEqual(result, 42.0) XCTAssertEqual(priority.value, .high) } - + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func testAsyncOneshotClosureWithTaskExecutor() async throws { let executor = AnyTaskExecutor() @@ -93,7 +93,7 @@ class JSClosureAsyncTests: XCTestCase { let result = try await JSPromise(from: closure.function!())!.value() XCTAssertEqual(result, 42.0) } - + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func testAsyncOneshotClosureWithTaskExecutorPreference() async throws { let executor = AnyTaskExecutor() diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index c5da33196..bed4ac02f 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -797,7 +797,7 @@ function testStructSupport(exports) { assert.equal(exports.DataPoint.dimensions, 2); // Test struct extension instance methods - const vec = new exports.Vector2D(3.0, 4.0); + const vec = exports.Vector2D.init(3.0, 4.0); assert.equal(vec.magnitude(), 5.0); const scaled = vec.scaled(2.0); assert.equal(scaled.dx, 6.0);