From 451c24867788e63d457760d28d217e0a43c689fe Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Wed, 1 Oct 2025 10:22:42 +0200 Subject: [PATCH 1/2] BridgeJS: WIP default values --- .../Sources/BridgeJSCore/ExportSwift.swift | 272 +++++++++- .../Sources/BridgeJSLink/BridgeJSLink.swift | 118 ++++- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 20 +- .../Inputs/DefaultParameters.swift | 41 ++ .../DefaultParameters.Export.d.ts | 91 ++++ .../DefaultParameters.Export.js | 347 +++++++++++++ .../ExportSwiftTests/DefaultParameters.json | 482 ++++++++++++++++++ .../ExportSwiftTests/DefaultParameters.swift | 250 +++++++++ .../BridgeJS/Exporting-Swift-to-JavaScript.md | 1 + .../Exporting-Swift-Default-Parameters.md | 136 +++++ .../Exporting-Swift-Function.md | 2 +- .../BridgeJSRuntimeTests/ExportAPITests.swift | 46 ++ .../Generated/BridgeJS.ExportSwift.swift | 110 ++++ .../JavaScript/BridgeJS.ExportSwift.json | 348 +++++++++++++ Tests/prelude.mjs | 41 ++ 15 files changed, 2295 insertions(+), 10 deletions(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 6679c4a8..71e72460 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -152,6 +152,255 @@ public class ExportSwift { ) } + /// Detects whether given expression is supported as default parameter value + private func isSupportedDefaultValueExpression(_ initClause: InitializerClauseSyntax) -> Bool { + let expression = initClause.value + + // Function calls are checked later in extractDefaultValue (as constructors are allowed) + if expression.is(ArrayExprSyntax.self) { return false } + if expression.is(DictionaryExprSyntax.self) { return false } + if expression.is(BinaryOperatorExprSyntax.self) { return false } + if expression.is(ClosureExprSyntax.self) { return false } + + // Method call chains (e.g., obj.foo()) + if let memberExpression = expression.as(MemberAccessExprSyntax.self), + memberExpression.base?.is(FunctionCallExprSyntax.self) == true + { + return false + } + + return true + } + + /// Extract enum case value from member access expression + private func extractEnumCaseValue( + from memberExpr: MemberAccessExprSyntax, + type: BridgeType + ) -> DefaultValue? { + let caseName = memberExpr.declName.baseName.text + + let enumName: String? + switch type { + case .caseEnum(let name), .rawValueEnum(let name, _), .associatedValueEnum(let name): + enumName = name + case .optional(let wrappedType): + switch wrappedType { + case .caseEnum(let name), .rawValueEnum(let name, _), .associatedValueEnum(let name): + enumName = name + default: + return nil + } + default: + return nil + } + + guard let enumName = enumName else { return nil } + + if memberExpr.base == nil { + return .enumCase(enumName, caseName) + } + + if let baseExpr = memberExpr.base?.as(DeclReferenceExprSyntax.self) { + let baseName = baseExpr.baseName.text + let lastComponent = enumName.split(separator: ".").last.map(String.init) ?? enumName + if baseName == enumName || baseName == lastComponent { + return .enumCase(enumName, caseName) + } + } + + return nil + } + + /// Extracts default value from parameter's default value clause + private func extractDefaultValue( + from defaultClause: InitializerClauseSyntax?, + type: BridgeType + ) -> DefaultValue? { + guard let defaultClause = defaultClause else { + return nil + } + + if !isSupportedDefaultValueExpression(defaultClause) { + diagnose( + node: defaultClause, + message: "Complex default parameter expressions are not supported", + hint: "Use simple literal values (e.g., \"text\", 42, true, nil) or simple constants" + ) + return nil + } + + let expr = defaultClause.value + + if expr.is(NilLiteralExprSyntax.self) { + guard case .optional(_) = type else { + diagnose( + node: expr, + message: "nil is only valid for optional parameters", + hint: "Make the parameter optional by adding ? to the type" + ) + return nil + } + return .null + } + + if let stringLiteral = expr.as(StringLiteralExprSyntax.self), + let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self), + type.matches(against: .string) + { + return .string(segment.content.text) + } + + if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self), + type.matches(against: .bool) + { + return .bool(boolLiteral.literal.text == "true") + } + + if let intLiteral = expr.as(IntegerLiteralExprSyntax.self), + let intValue = Int(intLiteral.literal.text), + type.matches(against: .int) + { + return .int(intValue) + } + + if let floatLiteral = expr.as(FloatLiteralExprSyntax.self) { + if type.matches(against: .float), + let floatValue = Float(floatLiteral.literal.text) + { + return .float(floatValue) + } + if type.matches(against: .double), + let doubleValue = Double(floatLiteral.literal.text) + { + return .double(doubleValue) + } + } + + if let memberExpr = expr.as(MemberAccessExprSyntax.self), + let enumValue = extractEnumCaseValue(from: memberExpr, type: type) + { + return enumValue + } + + // Constructor calls (e.g., Greeter(name: "John")) + if let funcCall = expr.as(FunctionCallExprSyntax.self) { + return extractConstructorDefaultValue(from: funcCall, type: type) + } + + diagnose( + node: expr, + message: "Unsupported default parameter value expression", + hint: "Use simple literal values like \"text\", 42, true, false, nil, or enum cases like .caseName" + ) + return nil + } + + /// Extracts default value from a constructor call expression + private func extractConstructorDefaultValue( + from funcCall: FunctionCallExprSyntax, + type: BridgeType + ) -> DefaultValue? { + // Extract class name + guard let calledExpr = funcCall.calledExpression.as(DeclReferenceExprSyntax.self) else { + diagnose( + node: funcCall, + message: "Complex constructor expressions are not supported", + hint: "Use a simple constructor call like ClassName() or ClassName(arg: value)" + ) + return nil + } + + let className = calledExpr.baseName.text + + // Verify type matches + let expectedClassName: String? + switch type { + case .swiftHeapObject(let name): + expectedClassName = name.split(separator: ".").last.map(String.init) + case .optional(.swiftHeapObject(let name)): + expectedClassName = name.split(separator: ".").last.map(String.init) + default: + diagnose( + node: funcCall, + message: "Constructor calls are only supported for class types", + hint: "Parameter type should be a Swift class" + ) + return nil + } + + guard let expectedClassName = expectedClassName, className == expectedClassName else { + diagnose( + node: funcCall, + message: "Constructor class name '\(className)' doesn't match parameter type", + hint: "Ensure the constructor matches the parameter type" + ) + return nil + } + + // Handle parameterless constructor + if funcCall.arguments.isEmpty { + return .object(className) + } + + // Extract arguments for constructor with parameters + var constructorArgs: [DefaultValue] = [] + for argument in funcCall.arguments { + // Recursively extract the argument's default value + // For now, only support literals in constructor arguments + guard let argValue = extractConstructorArgumentValue(from: argument.expression) else { + diagnose( + node: argument.expression, + message: "Constructor argument must be a literal value", + hint: "Use simple literals like \"text\", 42, true, false in constructor arguments" + ) + return nil + } + + constructorArgs.append(argValue) + } + + return .objectWithArguments(className, constructorArgs) + } + + /// Extracts a literal value from an expression for use in constructor arguments + private func extractConstructorArgumentValue(from expr: ExprSyntax) -> DefaultValue? { + // String literals + if let stringLiteral = expr.as(StringLiteralExprSyntax.self), + let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self) + { + return .string(segment.content.text) + } + + // Boolean literals + if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self) { + return .bool(boolLiteral.literal.text == "true") + } + + // Integer literals + if let intLiteral = expr.as(IntegerLiteralExprSyntax.self), + let intValue = Int(intLiteral.literal.text) + { + return .int(intValue) + } + + // Float literals + if let floatLiteral = expr.as(FloatLiteralExprSyntax.self) { + if let floatValue = Float(floatLiteral.literal.text) { + return .float(floatValue) + } + if let doubleValue = Double(floatLiteral.literal.text) { + return .double(doubleValue) + } + } + + // nil literal + if expr.is(NilLiteralExprSyntax.self) { + return .null + } + + return nil + } + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { return .skipChildren @@ -252,7 +501,10 @@ public class ExportSwift { let name = param.secondName?.text ?? param.firstName.text let label = param.firstName.text - parameters.append(Parameter(label: label, name: name, type: type)) + + let defaultValue = extractDefaultValue(from: param.defaultValue, type: type) + + parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue)) } let returnType: BridgeType if let returnClause = node.signature.returnClause { @@ -409,7 +661,10 @@ public class ExportSwift { } let name = param.secondName?.text ?? param.firstName.text let label = param.firstName.text - parameters.append(Parameter(label: label, name: name, type: type)) + + let defaultValue = extractDefaultValue(from: param.defaultValue, type: type) + + parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue)) } guard let effects = collectEffects(signature: node.signature) else { @@ -1903,3 +2158,16 @@ extension WithModifiersSyntax { } } } + +fileprivate extension BridgeType { + func matches(against expected: BridgeType) -> Bool { + switch (self, expected) { + case let (lhs, rhs) where lhs == rhs: + return true + case (.optional(let wrapped), expected): + return wrapped == expected + default: + return false + } + } +} diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 42bfef50..0cb18fa6 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1038,6 +1038,18 @@ struct BridgeJSLink { "\(declarationPrefixKeyword.map { "\($0) "} ?? "")\(name)(\(parameters.map { $0.name }.joined(separator: ", "))) {" ) printer.indent { + let generator = DefaultValueGenerator() + for param in parameters { + if let defaultValue = param.defaultValue { + let defaultJs = generator.generate(defaultValue, format: .javascript) + printer.write("if (\(param.name) === undefined) {") + printer.indent { + printer.write("\(param.name) = \(defaultJs);") + } + printer.write("}") + } + } + printer.write(contentsOf: body) printer.write(contentsOf: cleanupCode) printer.write(lines: checkExceptionLines()) @@ -1058,8 +1070,76 @@ struct BridgeJSLink { } else { returnTypeWithEffect = returnType.tsType } - return - "(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnTypeWithEffect)" + let parameterSignatures = parameters.map { param in + let optional = param.hasDefault ? "?" : "" + return "\(param.name)\(optional): \(param.type.tsType)" + } + return "(\(parameterSignatures.joined(separator: ", "))): \(returnTypeWithEffect)" + } + + /// Helper struct for generating default value representations + private struct DefaultValueGenerator { + enum OutputFormat { + case javascript + case typescript + } + + /// Generates default value representation for JavaScript or TypeScript + func generate(_ defaultValue: DefaultValue, format: OutputFormat) -> String { + switch defaultValue { + case .string(let value): + let escapedValue = + format == .javascript + ? escapeForJavaScript(value) + : value // TypeScript doesn't need escape in doc comments + return "\"\(escapedValue)\"" + case .int(let value): + return "\(value)" + case .float(let value): + return "\(value)" + case .double(let value): + return "\(value)" + case .bool(let value): + return value ? "true" : "false" + case .null: + return "null" + case .enumCase(let enumName, let caseName): + // Extract simple enum name and add "Values" suffix for JavaScript const-style enums + // (TypeScript definitions use the base name, but JS runtime uses {Name}Values) + let simpleName = enumName.components(separatedBy: ".").last ?? enumName + let jsEnumName = format == .javascript ? "\(simpleName)Values" : simpleName + return "\(jsEnumName).\(caseName.capitalizedFirstLetter)" + case .object(let className): + return "new \(className)()" + case .objectWithArguments(let className, let args): + // JavaScript doesn't support labeled arguments, only generate values + let argStrings = args.map { arg in + generate(arg, format: format) + } + return "new \(className)(\(argStrings.joined(separator: ", ")))" + } + } + + private func escapeForJavaScript(_ string: String) -> String { + return + string + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + } + + /// Generates JSDoc comment lines for parameters with default values + /// Note: Caller should check if parameters have defaults before calling + func generateJSDoc(for parameters: [Parameter]) -> [String] { + var jsDocLines: [String] = ["/**"] + for param in parameters where param.hasDefault { + if let defaultValue = param.defaultValue { + let defaultDoc = generate(defaultValue, format: .typescript) + jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))") + } + } + jsDocLines.append(" */") + return jsDocLines + } } func renderExportedEnum(_ enumDefinition: ExportedEnum) throws -> (js: [String], dts: [String]) { @@ -1224,6 +1304,14 @@ extension BridgeJSLink { declarationPrefixKeyword: "function" ) var dtsLines: [String] = [] + + // Add JSDoc comments for parameters with default values + if function.parameters.contains(where: { $0.hasDefault }) { + let generator = DefaultValueGenerator() + let jsDocLines = generator.generateJSDoc(for: function.parameters) + dtsLines.append(contentsOf: jsDocLines) + } + dtsLines.append( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" ) @@ -1266,9 +1354,18 @@ extension BridgeJSLink { declarationPrefixKeyword: "static" ) - let dtsLines = [ + var dtsLines: [String] = [] + + // Add JSDoc comments for parameters with default values + if function.parameters.contains(where: { $0.hasDefault }) { + let generator = DefaultValueGenerator() + let jsDocLines = generator.generateJSDoc(for: function.parameters) + dtsLines.append(contentsOf: jsDocLines) + } + + dtsLines.append( "static \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" - ] + ) return (funcLines, dtsLines) } @@ -1295,9 +1392,18 @@ extension BridgeJSLink { } printer.write("},") - let dtsLines = [ + var dtsLines: [String] = [] + + // Add JSDoc comments for parameters with default values + if function.parameters.contains(where: { $0.hasDefault }) { + let generator = DefaultValueGenerator() + let jsDocLines = generator.generateJSDoc(for: function.parameters) + dtsLines.append(contentsOf: jsDocLines) + } + + dtsLines.append( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" - ] + ) return (printer.lines, dtsLines) } diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index e581fa40..a8132ad9 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -127,15 +127,33 @@ public enum SwiftEnumRawType: String, CaseIterable, Codable, Sendable { } } +public enum DefaultValue: Codable, Equatable, Sendable { + case string(String) + case int(Int) + case float(Float) + case double(Double) + case bool(Bool) + case null + case enumCase(String, String) // enumName, caseName + case object(String) // className for parameterless constructor + case objectWithArguments(String, [DefaultValue]) // className, constructor argument values +} + public struct Parameter: Codable, Equatable, Sendable { public let label: String? public let name: String public let type: BridgeType + public let defaultValue: DefaultValue? + + public var hasDefault: Bool { + return defaultValue != nil + } - public init(label: String?, name: String, type: BridgeType) { + public init(label: String?, name: String, type: BridgeType, defaultValue: DefaultValue? = nil) { self.label = label self.name = name self.type = type + self.defaultValue = defaultValue } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift new file mode 100644 index 00000000..ad4ec7de --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift @@ -0,0 +1,41 @@ +@JS public func testStringDefault(message: String = "Hello World") -> String + +@JS public func testIntDefault(count: Int = 42) -> Int + +@JS public func testBoolDefault(flag: Bool = true) -> Bool + +@JS public func testFloatDefault(value: Float = 3.14) -> Float + +@JS public func testDoubleDefault(precision: Double = 2.718) -> Double + +@JS public func testOptionalDefault(name: String? = nil) -> String? + +@JS public func testOptionalStringDefault(greeting: String? = "Hi") -> String? + +@JS public func testMultipleDefaults( + title: String = "Default Title", + count: Int = 10, + enabled: Bool = false +) -> String + +@JS public enum Status { + case active + case inactive + case pending +} + +@JS public func testEnumDefault(status: Status = .active) -> Status + +@JS class DefaultGreeter { + @JS var name: String + @JS init(name: String) { + self.name = name + } +} + +@JS class EmptyGreeter { + @JS init() {} +} + +@JS public func testComplexInit(greeter: DefaultGreeter = DefaultGreeter(name: "DefaultUser")) -> DefaultGreeter +@JS public func testEmptyInit(greeter: EmptyGreeter = EmptyGreeter()) -> EmptyGreeter diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts new file mode 100644 index 00000000..a7c20f3d --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts @@ -0,0 +1,91 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export const StatusValues: { + readonly Active: 0; + readonly Inactive: 1; + readonly Pending: 2; +}; +export type StatusTag = typeof StatusValues[keyof typeof StatusValues]; + +export type StatusObject = typeof StatusValues; + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface DefaultGreeter extends SwiftHeapObject { + name: string; +} +export interface EmptyGreeter extends SwiftHeapObject { +} +export type Exports = { + DefaultGreeter: { + new(name: string): DefaultGreeter; + } + EmptyGreeter: { + new(): EmptyGreeter; + } + /** + * @param message - Optional parameter (default: "Hello World") + */ + testStringDefault(message?: string): string; + /** + * @param count - Optional parameter (default: 42) + */ + testIntDefault(count?: number): number; + /** + * @param flag - Optional parameter (default: true) + */ + testBoolDefault(flag?: boolean): boolean; + /** + * @param value - Optional parameter (default: 3.14) + */ + testFloatDefault(value?: number): number; + /** + * @param precision - Optional parameter (default: 2.718) + */ + testDoubleDefault(precision?: number): number; + /** + * @param name - Optional parameter (default: null) + */ + testOptionalDefault(name?: string | null): string | null; + /** + * @param greeting - Optional parameter (default: "Hi") + */ + testOptionalStringDefault(greeting?: string | null): string | null; + /** + * @param title - Optional parameter (default: "Default Title") + * @param count - Optional parameter (default: 10) + * @param enabled - Optional parameter (default: false) + */ + testMultipleDefaults(title?: string, count?: number, enabled?: boolean): string; + /** + * @param status - Optional parameter (default: Status.Active) + */ + testEnumDefault(status?: StatusTag): StatusTag; + /** + * @param greeter - Optional parameter (default: new DefaultGreeter("DefaultUser")) + */ + testComplexInit(greeter?: DefaultGreeter): DefaultGreeter; + /** + * @param greeter - Optional parameter (default: new EmptyGreeter()) + */ + testEmptyInit(greeter?: EmptyGreeter): EmptyGreeter; + Status: StatusObject +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js new file mode 100644 index 00000000..31e746be --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js @@ -0,0 +1,347 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export const StatusValues = { + Active: 0, + Inactive: 1, + Pending: 2, +}; + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let tmpRetTag; + let tmpRetStrings = []; + let tmpRetInts = []; + let tmpRetF32s = []; + let tmpRetF64s = []; + let tmpParamInts = []; + let tmpParamF32s = []; + let tmpParamF64s = []; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + const bjs = {}; + importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); + bjs["swift_js_return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_tag"] = function(tag) { + tmpRetTag = tag; + } + bjs["swift_js_push_int"] = function(v) { + tmpRetInts.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + tmpRetF32s.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + tmpRetF64s.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + const value = textDecoder.decode(bytes); + tmpRetStrings.push(value); + } + bjs["swift_js_pop_param_int32"] = function() { + return tmpParamInts.pop(); + } + bjs["swift_js_pop_param_f32"] = function() { + return tmpParamF32s.pop(); + } + bjs["swift_js_pop_param_f64"] = function() { + return tmpParamF64s.pop(); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_DefaultGreeter_wrap"] = function(pointer) { + const obj = DefaultGreeter.__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_EmptyGreeter_wrap"] = function(pointer) { + const obj = EmptyGreeter.__construct(pointer); + return swift.memory.retain(obj); + }; + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype) { + const obj = Object.create(prototype); + obj.pointer = pointer; + obj.hasReleased = false; + obj.deinit = deinit; + obj.registry = new FinalizationRegistry((pointer) => { + deinit(pointer); + }); + obj.registry.register(this, obj.pointer); + return obj; + } + + release() { + this.registry.unregister(this); + this.deinit(this.pointer); + } + } + class DefaultGreeter extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_DefaultGreeter_deinit, DefaultGreeter.prototype); + } + + constructor(name) { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const ret = instance.exports.bjs_DefaultGreeter_init(nameId, nameBytes.length); + swift.memory.release(nameId); + return DefaultGreeter.__construct(ret); + } + get name() { + instance.exports.bjs_DefaultGreeter_name_get(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + set name(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_DefaultGreeter_name_set(this.pointer, valueId, valueBytes.length); + swift.memory.release(valueId); + } + } + class EmptyGreeter extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_EmptyGreeter_deinit, EmptyGreeter.prototype); + } + + constructor() { + const ret = instance.exports.bjs_EmptyGreeter_init(); + return EmptyGreeter.__construct(ret); + } + } + return { + DefaultGreeter, + EmptyGreeter, + testStringDefault: function bjs_testStringDefault(message) { + if (message === undefined) { + message = "Hello World"; + } + const messageBytes = textEncoder.encode(message); + const messageId = swift.memory.retain(messageBytes); + instance.exports.bjs_testStringDefault(messageId, messageBytes.length); + const ret = tmpRetString; + tmpRetString = undefined; + swift.memory.release(messageId); + return ret; + }, + testIntDefault: function bjs_testIntDefault(count) { + if (count === undefined) { + count = 42; + } + const ret = instance.exports.bjs_testIntDefault(count); + return ret; + }, + testBoolDefault: function bjs_testBoolDefault(flag) { + if (flag === undefined) { + flag = true; + } + const ret = instance.exports.bjs_testBoolDefault(flag); + return ret !== 0; + }, + testFloatDefault: function bjs_testFloatDefault(value) { + if (value === undefined) { + value = 3.14; + } + const ret = instance.exports.bjs_testFloatDefault(value); + return ret; + }, + testDoubleDefault: function bjs_testDoubleDefault(precision) { + if (precision === undefined) { + precision = 2.718; + } + const ret = instance.exports.bjs_testDoubleDefault(precision); + return ret; + }, + testOptionalDefault: function bjs_testOptionalDefault(name) { + if (name === undefined) { + name = null; + } + const isSome = name != null; + let nameId, nameBytes; + if (isSome) { + nameBytes = textEncoder.encode(name); + nameId = swift.memory.retain(nameBytes); + } + instance.exports.bjs_testOptionalDefault(+isSome, isSome ? nameId : 0, isSome ? nameBytes.length : 0); + const optResult = tmpRetString; + tmpRetString = undefined; + if (nameId != undefined) { + swift.memory.release(nameId); + } + return optResult; + }, + testOptionalStringDefault: function bjs_testOptionalStringDefault(greeting) { + if (greeting === undefined) { + greeting = "Hi"; + } + const isSome = greeting != null; + let greetingId, greetingBytes; + if (isSome) { + greetingBytes = textEncoder.encode(greeting); + greetingId = swift.memory.retain(greetingBytes); + } + instance.exports.bjs_testOptionalStringDefault(+isSome, isSome ? greetingId : 0, isSome ? greetingBytes.length : 0); + const optResult = tmpRetString; + tmpRetString = undefined; + if (greetingId != undefined) { + swift.memory.release(greetingId); + } + return optResult; + }, + testMultipleDefaults: function bjs_testMultipleDefaults(title, count, enabled) { + if (title === undefined) { + title = "Default Title"; + } + if (count === undefined) { + count = 10; + } + if (enabled === undefined) { + enabled = false; + } + const titleBytes = textEncoder.encode(title); + const titleId = swift.memory.retain(titleBytes); + instance.exports.bjs_testMultipleDefaults(titleId, titleBytes.length, count, enabled); + const ret = tmpRetString; + tmpRetString = undefined; + swift.memory.release(titleId); + return ret; + }, + testEnumDefault: function bjs_testEnumDefault(status) { + if (status === undefined) { + status = StatusValues.Active; + } + const ret = instance.exports.bjs_testEnumDefault(status); + return ret; + }, + testComplexInit: function bjs_testComplexInit(greeter) { + if (greeter === undefined) { + greeter = new DefaultGreeter("DefaultUser"); + } + const ret = instance.exports.bjs_testComplexInit(greeter.pointer); + return DefaultGreeter.__construct(ret); + }, + testEmptyInit: function bjs_testEmptyInit(greeter) { + if (greeter === undefined) { + greeter = new EmptyGreeter(); + } + const ret = instance.exports.bjs_testEmptyInit(greeter.pointer); + return EmptyGreeter.__construct(ret); + }, + Status: StatusValues, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json new file mode 100644 index 00000000..e29ec6a1 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json @@ -0,0 +1,482 @@ +{ + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_DefaultGreeter_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + }, + "methods" : [ + + ], + "name" : "DefaultGreeter", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + } + ], + "swiftCallName" : "DefaultGreeter" + }, + { + "constructor" : { + "abiName" : "bjs_EmptyGreeter_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + + ], + "name" : "EmptyGreeter", + "properties" : [ + + ], + "swiftCallName" : "EmptyGreeter" + } + ], + "enums" : [ + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "active" + }, + { + "associatedValues" : [ + + ], + "name" : "inactive" + }, + { + "associatedValues" : [ + + ], + "name" : "pending" + } + ], + "emitStyle" : "const", + "explicitAccessControl" : "public", + "name" : "Status", + "staticMethods" : [ + + ], + "staticProperties" : [ + + ], + "swiftCallName" : "Status" + } + ], + "functions" : [ + { + "abiName" : "bjs_testStringDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testStringDefault", + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Hello World" + } + }, + "label" : "message", + "name" : "message", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_testIntDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testIntDefault", + "parameters" : [ + { + "defaultValue" : { + "int" : { + "_0" : 42 + } + }, + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_testBoolDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testBoolDefault", + "parameters" : [ + { + "defaultValue" : { + "bool" : { + "_0" : true + } + }, + "label" : "flag", + "name" : "flag", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_testFloatDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testFloatDefault", + "parameters" : [ + { + "defaultValue" : { + "float" : { + "_0" : 3.14 + } + }, + "label" : "value", + "name" : "value", + "type" : { + "float" : { + + } + } + } + ], + "returnType" : { + "float" : { + + } + } + }, + { + "abiName" : "bjs_testDoubleDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testDoubleDefault", + "parameters" : [ + { + "defaultValue" : { + "double" : { + "_0" : 2.718 + } + }, + "label" : "precision", + "name" : "precision", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "double" : { + + } + } + }, + { + "abiName" : "bjs_testOptionalDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testOptionalDefault", + "parameters" : [ + { + "defaultValue" : { + "null" : { + + } + }, + "label" : "name", + "name" : "name", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ], + "returnType" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + }, + { + "abiName" : "bjs_testOptionalStringDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testOptionalStringDefault", + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Hi" + } + }, + "label" : "greeting", + "name" : "greeting", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ], + "returnType" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + }, + { + "abiName" : "bjs_testMultipleDefaults", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testMultipleDefaults", + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Default Title" + } + }, + "label" : "title", + "name" : "title", + "type" : { + "string" : { + + } + } + }, + { + "defaultValue" : { + "int" : { + "_0" : 10 + } + }, + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + }, + { + "defaultValue" : { + "bool" : { + "_0" : false + } + }, + "label" : "enabled", + "name" : "enabled", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_testEnumDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testEnumDefault", + "parameters" : [ + { + "defaultValue" : { + "enumCase" : { + "_0" : "Status", + "_1" : "active" + } + }, + "label" : "status", + "name" : "status", + "type" : { + "caseEnum" : { + "_0" : "Status" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "abiName" : "bjs_testComplexInit", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testComplexInit", + "parameters" : [ + { + "defaultValue" : { + "objectWithArguments" : { + "_0" : "DefaultGreeter", + "_1" : [ + { + "string" : { + "_0" : "DefaultUser" + } + } + ] + } + }, + "label" : "greeter", + "name" : "greeter", + "type" : { + "swiftHeapObject" : { + "_0" : "DefaultGreeter" + } + } + } + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "DefaultGreeter" + } + } + }, + { + "abiName" : "bjs_testEmptyInit", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testEmptyInit", + "parameters" : [ + { + "defaultValue" : { + "object" : { + "_0" : "EmptyGreeter" + } + }, + "label" : "greeter", + "name" : "greeter", + "type" : { + "swiftHeapObject" : { + "_0" : "EmptyGreeter" + } + } + } + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "EmptyGreeter" + } + } + } + ], + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift new file mode 100644 index 00000000..d20df871 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift @@ -0,0 +1,250 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(BridgeJS) import JavaScriptKit + +extension Status: _BridgedSwiftCaseEnum { + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { + return bridgeJSRawValue + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftReturn(_ value: Int32) -> Status { + return Status(bridgeJSRawValue: value)! + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter(_ value: Int32) -> Status { + return Status(bridgeJSRawValue: value)! + } + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() -> Int32 { + return bridgeJSRawValue + } + + private init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .active + case 1: + self = .inactive + case 2: + self = .pending + default: + return nil + } + } + + private var bridgeJSRawValue: Int32 { + switch self { + case .active: + return 0 + case .inactive: + return 1 + case .pending: + return 2 + } + } +} + +@_expose(wasm, "bjs_testStringDefault") +@_cdecl("bjs_testStringDefault") +public func _bjs_testStringDefault(messageBytes: Int32, messageLength: Int32) -> Void { + #if arch(wasm32) + let ret = testStringDefault(message: String.bridgeJSLiftParameter(messageBytes, messageLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testIntDefault") +@_cdecl("bjs_testIntDefault") +public func _bjs_testIntDefault(count: Int32) -> Int32 { + #if arch(wasm32) + let ret = testIntDefault(count: Int.bridgeJSLiftParameter(count)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testBoolDefault") +@_cdecl("bjs_testBoolDefault") +public func _bjs_testBoolDefault(flag: Int32) -> Int32 { + #if arch(wasm32) + let ret = testBoolDefault(flag: Bool.bridgeJSLiftParameter(flag)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testFloatDefault") +@_cdecl("bjs_testFloatDefault") +public func _bjs_testFloatDefault(value: Float32) -> Float32 { + #if arch(wasm32) + let ret = testFloatDefault(value: Float.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testDoubleDefault") +@_cdecl("bjs_testDoubleDefault") +public func _bjs_testDoubleDefault(precision: Float64) -> Float64 { + #if arch(wasm32) + let ret = testDoubleDefault(precision: Double.bridgeJSLiftParameter(precision)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testOptionalDefault") +@_cdecl("bjs_testOptionalDefault") +public func _bjs_testOptionalDefault(nameIsSome: Int32, nameBytes: Int32, nameLength: Int32) -> Void { + #if arch(wasm32) + let ret = testOptionalDefault(name: Optional.bridgeJSLiftParameter(nameIsSome, nameBytes, nameLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testOptionalStringDefault") +@_cdecl("bjs_testOptionalStringDefault") +public func _bjs_testOptionalStringDefault(greetingIsSome: Int32, greetingBytes: Int32, greetingLength: Int32) -> Void { + #if arch(wasm32) + let ret = testOptionalStringDefault(greeting: Optional.bridgeJSLiftParameter(greetingIsSome, greetingBytes, greetingLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testMultipleDefaults") +@_cdecl("bjs_testMultipleDefaults") +public func _bjs_testMultipleDefaults(titleBytes: Int32, titleLength: Int32, count: Int32, enabled: Int32) -> Void { + #if arch(wasm32) + let ret = testMultipleDefaults(title: String.bridgeJSLiftParameter(titleBytes, titleLength), count: Int.bridgeJSLiftParameter(count), enabled: Bool.bridgeJSLiftParameter(enabled)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testEnumDefault") +@_cdecl("bjs_testEnumDefault") +public func _bjs_testEnumDefault(status: Int32) -> Int32 { + #if arch(wasm32) + let ret = testEnumDefault(status: Status.bridgeJSLiftParameter(status)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testComplexInit") +@_cdecl("bjs_testComplexInit") +public func _bjs_testComplexInit(greeter: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = testComplexInit(greeter: DefaultGreeter.bridgeJSLiftParameter(greeter)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testEmptyInit") +@_cdecl("bjs_testEmptyInit") +public func _bjs_testEmptyInit(greeter: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = testEmptyInit(greeter: EmptyGreeter.bridgeJSLiftParameter(greeter)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_DefaultGreeter_init") +@_cdecl("bjs_DefaultGreeter_init") +public func _bjs_DefaultGreeter_init(nameBytes: Int32, nameLength: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = DefaultGreeter(name: String.bridgeJSLiftParameter(nameBytes, nameLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_DefaultGreeter_name_get") +@_cdecl("bjs_DefaultGreeter_name_get") +public func _bjs_DefaultGreeter_name_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = DefaultGreeter.bridgeJSLiftParameter(_self).name + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_DefaultGreeter_name_set") +@_cdecl("bjs_DefaultGreeter_name_set") +public func _bjs_DefaultGreeter_name_set(_self: UnsafeMutableRawPointer, valueBytes: Int32, valueLength: Int32) -> Void { + #if arch(wasm32) + DefaultGreeter.bridgeJSLiftParameter(_self).name = String.bridgeJSLiftParameter(valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_DefaultGreeter_deinit") +@_cdecl("bjs_DefaultGreeter_deinit") +public func _bjs_DefaultGreeter_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension DefaultGreeter: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "TestModule", name: "bjs_DefaultGreeter_wrap") + func _bjs_DefaultGreeter_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_DefaultGreeter_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_DefaultGreeter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +@_expose(wasm, "bjs_EmptyGreeter_init") +@_cdecl("bjs_EmptyGreeter_init") +public func _bjs_EmptyGreeter_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = EmptyGreeter() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_EmptyGreeter_deinit") +@_cdecl("bjs_EmptyGreeter_deinit") +public func _bjs_EmptyGreeter_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension EmptyGreeter: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "TestModule", name: "bjs_EmptyGreeter_wrap") + func _bjs_EmptyGreeter_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_EmptyGreeter_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_EmptyGreeter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md index 41315708..05be58fa 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md @@ -67,6 +67,7 @@ This command will: - - - +- - - - diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md new file mode 100644 index 00000000..238829db --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md @@ -0,0 +1,136 @@ +# Default Parameters in Exported Swift Functions + +Learn how to use default parameter values in Swift functions exported to JavaScript. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS supports default parameter values for Swift functions exported to JavaScript. When you specify default values in your Swift code, they are automatically applied in the generated JavaScript bindings. + +```swift +import JavaScriptKit + +@JS public func greet(name: String = "World", enthusiastic: Bool = false) -> String { + let greeting = "Hello, \(name)" + return enthusiastic ? "\(greeting)!" : greeting +} +``` + +In JavaScript, parameters with defaults become optional: + +```javascript +exports.greet(); // "Hello, World" +exports.greet("Alice"); // "Hello, Alice" +exports.greet("Bob", true); // "Hello, Bob!" +``` + +The generated TypeScript definitions show optional parameters with JSDoc comments: + +```typescript +export type Exports = { + /** + * @param name - Optional parameter (default: "World") + * @param enthusiastic - Optional parameter (default: false) + */ + greet(name?: string, enthusiastic?: boolean): string; +} +``` + +## Skipping Parameters With Default Values + +To use a default value for a middle parameter while providing later parameters, pass `undefined`: + +```swift +@JS public func configure(title: String = "Default", count: Int = 10, enabled: Bool = false) -> String { + return "\(title): \(count) (\(enabled))" +} +``` + +```javascript +// Use all defaults +exports.configure(); // "Default: 10 (false)" + +// Provide first parameter only +exports.configure("Custom"); // "Custom: 10 (false)" + +// Skip middle parameter with undefined +exports.configure("Custom", undefined, true); // "Custom: 10 (true)" + +// Provide all parameters +exports.configure("Custom", 5, true); // "Custom: 5 (true)" +``` + +## Supported Default Value Types + +| Default Value Type | Swift Example | JavaScript/TypeScript | +|:-------------------|:-------------|:----------------------| +| String literals | `"hello"` | `"hello"` | +| Integer literals | `42` | `42` | +| Float literals | `3.14` | `3.14` | +| Double literals | `2.718` | `2.718` | +| Boolean literals | `true`, `false` | `true`, `false` | +| Nil for optionals | `nil` | `null` | +| Enum cases (shorthand) | `.north` | `Direction.North` | +| Enum cases (qualified) | `Direction.north` | `Direction.North` | +| Object initialization (no args) | `MyClass()` | `new MyClass()` | +| Object initialization (literal args) | `MyClass("value", 42)` | `new MyClass("value", 42)` | + +## Working with Class Instances as Default Parameters + +You can use class initialization expressions as default values: + +```swift +@JS class Config { + var setting: String + + @JS init(setting: String) { + self.setting = setting + } +} + +@JS public func process(config: Config = Config(setting: "default")) -> String { + return "Using: \(config.setting)" +} +``` + +In JavaScript: + +```javascript +// Uses default Config instance +exports.process(); // "Using: default" + +// Provides custom Config instance +const custom = new exports.Config("custom"); +exports.process(custom); // "Using: custom" +custom.release(); +``` + +**Limitations for object initialization:** +- Constructor arguments must be literal values (`"text"`, `42`, `true`, `false`, `nil`) +- Complex expressions in constructor arguments are not supported +- Computed properties or method calls as arguments are not supported + +## Unsupported Default Value Types + +The following expressions are **not supported** as default parameter values: + +| Expression Type | Example | Status | +|:----------------|:--------|:-------| +| Method calls | `Date().description` | ❌ | +| Closures | `{ "computed" }()` | ❌ | +| Array literals | `[1, 2, 3]` | ❌ | +| Dictionary literals | `["key": "value"]` | ❌ | +| Binary operations | `10 + 20` | ❌ | +| Complex member access | `Config.shared.value` | ❌ | +| Ternary operators | `flag ? "a" : "b"` | ❌ | +| Object init with complex args | `Config(setting: getValue())` | ❌ | + +When a complex default value is needed, make the parameter optional and handle the default in the function body: + +```swift +@JS public func process(config: Config? = nil) -> String { + let actualConfig = config ?? createDefaultConfig() + return actualConfig.process() +} +``` diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md index fff45f11..ebc8037c 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md @@ -150,4 +150,4 @@ export type Exports = { | Async methods: `func x() async` | ✅ | | Generics | ❌ | | Opaque types: `func x() -> some P`, `func y(_: some P)` | ❌ | -| Default parameter values: `func x(_ foo: String = "")` | ❌ | \ No newline at end of file +| Default parameter values: `func x(_ foo: String = "")` | ✅ (See ) | \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index a2cd3c1a..e766d2c6 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -700,6 +700,52 @@ enum APIOptionalResult { } } +// MARK: - Default Parameters + +@JS func testStringDefault(message: String = "Hello World") -> String { + return message +} + +@JS func testIntDefault(count: Int = 42) -> Int { + return count +} + +@JS func testBoolDefault(flag: Bool = true) -> Bool { + return flag +} + +@JS func testOptionalDefault(name: String? = nil) -> String? { + return name +} + +@JS func testMultipleDefaults( + title: String = "Default Title", + count: Int = 10, + enabled: Bool = false +) -> String { + return "\(title): \(count) (\(enabled))" +} + +@JS func testSimpleEnumDefault(status: Status = .success) -> Status { + return status +} + +@JS func testDirectionDefault(direction: Direction = .north) -> Direction { + return direction +} + +@JS func testRawStringEnumDefault(theme: Theme = .light) -> Theme { + return theme +} + +@JS func testComplexInit(greeter: Greeter = Greeter(name: "DefaultGreeter")) -> String { + return greeter.greet() +} + +@JS func testEmptyInit(_ object: StaticPropertyHolder = StaticPropertyHolder()) -> StaticPropertyHolder { + return object +} + // MARK: - Static Properties @JS class StaticPropertyHolder { diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift index 3b5baabb..7504d20d 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift @@ -1918,6 +1918,116 @@ public func _bjs_getObserverStats() -> Void { #endif } +@_expose(wasm, "bjs_testStringDefault") +@_cdecl("bjs_testStringDefault") +public func _bjs_testStringDefault(messageBytes: Int32, messageLength: Int32) -> Void { + #if arch(wasm32) + let ret = testStringDefault(message: String.bridgeJSLiftParameter(messageBytes, messageLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testIntDefault") +@_cdecl("bjs_testIntDefault") +public func _bjs_testIntDefault(count: Int32) -> Int32 { + #if arch(wasm32) + let ret = testIntDefault(count: Int.bridgeJSLiftParameter(count)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testBoolDefault") +@_cdecl("bjs_testBoolDefault") +public func _bjs_testBoolDefault(flag: Int32) -> Int32 { + #if arch(wasm32) + let ret = testBoolDefault(flag: Bool.bridgeJSLiftParameter(flag)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testOptionalDefault") +@_cdecl("bjs_testOptionalDefault") +public func _bjs_testOptionalDefault(nameIsSome: Int32, nameBytes: Int32, nameLength: Int32) -> Void { + #if arch(wasm32) + let ret = testOptionalDefault(name: Optional.bridgeJSLiftParameter(nameIsSome, nameBytes, nameLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testMultipleDefaults") +@_cdecl("bjs_testMultipleDefaults") +public func _bjs_testMultipleDefaults(titleBytes: Int32, titleLength: Int32, count: Int32, enabled: Int32) -> Void { + #if arch(wasm32) + let ret = testMultipleDefaults(title: String.bridgeJSLiftParameter(titleBytes, titleLength), count: Int.bridgeJSLiftParameter(count), enabled: Bool.bridgeJSLiftParameter(enabled)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testSimpleEnumDefault") +@_cdecl("bjs_testSimpleEnumDefault") +public func _bjs_testSimpleEnumDefault(status: Int32) -> Int32 { + #if arch(wasm32) + let ret = testSimpleEnumDefault(status: Status.bridgeJSLiftParameter(status)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testDirectionDefault") +@_cdecl("bjs_testDirectionDefault") +public func _bjs_testDirectionDefault(direction: Int32) -> Int32 { + #if arch(wasm32) + let ret = testDirectionDefault(direction: Direction.bridgeJSLiftParameter(direction)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testRawStringEnumDefault") +@_cdecl("bjs_testRawStringEnumDefault") +public func _bjs_testRawStringEnumDefault(themeBytes: Int32, themeLength: Int32) -> Void { + #if arch(wasm32) + let ret = testRawStringEnumDefault(theme: Theme.bridgeJSLiftParameter(themeBytes, themeLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testComplexInit") +@_cdecl("bjs_testComplexInit") +public func _bjs_testComplexInit(greeter: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = testComplexInit(greeter: Greeter.bridgeJSLiftParameter(greeter)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_testEmptyInit") +@_cdecl("bjs_testEmptyInit") +public func _bjs_testEmptyInit(object: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = testEmptyInit(_: StaticPropertyHolder.bridgeJSLiftParameter(object)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_getAllStaticPropertyValues") @_cdecl("bjs_getAllStaticPropertyValues") public func _bjs_getAllStaticPropertyValues() -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json index 15c5a3a7..0cfea50d 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -4898,6 +4898,354 @@ } } }, + { + "abiName" : "bjs_testStringDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testStringDefault", + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Hello World" + } + }, + "label" : "message", + "name" : "message", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_testIntDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testIntDefault", + "parameters" : [ + { + "defaultValue" : { + "int" : { + "_0" : 42 + } + }, + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_testBoolDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testBoolDefault", + "parameters" : [ + { + "defaultValue" : { + "bool" : { + "_0" : true + } + }, + "label" : "flag", + "name" : "flag", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_testOptionalDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testOptionalDefault", + "parameters" : [ + { + "defaultValue" : { + "null" : { + + } + }, + "label" : "name", + "name" : "name", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ], + "returnType" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + }, + { + "abiName" : "bjs_testMultipleDefaults", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testMultipleDefaults", + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Default Title" + } + }, + "label" : "title", + "name" : "title", + "type" : { + "string" : { + + } + } + }, + { + "defaultValue" : { + "int" : { + "_0" : 10 + } + }, + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + }, + { + "defaultValue" : { + "bool" : { + "_0" : false + } + }, + "label" : "enabled", + "name" : "enabled", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_testSimpleEnumDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testSimpleEnumDefault", + "parameters" : [ + { + "defaultValue" : { + "enumCase" : { + "_0" : "Status", + "_1" : "success" + } + }, + "label" : "status", + "name" : "status", + "type" : { + "caseEnum" : { + "_0" : "Status" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "abiName" : "bjs_testDirectionDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testDirectionDefault", + "parameters" : [ + { + "defaultValue" : { + "enumCase" : { + "_0" : "Direction", + "_1" : "north" + } + }, + "label" : "direction", + "name" : "direction", + "type" : { + "caseEnum" : { + "_0" : "Direction" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "Direction" + } + } + }, + { + "abiName" : "bjs_testRawStringEnumDefault", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testRawStringEnumDefault", + "parameters" : [ + { + "defaultValue" : { + "enumCase" : { + "_0" : "Theme", + "_1" : "light" + } + }, + "label" : "theme", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + }, + { + "abiName" : "bjs_testComplexInit", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testComplexInit", + "parameters" : [ + { + "defaultValue" : { + "objectWithArguments" : { + "_0" : "Greeter", + "_1" : [ + { + "string" : { + "_0" : "DefaultGreeter" + } + } + ] + } + }, + "label" : "greeter", + "name" : "greeter", + "type" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_testEmptyInit", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "testEmptyInit", + "parameters" : [ + { + "defaultValue" : { + "object" : { + "_0" : "StaticPropertyHolder" + } + }, + "label" : "_", + "name" : "object", + "type" : { + "swiftHeapObject" : { + "_0" : "StaticPropertyHolder" + } + } + } + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "StaticPropertyHolder" + } + } + }, { "abiName" : "bjs_getAllStaticPropertyValues", "effects" : { diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 6b877d3a..e080397f 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -642,6 +642,47 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(StaticCalculatorValues.Scientific, exports.StaticCalculator.Scientific); assert.equal(StaticCalculatorValues.Basic, exports.StaticCalculator.Basic); assert.equal(globalThis.StaticUtils.Nested.roundtrip("hello world"), "hello world"); + + // Test default parameters + assert.equal(exports.testStringDefault(), "Hello World"); + assert.equal(exports.testStringDefault("Custom Message"), "Custom Message"); + + assert.equal(exports.testIntDefault(), 42); + assert.equal(exports.testIntDefault(100), 100); + + assert.equal(exports.testBoolDefault(), true); + assert.equal(exports.testBoolDefault(false), false); + + assert.equal(exports.testOptionalDefault(), null); + assert.equal(exports.testOptionalDefault("Test"), "Test"); + + assert.equal(exports.testMultipleDefaults(), "Default Title: 10 (false)"); + assert.equal(exports.testMultipleDefaults("Custom"), "Custom: 10 (false)"); + assert.equal(exports.testMultipleDefaults("Custom", 5), "Custom: 5 (false)"); + assert.equal(exports.testMultipleDefaults("Custom", undefined, true), "Custom: 10 (true)"); + assert.equal(exports.testMultipleDefaults("Custom", 5, true), "Custom: 5 (true)"); + + assert.equal(exports.testSimpleEnumDefault(), exports.Status.Success); + assert.equal(exports.testSimpleEnumDefault(exports.Status.Loading), exports.Status.Loading); + + assert.equal(exports.testDirectionDefault(), exports.Direction.North); + assert.equal(exports.testDirectionDefault(exports.Direction.South), exports.Direction.South); + + assert.equal(exports.testRawStringEnumDefault(), exports.Theme.Light); + assert.equal(exports.testRawStringEnumDefault(exports.Theme.Dark), exports.Theme.Dark); + + const holder = exports.testEmptyInit() + assert.notEqual(holder, null); + holder.release(); + + const customHolder = new exports.StaticPropertyHolder(); + assert.deepEqual(exports.testEmptyInit(customHolder), customHolder); + customHolder.release(); + + assert.equal(exports.testComplexInit(), "Hello, DefaultGreeter!"); + const customGreeter = new exports.Greeter("CustomName"); + assert.equal(exports.testComplexInit(customGreeter), "Hello, CustomName!"); + customGreeter.release(); } /** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */ From e14455471f6acead9f1a770e096d8b5b67707217 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Thu, 2 Oct 2025 15:10:37 +0200 Subject: [PATCH 2/2] BridgeJS: Fix constructor and negative numbers support BridgeJS: Clean up BridgeJS: Simplified syntax for defaults in .js --- .../Sources/BridgeJSCore/ExportSwift.swift | 122 +++++------- .../Sources/BridgeJSLink/BridgeJSLink.swift | 114 +++++------ .../Sources/BridgeJSLink/JSGlueGen.swift | 18 +- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 44 +++-- .../Inputs/DefaultParameters.swift | 37 +++- .../DefaultParameters.Export.d.ts | 26 ++- .../DefaultParameters.Export.js | 149 +++++++++----- .../ExportSwiftTests/DefaultParameters.json | 184 +++++++++++++++++- .../ExportSwiftTests/DefaultParameters.swift | 163 +++++++++++++++- .../Exporting-Swift-Default-Parameters.md | 63 ++++-- .../BridgeJSRuntimeTests/ExportAPITests.swift | 35 +++- .../Generated/BridgeJS.ExportSwift.swift | 147 ++++++++++++++ .../JavaScript/BridgeJS.ExportSwift.json | 166 +++++++++++++++- Tests/prelude.mjs | 26 ++- 14 files changed, 1035 insertions(+), 259 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 71e72460..2a10a360 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -243,50 +243,20 @@ public class ExportSwift { return .null } - if let stringLiteral = expr.as(StringLiteralExprSyntax.self), - let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self), - type.matches(against: .string) - { - return .string(segment.content.text) - } - - if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self), - type.matches(against: .bool) - { - return .bool(boolLiteral.literal.text == "true") - } - - if let intLiteral = expr.as(IntegerLiteralExprSyntax.self), - let intValue = Int(intLiteral.literal.text), - type.matches(against: .int) - { - return .int(intValue) - } - - if let floatLiteral = expr.as(FloatLiteralExprSyntax.self) { - if type.matches(against: .float), - let floatValue = Float(floatLiteral.literal.text) - { - return .float(floatValue) - } - if type.matches(against: .double), - let doubleValue = Double(floatLiteral.literal.text) - { - return .double(doubleValue) - } - } - if let memberExpr = expr.as(MemberAccessExprSyntax.self), let enumValue = extractEnumCaseValue(from: memberExpr, type: type) { return enumValue } - // Constructor calls (e.g., Greeter(name: "John")) if let funcCall = expr.as(FunctionCallExprSyntax.self) { return extractConstructorDefaultValue(from: funcCall, type: type) } + if let literalValue = extractLiteralValue(from: expr, type: type) { + return literalValue + } + diagnose( node: expr, message: "Unsupported default parameter value expression", @@ -300,7 +270,6 @@ public class ExportSwift { from funcCall: FunctionCallExprSyntax, type: BridgeType ) -> DefaultValue? { - // Extract class name guard let calledExpr = funcCall.calledExpression.as(DeclReferenceExprSyntax.self) else { diagnose( node: funcCall, @@ -311,8 +280,6 @@ public class ExportSwift { } let className = calledExpr.baseName.text - - // Verify type matches let expectedClassName: String? switch type { case .swiftHeapObject(let name): @@ -337,17 +304,13 @@ public class ExportSwift { return nil } - // Handle parameterless constructor if funcCall.arguments.isEmpty { return .object(className) } - // Extract arguments for constructor with parameters var constructorArgs: [DefaultValue] = [] for argument in funcCall.arguments { - // Recursively extract the argument's default value - // For now, only support literals in constructor arguments - guard let argValue = extractConstructorArgumentValue(from: argument.expression) else { + guard let argValue = extractLiteralValue(from: argument.expression) else { diagnose( node: argument.expression, message: "Constructor argument must be a literal value", @@ -362,42 +325,64 @@ public class ExportSwift { return .objectWithArguments(className, constructorArgs) } - /// Extracts a literal value from an expression for use in constructor arguments - private func extractConstructorArgumentValue(from expr: ExprSyntax) -> DefaultValue? { - // String literals + /// Extracts a literal value from an expression with optional type checking + private func extractLiteralValue(from expr: ExprSyntax, type: BridgeType? = nil) -> DefaultValue? { + if expr.is(NilLiteralExprSyntax.self) { + return .null + } + if let stringLiteral = expr.as(StringLiteralExprSyntax.self), let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self) { - return .string(segment.content.text) + let value = DefaultValue.string(segment.content.text) + if let type = type, !type.isCompatibleWith(.string) { + return nil + } + return value } - // Boolean literals if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self) { - return .bool(boolLiteral.literal.text == "true") + let value = DefaultValue.bool(boolLiteral.literal.text == "true") + if let type = type, !type.isCompatibleWith(.bool) { + return nil + } + return value + } + + var numericExpr = expr + var isNegative = false + if let prefixExpr = expr.as(PrefixOperatorExprSyntax.self), + prefixExpr.operator.text == "-" + { + numericExpr = prefixExpr.expression + isNegative = true } - // Integer literals - if let intLiteral = expr.as(IntegerLiteralExprSyntax.self), + if let intLiteral = numericExpr.as(IntegerLiteralExprSyntax.self), let intValue = Int(intLiteral.literal.text) { - return .int(intValue) + let value = DefaultValue.int(isNegative ? -intValue : intValue) + if let type = type, !type.isCompatibleWith(.int) { + return nil + } + return value } - // Float literals - if let floatLiteral = expr.as(FloatLiteralExprSyntax.self) { + if let floatLiteral = numericExpr.as(FloatLiteralExprSyntax.self) { if let floatValue = Float(floatLiteral.literal.text) { - return .float(floatValue) + let value = DefaultValue.float(isNegative ? -floatValue : floatValue) + if type == nil || type?.isCompatibleWith(.float) == true { + return value + } } if let doubleValue = Double(floatLiteral.literal.text) { - return .double(doubleValue) + let value = DefaultValue.double(isNegative ? -doubleValue : doubleValue) + if type == nil || type?.isCompatibleWith(.double) == true { + return value + } } } - // nil literal - if expr.is(NilLiteralExprSyntax.self) { - return .null - } - return nil } @@ -885,7 +870,7 @@ public class ExportSwift { swiftCallName: swiftCallName, explicitAccessControl: explicitAccessControl, cases: [], // Will be populated in visit(EnumCaseDeclSyntax) - rawType: rawType, + rawType: SwiftEnumRawType(rawType), namespace: effectiveNamespace, emitStyle: emitStyle, staticMethods: [], @@ -923,9 +908,7 @@ public class ExportSwift { if case .tsEnum = emitStyle { // Check for Bool raw type limitation - if let raw = exportedEnum.rawType, - let rawEnum = SwiftEnumRawType.from(raw), rawEnum == .bool - { + if exportedEnum.rawType == .bool { diagnose( node: jsAttribute, message: "TypeScript enum style is not supported for Bool raw-value enums", @@ -1180,7 +1163,7 @@ public class ExportSwift { return Constants.supportedRawTypes.contains(typeName) }?.type.trimmedDescription - if let rawTypeString, let rawType = SwiftEnumRawType.from(rawTypeString) { + if let rawType = SwiftEnumRawType(rawTypeString) { return .rawValueEnum(swiftCallName, rawType) } else { let hasAnyCases = enumDecl.memberBlock.members.contains { member in @@ -2160,12 +2143,13 @@ extension WithModifiersSyntax { } fileprivate extension BridgeType { - func matches(against expected: BridgeType) -> Bool { - switch (self, expected) { + /// Returns true if a value of `expectedType` can be assigned to this type. + func isCompatibleWith(_ expectedType: BridgeType) -> Bool { + switch (self, expectedType) { case let (lhs, rhs) where lhs == rhs: return true - case (.optional(let wrapped), expected): - return wrapped == expected + case (.optional(let wrapped), expectedType): + return wrapped == expectedType default: return false } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 0cb18fa6..087cf17d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1026,6 +1026,16 @@ struct BridgeJSLink { ] } + func generateParameterList(parameters: [Parameter]) -> String { + parameters.map { param in + if let defaultValue = param.defaultValue { + let defaultJs = DefaultValueGenerator().generate(defaultValue, format: .javascript) + return "\(param.name) = \(defaultJs)" + } + return param.name + }.joined(separator: ", ") + } + func renderFunction( name: String, parameters: [Parameter], @@ -1034,22 +1044,12 @@ struct BridgeJSLink { ) -> [String] { let printer = CodeFragmentPrinter() + let parameterList = generateParameterList(parameters: parameters) + printer.write( - "\(declarationPrefixKeyword.map { "\($0) "} ?? "")\(name)(\(parameters.map { $0.name }.joined(separator: ", "))) {" + "\(declarationPrefixKeyword.map { "\($0) "} ?? "")\(name)(\(parameterList)) {" ) printer.indent { - let generator = DefaultValueGenerator() - for param in parameters { - if let defaultValue = param.defaultValue { - let defaultJs = generator.generate(defaultValue, format: .javascript) - printer.write("if (\(param.name) === undefined) {") - printer.indent { - printer.write("\(param.name) = \(defaultJs);") - } - printer.write("}") - } - } - printer.write(contentsOf: body) printer.write(contentsOf: cleanupCode) printer.write(lines: checkExceptionLines()) @@ -1077,6 +1077,12 @@ struct BridgeJSLink { return "(\(parameterSignatures.joined(separator: ", "))): \(returnTypeWithEffect)" } + /// Helper method to append JSDoc comments for parameters with default values + private func appendJSDocIfNeeded(for parameters: [Parameter], to lines: inout [String]) { + let jsDocLines = DefaultValueGenerator().generateJSDoc(for: parameters) + lines.append(contentsOf: jsDocLines) + } + /// Helper struct for generating default value representations private struct DefaultValueGenerator { enum OutputFormat { @@ -1104,15 +1110,12 @@ struct BridgeJSLink { case .null: return "null" case .enumCase(let enumName, let caseName): - // Extract simple enum name and add "Values" suffix for JavaScript const-style enums - // (TypeScript definitions use the base name, but JS runtime uses {Name}Values) let simpleName = enumName.components(separatedBy: ".").last ?? enumName let jsEnumName = format == .javascript ? "\(simpleName)Values" : simpleName return "\(jsEnumName).\(caseName.capitalizedFirstLetter)" case .object(let className): return "new \(className)()" case .objectWithArguments(let className, let args): - // JavaScript doesn't support labeled arguments, only generate values let argStrings = args.map { arg in generate(arg, format: format) } @@ -1128,10 +1131,14 @@ struct BridgeJSLink { } /// Generates JSDoc comment lines for parameters with default values - /// Note: Caller should check if parameters have defaults before calling func generateJSDoc(for parameters: [Parameter]) -> [String] { + let paramsWithDefaults = parameters.filter { $0.hasDefault } + guard !paramsWithDefaults.isEmpty else { + return [] + } + var jsDocLines: [String] = ["/**"] - for param in parameters where param.hasDefault { + for param in paramsWithDefaults { if let defaultValue = param.defaultValue { let defaultDoc = generate(defaultValue, format: .typescript) jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))") @@ -1190,9 +1197,7 @@ struct BridgeJSLink { printer.indent { for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - let value = getEnumCaseValue( - enumCase: enumCase, - enumType: enumDefinition.enumType, + let value = enumCase.jsValue( rawType: enumDefinition.rawType, index: index ) @@ -1212,9 +1217,7 @@ struct BridgeJSLink { printer.indent { for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - let value = getEnumCaseValue( - enumCase: enumCase, - enumType: enumDefinition.enumType, + let value = enumCase.jsValue( rawType: enumDefinition.rawType, index: index ) @@ -1272,17 +1275,6 @@ struct BridgeJSLink { return printer.lines } - private func getEnumCaseValue(enumCase: EnumCase, enumType: EnumType, rawType: String?, index: Int) -> String { - switch enumType { - case .simple: - return "\(index)" - case .rawValue: - let rawValue = enumCase.rawValue ?? enumCase.name - return SwiftEnumRawType.formatValue(rawValue, rawType: rawType ?? "") - case .associatedValue, .namespace: - return "" - } - } } extension BridgeJSLink { @@ -1305,12 +1297,7 @@ extension BridgeJSLink { ) var dtsLines: [String] = [] - // Add JSDoc comments for parameters with default values - if function.parameters.contains(where: { $0.hasDefault }) { - let generator = DefaultValueGenerator() - let jsDocLines = generator.generateJSDoc(for: function.parameters) - dtsLines.append(contentsOf: jsDocLines) - } + appendJSDocIfNeeded(for: function.parameters, to: &dtsLines) dtsLines.append( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" @@ -1356,12 +1343,7 @@ extension BridgeJSLink { var dtsLines: [String] = [] - // Add JSDoc comments for parameters with default values - if function.parameters.contains(where: { $0.hasDefault }) { - let generator = DefaultValueGenerator() - let jsDocLines = generator.generateJSDoc(for: function.parameters) - dtsLines.append(contentsOf: jsDocLines) - } + appendJSDocIfNeeded(for: function.parameters, to: &dtsLines) dtsLines.append( "static \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" @@ -1394,12 +1376,7 @@ extension BridgeJSLink { var dtsLines: [String] = [] - // Add JSDoc comments for parameters with default values - if function.parameters.contains(where: { $0.hasDefault }) { - let generator = DefaultValueGenerator() - let jsDocLines = generator.generateJSDoc(for: function.parameters) - dtsLines.append(contentsOf: jsDocLines) - } + appendJSDocIfNeeded(for: function.parameters, to: &dtsLines) dtsLines.append( "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));" @@ -1439,10 +1416,11 @@ extension BridgeJSLink { try thunkBuilder.lowerParameter(param: param) } let returnExpr = try thunkBuilder.call(abiName: function.abiName, returnType: function.returnType) + let paramList = thunkBuilder.generateParameterList(parameters: function.parameters) let printer = CodeFragmentPrinter() printer.write( - "\(enumName).\(function.name) = function(\(function.parameters.map { $0.name }.joined(separator: ", "))) {" + "\(enumName).\(function.name) = function(\(paramList)) {" ) printer.indent { printer.write(contentsOf: thunkBuilder.body) @@ -1627,8 +1605,10 @@ extension BridgeJSLink { try thunkBuilder.lowerParameter(param: param) } + let constructorParamList = thunkBuilder.generateParameterList(parameters: constructor.parameters) + jsPrinter.indent { - jsPrinter.write("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {") + jsPrinter.write("constructor(\(constructorParamList)) {") let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) jsPrinter.indent { jsPrinter.write(contentsOf: thunkBuilder.body) @@ -1640,6 +1620,10 @@ extension BridgeJSLink { } dtsExportEntryPrinter.indent { + let jsDocLines = DefaultValueGenerator().generateJSDoc(for: constructor.parameters) + for line in jsDocLines { + dtsExportEntryPrinter.write(line) + } dtsExportEntryPrinter.write( "new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name), effects: constructor.effects));" ) @@ -2151,22 +2135,26 @@ extension BridgeJSLink { case .tsEnum: printer.write("enum \(enumDefinition.name) {") printer.indent { - for enumCase in enumDefinition.cases { + for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue = SwiftEnumRawType.formatValue(rawValue, rawType: rawType) - printer.write("\(caseName) = \(formattedValue),") + let value = enumCase.jsValue( + rawType: rawType, + index: index + ) + printer.write("\(caseName) = \(value),") } } printer.write("}") case .const: printer.write("const \(enumValuesName): {") printer.indent { - for enumCase in enumDefinition.cases { + for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue = SwiftEnumRawType.formatValue(rawValue, rawType: rawType) - printer.write("readonly \(caseName): \(formattedValue);") + let value = enumCase.jsValue( + rawType: rawType, + index: index + ) + printer.write("readonly \(caseName): \(value);") } } printer.write("};") diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 8b0f3f47..9d0d9162 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -623,7 +623,11 @@ struct IntrinsicJSFragment: Sendable { printer.indent { for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - printer.write("\(caseName): \(index),") + let value = enumCase.jsValue( + rawType: enumDefinition.rawType, + index: index + ) + printer.write("\(caseName): \(value),") } } printer.write("};") @@ -641,15 +645,13 @@ struct IntrinsicJSFragment: Sendable { let enumName = arguments[0] printer.write("const \(enumName) = {") printer.indent { - for enumCase in enumDefinition.cases { + for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue = SwiftEnumRawType.formatValue( - rawValue, - rawType: enumDefinition.rawType ?? "" + let value = enumCase.jsValue( + rawType: enumDefinition.rawType, + index: index ) - - printer.write("\(caseName): \(formattedValue),") + printer.write("\(caseName): \(value),") } } printer.write("};") diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index a8132ad9..84595782 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -107,23 +107,13 @@ public enum SwiftEnumRawType: String, CaseIterable, Codable, Sendable { } } - public static func from(_ rawTypeString: String) -> SwiftEnumRawType? { - return Self.allCases.first { $0.rawValue == rawTypeString } - } - - public static func formatValue(_ rawValue: String, rawType: String) -> String { - if let enumType = from(rawType) { - switch enumType { - case .string: - return "\"\(rawValue)\"" - case .bool: - return rawValue.lowercased() == "true" ? "true" : "false" - case .float, .double, .int, .int32, .int64, .uint, .uint32, .uint64: - return rawValue - } - } else { - return rawValue + public init?(_ rawTypeString: String?) { + guard let rawTypeString = rawTypeString, + let match = Self.allCases.first(where: { $0.rawValue == rawTypeString }) + else { + return nil } + self = match } } @@ -204,6 +194,24 @@ public struct EnumCase: Codable, Equatable, Sendable { } } +extension EnumCase { + /// Generates JavaScript/TypeScript value representation for this enum case + public func jsValue(rawType: SwiftEnumRawType?, index: Int) -> String { + guard let rawType = rawType else { + return "\(index)" + } + let rawValue = self.rawValue ?? self.name + switch rawType { + case .string: + return "\"\(rawValue)\"" + case .bool: + return rawValue.lowercased() == "true" ? "true" : "false" + case .float, .double, .int, .int32, .int64, .uint, .uint32, .uint64: + return rawValue + } + } +} + public enum EnumEmitStyle: String, Codable, Sendable { case const case tsEnum @@ -214,7 +222,7 @@ public struct ExportedEnum: Codable, Equatable, Sendable { public let swiftCallName: String public let explicitAccessControl: String? public var cases: [EnumCase] - public let rawType: String? + public let rawType: SwiftEnumRawType? public let namespace: [String]? public let emitStyle: EnumEmitStyle public var staticMethods: [ExportedFunction] @@ -234,7 +242,7 @@ public struct ExportedEnum: Codable, Equatable, Sendable { swiftCallName: String, explicitAccessControl: String?, cases: [EnumCase], - rawType: String?, + rawType: SwiftEnumRawType?, namespace: [String]?, emitStyle: EnumEmitStyle, staticMethods: [ExportedFunction] = [], diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift index ad4ec7de..b670d2db 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/DefaultParameters.swift @@ -1,10 +1,10 @@ @JS public func testStringDefault(message: String = "Hello World") -> String -@JS public func testIntDefault(count: Int = 42) -> Int +@JS public func testNegativeIntDefault(value: Int = -42) -> Int @JS public func testBoolDefault(flag: Bool = true) -> Bool -@JS public func testFloatDefault(value: Float = 3.14) -> Float +@JS public func testNegativeFloatDefault(temp: Float = -273.15) -> Float @JS public func testDoubleDefault(precision: Double = 2.718) -> Double @@ -39,3 +39,36 @@ @JS public func testComplexInit(greeter: DefaultGreeter = DefaultGreeter(name: "DefaultUser")) -> DefaultGreeter @JS public func testEmptyInit(greeter: EmptyGreeter = EmptyGreeter()) -> EmptyGreeter + +@JS class ConstructorDefaults { + @JS var name: String + @JS var count: Int + @JS var enabled: Bool + @JS var status: Status + @JS var tag: String? + + @JS init( + name: String = "Default", + count: Int = 42, + enabled: Bool = true, + status: Status = .active, + tag: String? = nil + ) { + self.name = name + self.count = count + self.enabled = enabled + self.status = status + self.tag = tag + } + + @JS func describe() -> String { + let tagStr = tag ?? "nil" + let statusStr: String + switch status { + case .active: statusStr = "active" + case .inactive: statusStr = "inactive" + case .pending: statusStr = "pending" + } + return "\(name):\(count):\(enabled):\(statusStr):\(tagStr)" + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts index a7c20f3d..38cbf989 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.d.ts @@ -25,6 +25,14 @@ export interface DefaultGreeter extends SwiftHeapObject { } export interface EmptyGreeter extends SwiftHeapObject { } +export interface ConstructorDefaults extends SwiftHeapObject { + describe(): string; + name: string; + count: number; + enabled: boolean; + status: StatusTag; + tag: string | null; +} export type Exports = { DefaultGreeter: { new(name: string): DefaultGreeter; @@ -32,22 +40,32 @@ export type Exports = { EmptyGreeter: { new(): EmptyGreeter; } + ConstructorDefaults: { + /** + * @param name - Optional parameter (default: "Default") + * @param count - Optional parameter (default: 42) + * @param enabled - Optional parameter (default: true) + * @param status - Optional parameter (default: Status.Active) + * @param tag - Optional parameter (default: null) + */ + new(name?: string, count?: number, enabled?: boolean, status?: StatusTag, tag?: string | null): ConstructorDefaults; + } /** * @param message - Optional parameter (default: "Hello World") */ testStringDefault(message?: string): string; /** - * @param count - Optional parameter (default: 42) + * @param value - Optional parameter (default: -42) */ - testIntDefault(count?: number): number; + testNegativeIntDefault(value?: number): number; /** * @param flag - Optional parameter (default: true) */ testBoolDefault(flag?: boolean): boolean; /** - * @param value - Optional parameter (default: 3.14) + * @param temp - Optional parameter (default: -273.15) */ - testFloatDefault(value?: number): number; + testNegativeFloatDefault(temp?: number): number; /** * @param precision - Optional parameter (default: 2.718) */ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js index 31e746be..dac7bdfc 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.Export.js @@ -156,6 +156,10 @@ export async function createInstantiator(options, swift) { const obj = EmptyGreeter.__construct(pointer); return swift.memory.retain(obj); }; + importObject["TestModule"]["bjs_ConstructorDefaults_wrap"] = function(pointer) { + const obj = ConstructorDefaults.__construct(pointer); + return swift.memory.retain(obj); + }; }, setInstance: (i) => { instance = i; @@ -222,13 +226,90 @@ export async function createInstantiator(options, swift) { return EmptyGreeter.__construct(ret); } } + class ConstructorDefaults extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ConstructorDefaults_deinit, ConstructorDefaults.prototype); + } + + constructor(name = "Default", count = 42, enabled = true, status = StatusValues.Active, tag = null) { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const isSome = tag != null; + let tagId, tagBytes; + if (isSome) { + tagBytes = textEncoder.encode(tag); + tagId = swift.memory.retain(tagBytes); + } + const ret = instance.exports.bjs_ConstructorDefaults_init(nameId, nameBytes.length, count, enabled, status, +isSome, isSome ? tagId : 0, isSome ? tagBytes.length : 0); + swift.memory.release(nameId); + if (tagId != undefined) { + swift.memory.release(tagId); + } + return ConstructorDefaults.__construct(ret); + } + describe() { + instance.exports.bjs_ConstructorDefaults_describe(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + get name() { + instance.exports.bjs_ConstructorDefaults_name_get(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + set name(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_ConstructorDefaults_name_set(this.pointer, valueId, valueBytes.length); + swift.memory.release(valueId); + } + get count() { + const ret = instance.exports.bjs_ConstructorDefaults_count_get(this.pointer); + return ret; + } + set count(value) { + instance.exports.bjs_ConstructorDefaults_count_set(this.pointer, value); + } + get enabled() { + const ret = instance.exports.bjs_ConstructorDefaults_enabled_get(this.pointer); + return ret !== 0; + } + set enabled(value) { + instance.exports.bjs_ConstructorDefaults_enabled_set(this.pointer, value); + } + get status() { + const ret = instance.exports.bjs_ConstructorDefaults_status_get(this.pointer); + return ret; + } + set status(value) { + instance.exports.bjs_ConstructorDefaults_status_set(this.pointer, value); + } + get tag() { + instance.exports.bjs_ConstructorDefaults_tag_get(this.pointer); + const optResult = tmpRetString; + tmpRetString = undefined; + return optResult; + } + set tag(value) { + const isSome = value != null; + let valueId, valueBytes; + if (isSome) { + valueBytes = textEncoder.encode(value); + valueId = swift.memory.retain(valueBytes); + } + instance.exports.bjs_ConstructorDefaults_tag_set(this.pointer, +isSome, isSome ? valueId : 0, isSome ? valueBytes.length : 0); + if (valueId != undefined) { + swift.memory.release(valueId); + } + } + } return { DefaultGreeter, EmptyGreeter, - testStringDefault: function bjs_testStringDefault(message) { - if (message === undefined) { - message = "Hello World"; - } + ConstructorDefaults, + testStringDefault: function bjs_testStringDefault(message = "Hello World") { const messageBytes = textEncoder.encode(message); const messageId = swift.memory.retain(messageBytes); instance.exports.bjs_testStringDefault(messageId, messageBytes.length); @@ -237,38 +318,23 @@ export async function createInstantiator(options, swift) { swift.memory.release(messageId); return ret; }, - testIntDefault: function bjs_testIntDefault(count) { - if (count === undefined) { - count = 42; - } - const ret = instance.exports.bjs_testIntDefault(count); + testNegativeIntDefault: function bjs_testNegativeIntDefault(value = -42) { + const ret = instance.exports.bjs_testNegativeIntDefault(value); return ret; }, - testBoolDefault: function bjs_testBoolDefault(flag) { - if (flag === undefined) { - flag = true; - } + testBoolDefault: function bjs_testBoolDefault(flag = true) { const ret = instance.exports.bjs_testBoolDefault(flag); return ret !== 0; }, - testFloatDefault: function bjs_testFloatDefault(value) { - if (value === undefined) { - value = 3.14; - } - const ret = instance.exports.bjs_testFloatDefault(value); + testNegativeFloatDefault: function bjs_testNegativeFloatDefault(temp = -273.15) { + const ret = instance.exports.bjs_testNegativeFloatDefault(temp); return ret; }, - testDoubleDefault: function bjs_testDoubleDefault(precision) { - if (precision === undefined) { - precision = 2.718; - } + testDoubleDefault: function bjs_testDoubleDefault(precision = 2.718) { const ret = instance.exports.bjs_testDoubleDefault(precision); return ret; }, - testOptionalDefault: function bjs_testOptionalDefault(name) { - if (name === undefined) { - name = null; - } + testOptionalDefault: function bjs_testOptionalDefault(name = null) { const isSome = name != null; let nameId, nameBytes; if (isSome) { @@ -283,10 +349,7 @@ export async function createInstantiator(options, swift) { } return optResult; }, - testOptionalStringDefault: function bjs_testOptionalStringDefault(greeting) { - if (greeting === undefined) { - greeting = "Hi"; - } + testOptionalStringDefault: function bjs_testOptionalStringDefault(greeting = "Hi") { const isSome = greeting != null; let greetingId, greetingBytes; if (isSome) { @@ -301,16 +364,7 @@ export async function createInstantiator(options, swift) { } return optResult; }, - testMultipleDefaults: function bjs_testMultipleDefaults(title, count, enabled) { - if (title === undefined) { - title = "Default Title"; - } - if (count === undefined) { - count = 10; - } - if (enabled === undefined) { - enabled = false; - } + testMultipleDefaults: function bjs_testMultipleDefaults(title = "Default Title", count = 10, enabled = false) { const titleBytes = textEncoder.encode(title); const titleId = swift.memory.retain(titleBytes); instance.exports.bjs_testMultipleDefaults(titleId, titleBytes.length, count, enabled); @@ -319,24 +373,15 @@ export async function createInstantiator(options, swift) { swift.memory.release(titleId); return ret; }, - testEnumDefault: function bjs_testEnumDefault(status) { - if (status === undefined) { - status = StatusValues.Active; - } + testEnumDefault: function bjs_testEnumDefault(status = StatusValues.Active) { const ret = instance.exports.bjs_testEnumDefault(status); return ret; }, - testComplexInit: function bjs_testComplexInit(greeter) { - if (greeter === undefined) { - greeter = new DefaultGreeter("DefaultUser"); - } + testComplexInit: function bjs_testComplexInit(greeter = new DefaultGreeter("DefaultUser")) { const ret = instance.exports.bjs_testComplexInit(greeter.pointer); return DefaultGreeter.__construct(ret); }, - testEmptyInit: function bjs_testEmptyInit(greeter) { - if (greeter === undefined) { - greeter = new EmptyGreeter(); - } + testEmptyInit: function bjs_testEmptyInit(greeter = new EmptyGreeter()) { const ret = instance.exports.bjs_testEmptyInit(greeter.pointer); return EmptyGreeter.__construct(ret); }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json index e29ec6a1..edfd4e57 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json @@ -58,6 +58,170 @@ ], "swiftCallName" : "EmptyGreeter" + }, + { + "constructor" : { + "abiName" : "bjs_ConstructorDefaults_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Default" + } + }, + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "defaultValue" : { + "int" : { + "_0" : 42 + } + }, + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + }, + { + "defaultValue" : { + "bool" : { + "_0" : true + } + }, + "label" : "enabled", + "name" : "enabled", + "type" : { + "bool" : { + + } + } + }, + { + "defaultValue" : { + "enumCase" : { + "_0" : "Status", + "_1" : "active" + } + }, + "label" : "status", + "name" : "status", + "type" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "defaultValue" : { + "null" : { + + } + }, + "label" : "tag", + "name" : "tag", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_ConstructorDefaults_describe", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describe", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "ConstructorDefaults", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "count", + "type" : { + "int" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "enabled", + "type" : { + "bool" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "status", + "type" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "tag", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ], + "swiftCallName" : "ConstructorDefaults" } ], "enums" : [ @@ -126,22 +290,22 @@ } }, { - "abiName" : "bjs_testIntDefault", + "abiName" : "bjs_testNegativeIntDefault", "effects" : { "isAsync" : false, "isStatic" : false, "isThrows" : false }, - "name" : "testIntDefault", + "name" : "testNegativeIntDefault", "parameters" : [ { "defaultValue" : { "int" : { - "_0" : 42 + "_0" : -42 } }, - "label" : "count", - "name" : "count", + "label" : "value", + "name" : "value", "type" : { "int" : { @@ -186,22 +350,22 @@ } }, { - "abiName" : "bjs_testFloatDefault", + "abiName" : "bjs_testNegativeFloatDefault", "effects" : { "isAsync" : false, "isStatic" : false, "isThrows" : false }, - "name" : "testFloatDefault", + "name" : "testNegativeFloatDefault", "parameters" : [ { "defaultValue" : { "float" : { - "_0" : 3.14 + "_0" : -273.15 } }, - "label" : "value", - "name" : "value", + "label" : "temp", + "name" : "temp", "type" : { "float" : { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift index d20df871..fd382c0a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.swift @@ -56,11 +56,11 @@ public func _bjs_testStringDefault(messageBytes: Int32, messageLength: Int32) -> #endif } -@_expose(wasm, "bjs_testIntDefault") -@_cdecl("bjs_testIntDefault") -public func _bjs_testIntDefault(count: Int32) -> Int32 { +@_expose(wasm, "bjs_testNegativeIntDefault") +@_cdecl("bjs_testNegativeIntDefault") +public func _bjs_testNegativeIntDefault(value: Int32) -> Int32 { #if arch(wasm32) - let ret = testIntDefault(count: Int.bridgeJSLiftParameter(count)) + let ret = testNegativeIntDefault(value: Int.bridgeJSLiftParameter(value)) return ret.bridgeJSLowerReturn() #else fatalError("Only available on WebAssembly") @@ -78,11 +78,11 @@ public func _bjs_testBoolDefault(flag: Int32) -> Int32 { #endif } -@_expose(wasm, "bjs_testFloatDefault") -@_cdecl("bjs_testFloatDefault") -public func _bjs_testFloatDefault(value: Float32) -> Float32 { +@_expose(wasm, "bjs_testNegativeFloatDefault") +@_cdecl("bjs_testNegativeFloatDefault") +public func _bjs_testNegativeFloatDefault(temp: Float32) -> Float32 { #if arch(wasm32) - let ret = testFloatDefault(value: Float.bridgeJSLiftParameter(value)) + let ret = testNegativeFloatDefault(temp: Float.bridgeJSLiftParameter(temp)) return ret.bridgeJSLowerReturn() #else fatalError("Only available on WebAssembly") @@ -247,4 +247,151 @@ extension EmptyGreeter: ConvertibleToJSValue, _BridgedSwiftHeapObject { #endif return .object(JSObject(id: UInt32(bitPattern: _bjs_EmptyGreeter_wrap(Unmanaged.passRetained(self).toOpaque())))) } +} + +@_expose(wasm, "bjs_ConstructorDefaults_init") +@_cdecl("bjs_ConstructorDefaults_init") +public func _bjs_ConstructorDefaults_init(nameBytes: Int32, nameLength: Int32, count: Int32, enabled: Int32, status: Int32, tagIsSome: Int32, tagBytes: Int32, tagLength: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ConstructorDefaults(name: String.bridgeJSLiftParameter(nameBytes, nameLength), count: Int.bridgeJSLiftParameter(count), enabled: Bool.bridgeJSLiftParameter(enabled), status: Status.bridgeJSLiftParameter(status), tag: Optional.bridgeJSLiftParameter(tagIsSome, tagBytes, tagLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_describe") +@_cdecl("bjs_ConstructorDefaults_describe") +public func _bjs_ConstructorDefaults_describe(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).describe() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_name_get") +@_cdecl("bjs_ConstructorDefaults_name_get") +public func _bjs_ConstructorDefaults_name_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).name + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_name_set") +@_cdecl("bjs_ConstructorDefaults_name_set") +public func _bjs_ConstructorDefaults_name_set(_self: UnsafeMutableRawPointer, valueBytes: Int32, valueLength: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).name = String.bridgeJSLiftParameter(valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_count_get") +@_cdecl("bjs_ConstructorDefaults_count_get") +public func _bjs_ConstructorDefaults_count_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).count + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_count_set") +@_cdecl("bjs_ConstructorDefaults_count_set") +public func _bjs_ConstructorDefaults_count_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).count = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_enabled_get") +@_cdecl("bjs_ConstructorDefaults_enabled_get") +public func _bjs_ConstructorDefaults_enabled_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).enabled + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_enabled_set") +@_cdecl("bjs_ConstructorDefaults_enabled_set") +public func _bjs_ConstructorDefaults_enabled_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).enabled = Bool.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_status_get") +@_cdecl("bjs_ConstructorDefaults_status_get") +public func _bjs_ConstructorDefaults_status_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).status + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_status_set") +@_cdecl("bjs_ConstructorDefaults_status_set") +public func _bjs_ConstructorDefaults_status_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).status = Status.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_tag_get") +@_cdecl("bjs_ConstructorDefaults_tag_get") +public func _bjs_ConstructorDefaults_tag_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).tag + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_tag_set") +@_cdecl("bjs_ConstructorDefaults_tag_set") +public func _bjs_ConstructorDefaults_tag_set(_self: UnsafeMutableRawPointer, valueIsSome: Int32, valueBytes: Int32, valueLength: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).tag = Optional.bridgeJSLiftParameter(valueIsSome, valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_deinit") +@_cdecl("bjs_ConstructorDefaults_deinit") +public func _bjs_ConstructorDefaults_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension ConstructorDefaults: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "TestModule", name: "bjs_ConstructorDefaults_wrap") + func _bjs_ConstructorDefaults_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_ConstructorDefaults_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_ConstructorDefaults_wrap(Unmanaged.passRetained(self).toOpaque())))) + } } \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md index 238829db..11574f0d 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md @@ -1,12 +1,12 @@ # Default Parameters in Exported Swift Functions -Learn how to use default parameter values in Swift functions exported to JavaScript. +Learn how to use default parameter values in Swift functions and constructors exported to JavaScript. ## Overview > Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). -BridgeJS supports default parameter values for Swift functions exported to JavaScript. When you specify default values in your Swift code, they are automatically applied in the generated JavaScript bindings. +BridgeJS supports default parameter values for Swift functions and class constructors exported to JavaScript. When you specify default values in your Swift code, they are automatically applied in the generated JavaScript bindings. ```swift import JavaScriptKit @@ -42,7 +42,7 @@ export type Exports = { To use a default value for a middle parameter while providing later parameters, pass `undefined`: ```swift -@JS public func configure(title: String = "Default", count: Int = 10, enabled: Bool = false) -> String { +@JS public func configure(title: String = "Default", count: Int = -10, enabled: Bool = false) -> String { return "\(title): \(count) (\(enabled))" } ``` @@ -50,19 +50,53 @@ To use a default value for a middle parameter while providing later parameters, ```javascript // Use all defaults exports.configure(); // "Default: 10 (false)" +exports.configure("Custom"); // "Custom: -10 (false)" +exports.configure("Custom", undefined, true); // "Custom: -10 (true)" +exports.configure("Custom", 5, true); // "Custom: 5 (true)" +``` -// Provide first parameter only -exports.configure("Custom"); // "Custom: 10 (false)" +## Default Parameters in Constructors -// Skip middle parameter with undefined -exports.configure("Custom", undefined, true); // "Custom: 10 (true)" +Constructor parameters also support default values, making it easy to create instances with flexible initialization options: -// Provide all parameters -exports.configure("Custom", 5, true); // "Custom: 5 (true)" +```swift +@JS class Config { + @JS var name: String + @JS var timeout: Int + @JS var retries: Int + + @JS init(name: String = "default", timeout: Int = 30, retries: Int = 3) { + self.name = name + self.timeout = timeout + self.retries = retries + } +} +``` + +In JavaScript, you can omit constructor parameters or use `undefined` to skip them: + +```javascript +const c1 = new exports.Config(); // name: "default", timeout: 30, retries: 3 +const c2 = new exports.Config("custom"); // name: "custom", timeout: 30, retries: 3 +const c3 = new exports.Config("api", 60); // name: "api", timeout: 60, retries: 3 +const c4 = new exports.Config("api", undefined, 5); // name: "api", timeout: 30, retries: 5 +``` + +The generated TypeScript definitions include JSDoc comments for constructor parameters: + +```typescript +/** + * @param name - Optional parameter (default: "default") + * @param timeout - Optional parameter (default: 30) + * @param retries - Optional parameter (default: 3) + */ +new(name?: string, timeout?: number, retries?: number): Config; ``` ## Supported Default Value Types +The following default value types are supported for both function and constructor parameters: + | Default Value Type | Swift Example | JavaScript/TypeScript | |:-------------------|:-------------|:----------------------| | String literals | `"hello"` | `"hello"` | @@ -97,10 +131,8 @@ You can use class initialization expressions as default values: In JavaScript: ```javascript -// Uses default Config instance exports.process(); // "Using: default" -// Provides custom Config instance const custom = new exports.Config("custom"); exports.process(custom); // "Using: custom" custom.release(); @@ -125,12 +157,3 @@ The following expressions are **not supported** as default parameter values: | Complex member access | `Config.shared.value` | ❌ | | Ternary operators | `flag ? "a" : "b"` | ❌ | | Object init with complex args | `Config(setting: getValue())` | ❌ | - -When a complex default value is needed, make the parameter optional and handle the default in the function body: - -```swift -@JS public func process(config: Config? = nil) -> String { - let actualConfig = config ?? createDefaultConfig() - return actualConfig.process() -} -``` diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index e766d2c6..a5959e14 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -720,7 +720,7 @@ enum APIOptionalResult { @JS func testMultipleDefaults( title: String = "Default Title", - count: Int = 10, + count: Int = -10, enabled: Bool = false ) -> String { return "\(title): \(count) (\(enabled))" @@ -746,6 +746,39 @@ enum APIOptionalResult { return object } +@JS class ConstructorDefaults { + @JS var name: String + @JS var count: Int + @JS var enabled: Bool + @JS var status: Status + @JS var tag: String? + + @JS init( + name: String = "Default", + count: Int = 42, + enabled: Bool = true, + status: Status = .success, + tag: String? = nil + ) { + self.name = name + self.count = count + self.enabled = enabled + self.status = status + self.tag = tag + } + + @JS func describe() -> String { + let tagStr = tag ?? "nil" + let statusStr: String + switch status { + case .loading: statusStr = "loading" + case .success: statusStr = "success" + case .error: statusStr = "error" + } + return "\(name):\(count):\(enabled):\(statusStr):\(tagStr)" + } +} + // MARK: - Static Properties @JS class StaticPropertyHolder { diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift index 7504d20d..0eff4e0b 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift @@ -2855,6 +2855,153 @@ extension MathUtils: ConvertibleToJSValue, _BridgedSwiftHeapObject { } } +@_expose(wasm, "bjs_ConstructorDefaults_init") +@_cdecl("bjs_ConstructorDefaults_init") +public func _bjs_ConstructorDefaults_init(nameBytes: Int32, nameLength: Int32, count: Int32, enabled: Int32, status: Int32, tagIsSome: Int32, tagBytes: Int32, tagLength: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ConstructorDefaults(name: String.bridgeJSLiftParameter(nameBytes, nameLength), count: Int.bridgeJSLiftParameter(count), enabled: Bool.bridgeJSLiftParameter(enabled), status: Status.bridgeJSLiftParameter(status), tag: Optional.bridgeJSLiftParameter(tagIsSome, tagBytes, tagLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_describe") +@_cdecl("bjs_ConstructorDefaults_describe") +public func _bjs_ConstructorDefaults_describe(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).describe() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_name_get") +@_cdecl("bjs_ConstructorDefaults_name_get") +public func _bjs_ConstructorDefaults_name_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).name + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_name_set") +@_cdecl("bjs_ConstructorDefaults_name_set") +public func _bjs_ConstructorDefaults_name_set(_self: UnsafeMutableRawPointer, valueBytes: Int32, valueLength: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).name = String.bridgeJSLiftParameter(valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_count_get") +@_cdecl("bjs_ConstructorDefaults_count_get") +public func _bjs_ConstructorDefaults_count_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).count + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_count_set") +@_cdecl("bjs_ConstructorDefaults_count_set") +public func _bjs_ConstructorDefaults_count_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).count = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_enabled_get") +@_cdecl("bjs_ConstructorDefaults_enabled_get") +public func _bjs_ConstructorDefaults_enabled_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).enabled + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_enabled_set") +@_cdecl("bjs_ConstructorDefaults_enabled_set") +public func _bjs_ConstructorDefaults_enabled_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).enabled = Bool.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_status_get") +@_cdecl("bjs_ConstructorDefaults_status_get") +public func _bjs_ConstructorDefaults_status_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).status + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_status_set") +@_cdecl("bjs_ConstructorDefaults_status_set") +public func _bjs_ConstructorDefaults_status_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).status = Status.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_tag_get") +@_cdecl("bjs_ConstructorDefaults_tag_get") +public func _bjs_ConstructorDefaults_tag_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ConstructorDefaults.bridgeJSLiftParameter(_self).tag + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_tag_set") +@_cdecl("bjs_ConstructorDefaults_tag_set") +public func _bjs_ConstructorDefaults_tag_set(_self: UnsafeMutableRawPointer, valueIsSome: Int32, valueBytes: Int32, valueLength: Int32) -> Void { + #if arch(wasm32) + ConstructorDefaults.bridgeJSLiftParameter(_self).tag = Optional.bridgeJSLiftParameter(valueIsSome, valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ConstructorDefaults_deinit") +@_cdecl("bjs_ConstructorDefaults_deinit") +public func _bjs_ConstructorDefaults_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension ConstructorDefaults: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_ConstructorDefaults_wrap") + func _bjs_ConstructorDefaults_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_ConstructorDefaults_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_ConstructorDefaults_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + @_expose(wasm, "bjs_StaticPropertyHolder_init") @_cdecl("bjs_StaticPropertyHolder_init") public func _bjs_StaticPropertyHolder_init() -> UnsafeMutableRawPointer { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json index 0cfea50d..069b4332 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -806,6 +806,170 @@ ], "swiftCallName" : "MathUtils" }, + { + "constructor" : { + "abiName" : "bjs_ConstructorDefaults_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "defaultValue" : { + "string" : { + "_0" : "Default" + } + }, + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "defaultValue" : { + "int" : { + "_0" : 42 + } + }, + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + }, + { + "defaultValue" : { + "bool" : { + "_0" : true + } + }, + "label" : "enabled", + "name" : "enabled", + "type" : { + "bool" : { + + } + } + }, + { + "defaultValue" : { + "enumCase" : { + "_0" : "Status", + "_1" : "success" + } + }, + "label" : "status", + "name" : "status", + "type" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "defaultValue" : { + "null" : { + + } + }, + "label" : "tag", + "name" : "tag", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_ConstructorDefaults_describe", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describe", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "ConstructorDefaults", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "count", + "type" : { + "int" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "enabled", + "type" : { + "bool" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "status", + "type" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "tag", + "type" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ], + "swiftCallName" : "ConstructorDefaults" + }, { "constructor" : { "abiName" : "bjs_StaticPropertyHolder_init", @@ -5052,7 +5216,7 @@ { "defaultValue" : { "int" : { - "_0" : 10 + "_0" : -10 } }, "label" : "count", diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index e080397f..32560659 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -656,10 +656,10 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.testOptionalDefault(), null); assert.equal(exports.testOptionalDefault("Test"), "Test"); - assert.equal(exports.testMultipleDefaults(), "Default Title: 10 (false)"); - assert.equal(exports.testMultipleDefaults("Custom"), "Custom: 10 (false)"); + assert.equal(exports.testMultipleDefaults(), "Default Title: -10 (false)"); + assert.equal(exports.testMultipleDefaults("Custom"), "Custom: -10 (false)"); assert.equal(exports.testMultipleDefaults("Custom", 5), "Custom: 5 (false)"); - assert.equal(exports.testMultipleDefaults("Custom", undefined, true), "Custom: 10 (true)"); + assert.equal(exports.testMultipleDefaults("Custom", undefined, true), "Custom: -10 (true)"); assert.equal(exports.testMultipleDefaults("Custom", 5, true), "Custom: 5 (true)"); assert.equal(exports.testSimpleEnumDefault(), exports.Status.Success); @@ -683,6 +683,26 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { const customGreeter = new exports.Greeter("CustomName"); assert.equal(exports.testComplexInit(customGreeter), "Hello, CustomName!"); customGreeter.release(); + + const cd1 = new exports.ConstructorDefaults(); + assert.equal(cd1.describe(), "Default:42:true:success:nil"); + cd1.release(); + + const cd2 = new exports.ConstructorDefaults("Custom"); + assert.equal(cd2.describe(), "Custom:42:true:success:nil"); + cd2.release(); + + const cd3 = new exports.ConstructorDefaults("Custom", 100); + assert.equal(cd3.describe(), "Custom:100:true:success:nil"); + cd3.release(); + + const cd4 = new exports.ConstructorDefaults("Custom", undefined, false); + assert.equal(cd4.describe(), "Custom:42:false:success:nil"); + cd4.release(); + + const cd5 = new exports.ConstructorDefaults("Test", 99, false, exports.Status.Loading); + assert.equal(cd5.describe(), "Test:99:false:loading:nil"); + cd5.release(); } /** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */