From d03e74caed1c6d9070b17a06d4eefafe28e60b94 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 5 Aug 2025 22:21:27 -0600 Subject: [PATCH 1/4] BridgeJS: Support for case / raw type Swift -> TS code generation --- .../Sources/BridgeJSCore/ExportSwift.swift | 304 +++++- .../Sources/BridgeJSCore/ImportTS.swift | 4 + .../Sources/BridgeJSLink/BridgeJSLink.swift | 188 ++++ .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 132 ++- .../TS2Skeleton/JavaScript/package-lock.json | 28 + .../TS2Skeleton/JavaScript/package.json | 2 +- .../BridgeJSToolTests/Inputs/EnumCase.swift | 16 + .../Inputs/EnumNamespace.swift | 36 + .../Inputs/EnumRawType.swift | 104 ++ .../BridgeJSToolTests/Inputs/tsconfig.json | 11 + .../ArrayParameter.Import.js | 1 + .../BridgeJSLinkTests/Async.Export.js | 1 + .../BridgeJSLinkTests/Async.Import.js | 1 + .../BridgeJSLinkTests/EnumCase.Export.d.ts | 35 + .../BridgeJSLinkTests/EnumCase.Export.js | 96 ++ .../EnumNamespace.Export.d.ts | 20 + .../BridgeJSLinkTests/EnumNamespace.Export.js | 76 ++ .../BridgeJSLinkTests/EnumRawType.Export.d.ts | 121 +++ .../BridgeJSLinkTests/EnumRawType.Export.js | 248 +++++ .../BridgeJSLinkTests/Interface.Import.js | 1 + .../MultipleImportedTypes.Import.js | 1 + .../BridgeJSLinkTests/Namespaces.Export.js | 1 + .../PrimitiveParameters.Export.js | 1 + .../PrimitiveParameters.Import.js | 1 + .../PrimitiveReturn.Export.js | 1 + .../PrimitiveReturn.Import.js | 1 + .../StringParameter.Export.js | 1 + .../StringParameter.Import.js | 1 + .../BridgeJSLinkTests/StringReturn.Export.js | 1 + .../BridgeJSLinkTests/StringReturn.Import.js | 1 + .../BridgeJSLinkTests/SwiftClass.Export.js | 1 + .../TS2SkeletonLike.Import.js | 1 + .../BridgeJSLinkTests/Throws.Export.js | 1 + .../BridgeJSLinkTests/TypeAlias.Import.js | 1 + .../TypeScriptClass.Import.js | 1 + .../VoidParameterVoidReturn.Export.js | 1 + .../VoidParameterVoidReturn.Import.js | 1 + .../__Snapshots__/ExportSwiftTests/Async.json | 3 + .../ExportSwiftTests/EnumCase.json | 132 +++ .../ExportSwiftTests/EnumCase.swift | 39 + .../ExportSwiftTests/EnumNamespace.json | 39 + .../ExportSwiftTests/EnumNamespace.swift | 7 + .../ExportSwiftTests/EnumRawType.json | 894 ++++++++++++++++++ .../ExportSwiftTests/EnumRawType.swift | 283 ++++++ .../ExportSwiftTests/Namespaces.json | 3 + .../ExportSwiftTests/PrimitiveParameters.json | 3 + .../ExportSwiftTests/PrimitiveReturn.json | 3 + .../ExportSwiftTests/StringParameter.json | 3 + .../ExportSwiftTests/StringReturn.json | 3 + .../ExportSwiftTests/SwiftClass.json | 3 + .../ExportSwiftTests/Throws.json | 3 + .../VoidParameterVoidReturn.json | 3 + 52 files changed, 2859 insertions(+), 4 deletions(-) create mode 100644 Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/tsconfig.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index e928011a5..fba27aa1b 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -20,6 +20,7 @@ public class ExportSwift { private var exportedFunctions: [ExportedFunction] = [] private var exportedClasses: [ExportedClass] = [] + private var exportedEnums: [ExportedEnum] = [] private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver() public init(progress: ProgressReporting, moduleName: String) { @@ -58,7 +59,8 @@ public class ExportSwift { outputSkeleton: ExportedSkeleton( moduleName: moduleName, functions: exportedFunctions, - classes: exportedClasses + classes: exportedClasses, + enums: exportedEnums ) ) } @@ -68,6 +70,9 @@ public class ExportSwift { /// The names of the exported classes, in the order they were written in the source file var exportedClassNames: [String] = [] var exportedClassByName: [String: ExportedClass] = [:] + /// The names of the exported enums, in the order they were written in the source file + var exportedEnumNames: [String] = [] + var exportedEnumByName: [String: ExportedEnum] = [:] var errors: [DiagnosticError] = [] enum State { @@ -292,6 +297,94 @@ public class ExportSwift { override func visitPost(_ node: ClassDeclSyntax) { stateStack.pop() } + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + guard let jsAttribute = node.attributes.firstJSAttribute else { + return .skipChildren + } + + let name = node.name.text + let namespace = extractNamespace(from: jsAttribute) + + if let exportedEnum = parseEnum(node: node, namespace: namespace) { + exportedEnumByName[name] = exportedEnum + exportedEnumNames.append(name) + } + + return .skipChildren + } + + private func parseEnum(node: EnumDeclSyntax, namespace: [String]?) -> ExportedEnum? { + let name = node.name.text + + let rawType: String? = node.inheritanceClause?.inheritedTypes.first { inheritedType in + let typeName = inheritedType.type.trimmedDescription + return Constants.supportedRawTypes.contains(typeName) + }?.type.trimmedDescription + + var cases: [EnumCase] = [] + var nestedTypes: [String] = [] + + for member in node.memberBlock.members { + if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { + for element in caseDecl.elements { + let caseName = element.name.text + let rawValue: String? + if let stringLiteral = element.rawValue?.value.as(StringLiteralExprSyntax.self) { + rawValue = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text + } else if let boolLiteral = element.rawValue?.value.as(BooleanLiteralExprSyntax.self) { + rawValue = boolLiteral.literal.text + } else if let intLiteral = element.rawValue?.value.as(IntegerLiteralExprSyntax.self) { + rawValue = intLiteral.literal.text + } else if let floatLiteral = element.rawValue?.value.as(FloatLiteralExprSyntax.self) { + rawValue = floatLiteral.literal.text + } else { + // Other unsupported type or no raw value + rawValue = nil + } + + var associatedValues: [AssociatedValue] = [] + if let parameterClause = element.parameterClause { + for param in parameterClause.parameters { + guard let bridgeType = parent.lookupType(for: param.type) else { + diagnose( + node: param.type, + message: "Unsupported associated value type: \(param.type.trimmedDescription)", + hint: "Only primitive types and types defined in the same module are allowed" + ) + continue + } + + let label = param.firstName?.text + associatedValues.append(AssociatedValue(label: label, type: bridgeType)) + } + } + + cases.append( + EnumCase( + name: caseName, + rawValue: rawValue, + associatedValues: associatedValues + ) + ) + } + } else if let nestedEnum = member.decl.as(EnumDeclSyntax.self) { + // Track nested enums for namespace enums + nestedTypes.append(nestedEnum.name.text) + } else if let nestedClass = member.decl.as(ClassDeclSyntax.self) { + // Track nested classes for namespace enums + nestedTypes.append(nestedClass.name.text) + } + } + + return ExportedEnum( + name: name, + cases: cases, + rawType: rawType, + namespace: namespace, + nestedTypes: nestedTypes + ) + } } func parseSingleFile(_ sourceFile: SourceFileSyntax) throws -> [DiagnosticError] { @@ -303,6 +396,11 @@ public class ExportSwift { collector.exportedClassByName[$0]! } ) + exportedEnums.append( + contentsOf: collector.exportedEnumNames.map { + collector.exportedEnumByName[$0]! + } + ) return collector.errors } @@ -310,12 +408,20 @@ public class ExportSwift { if let primitive = BridgeType(swiftType: type.trimmedDescription) { return primitive } + guard let identifier = type.as(IdentifierTypeSyntax.self) else { return nil } + guard let typeDecl = typeDeclResolver.lookupType(for: identifier) else { return nil } + + // Check if it's an enum + if let enumDecl = typeDecl.as(EnumDeclSyntax.self) { + return classifyEnumType(enumDecl) + } + guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else { return nil } @@ -334,7 +440,7 @@ public class ExportSwift { func renderSwiftGlue() -> String? { var decls: [DeclSyntax] = [] - guard exportedFunctions.count > 0 || exportedClasses.count > 0 else { + guard exportedFunctions.count > 0 || exportedClasses.count > 0 || exportedEnums.count > 0 else { return nil } decls.append(Self.prelude) @@ -344,10 +450,53 @@ public class ExportSwift { for klass in exportedClasses { decls.append(contentsOf: renderSingleExportedClass(klass: klass)) } + // Note: Enums don't need Swift glue code generation - they're used directly let format = BasicFormat() return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") } + /// Classifies an enum declaration into the appropriate BridgeType + private func classifyEnumType(_ enumDecl: EnumDeclSyntax) -> BridgeType? { + let enumName = enumDecl.name.text + + // Check for raw value type + let rawType = enumDecl.inheritanceClause?.inheritedTypes.first { inheritedType in + let typeName = inheritedType.type.trimmedDescription + return Constants.supportedRawTypes.contains(typeName) + }?.type.trimmedDescription + + // Count cases and check for associated values + var hasCases = false + var hasAssociatedValues = false + + for member in enumDecl.memberBlock.members { + if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { + hasCases = true + for element in caseDecl.elements { + if element.parameterClause != nil { + hasAssociatedValues = true + break + } + } + if hasAssociatedValues { break } + } + } + + // Classify based on structure + if !hasCases { + // Empty enum - used as namespace + return .namespaceEnum(enumName) + } else if hasAssociatedValues { + // Has associated values + return .associatedValueEnum(enumName) + } else if let rawType = rawType { + // Has raw values + return .rawValueEnum(enumName, rawType) + } else { + return .caseEnum(enumName) + } + } + class ExportedThunkBuilder { var body: [CodeBlockItemSyntax] = [] var abiParameterForwardings: [LabeledExprSyntax] = [] @@ -420,6 +569,92 @@ public class ExportSwift { ) abiParameterSignatures.append((bytesLabel, .i32)) abiParameterSignatures.append((lengthLabel, .i32)) + case .caseEnum(let enumName): + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax("\(raw: enumName)(rawValue: Int(\(raw: param.name)))!") + ) + ) + abiParameterSignatures.append((param.name, .i32)) + case .rawValueEnum(let enumName, let rawType): + if rawType == "String" { + let bytesLabel = "\(param.name)Bytes" + let lengthLabel = "\(param.name)Len" + let prepare: CodeBlockItemSyntax = """ + let \(raw: param.name) = String(unsafeUninitializedCapacity: Int(\(raw: lengthLabel))) { b in + _swift_js_init_memory(\(raw: bytesLabel), b.baseAddress.unsafelyUnwrapped) + return Int(\(raw: lengthLabel)) + } + """ + append(prepare) + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax("\(raw: enumName)(rawValue: \(raw: param.name))!") + ) + ) + abiParameterSignatures.append((bytesLabel, .i32)) + abiParameterSignatures.append((lengthLabel, .i32)) + } else { + // Numeric raw types - use correct WASM ABI type + let conversionExpr: String + switch rawType { + case "Bool": + // Bool requires special conversion from Int32 (0/1) to Bool (false/true) + conversionExpr = "\(enumName)(rawValue: \(param.name) != 0)!" + case "UInt", "UInt32", "UInt64": + // Unsigned types: use bitPattern to handle potential negative Int32 values safely + if rawType == "UInt64" { + conversionExpr = "\(enumName)(rawValue: \(rawType)(bitPattern: Int64(\(param.name))))!" + } else { + conversionExpr = "\(enumName)(rawValue: \(rawType)(bitPattern: \(param.name)))!" + } + default: + // Signed integer and float types: direct conversion + conversionExpr = "\(enumName)(rawValue: \(rawType)(\(param.name)))!" + } + + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax(stringLiteral: conversionExpr) + ) + ) + switch rawType { + case "Bool", "Int", "Int32", "UInt", "UInt32": + abiParameterSignatures.append((param.name, .i32)) + case "Int64", "UInt64": + abiParameterSignatures.append((param.name, .i64)) + case "Float": + abiParameterSignatures.append((param.name, .f32)) + case "Double": + abiParameterSignatures.append((param.name, .f64)) + default: + abiParameterSignatures.append((param.name, .i32)) // Fallback + } + } + case .associatedValueEnum(let enumName): + let bytesLabel = "\(param.name)Bytes" + let lengthLabel = "\(param.name)Len" + let prepare: CodeBlockItemSyntax = """ + let \(raw: param.name)JsonString = String(unsafeUninitializedCapacity: Int(\(raw: lengthLabel))) { b in + _swift_js_init_memory(\(raw: bytesLabel), b.baseAddress.unsafelyUnwrapped) + return Int(\(raw: lengthLabel)) + } + let \(raw: param.name) = try! JSONDecoder().decode(\(raw: enumName).self, from: \(raw: param.name)JsonString.data(using: .utf8)!) + """ + append(prepare) + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax("\(raw: param.name)") + ) + ) + abiParameterSignatures.append((bytesLabel, .i32)) + abiParameterSignatures.append((lengthLabel, .i32)) + case .namespaceEnum: + break case .jsObject(nil): abiParameterForwardings.append( LabeledExprSyntax( @@ -520,6 +755,27 @@ public class ExportSwift { case .swiftHeapObject: // UnsafeMutableRawPointer is returned as an i32 pointer abiReturnType = .pointer + case .caseEnum: + abiReturnType = .i32 + case .rawValueEnum(_, let rawType): + switch rawType { + case "String": + abiReturnType = nil + case "Bool", "Int", "Int32", "UInt", "UInt32": + abiReturnType = .i32 + case "Int64", "UInt64": + abiReturnType = .i64 + case "Float": + abiReturnType = .f32 + case "Double": + abiReturnType = .f64 + default: + abiReturnType = nil + } + case .associatedValueEnum: + abiReturnType = nil + case .namespaceEnum: + abiReturnType = nil } if effects.isAsync { @@ -542,6 +798,46 @@ public class ExportSwift { } """ ) + case .caseEnum: + abiReturnType = .i32 + append("return Int32(ret.rawValue)") + case .rawValueEnum(_, let rawType): + if rawType == "String" { + append( + """ + return ret.rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + """ + ) + } else { + switch rawType { + case "Bool": + append("return Int32(ret.rawValue ? 1 : 0)") + case "Int", "Int32", "UInt", "UInt32": + append("return Int32(ret.rawValue)") + case "Int64", "UInt64": + append("return Int64(ret.rawValue)") + case "Float": + append("return Float32(ret.rawValue)") + case "Double": + append("return Float64(ret.rawValue)") + default: + append("return Int32(ret.rawValue)") // Fallback + } + } + case .associatedValueEnum: + append( + """ + let jsonData = try! JSONEncoder().encode(ret) + return String(data: jsonData, encoding: .utf8)!.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + """ + ) + case .namespaceEnum: + abiReturnType = .i32 + append("return 0") case .jsObject(nil): append( """ @@ -825,6 +1121,10 @@ extension BridgeType { case .jsObject(let name?): return name case .swiftHeapObject(let name): return name case .void: return "Void" + case .caseEnum(let name): return name + case .rawValueEnum(let name, _): return name + case .associatedValueEnum(let name): return name + case .namespaceEnum(let name): return name } } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index c7966a84e..9c4679e98 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -123,6 +123,8 @@ public struct ImportTS { ) ) abiParameterSignatures.append((param.name, .i32)) + case .caseEnum, .rawValueEnum, .associatedValueEnum, .namespaceEnum: + throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports") case .jsObject(_?): abiParameterSignatures.append((param.name, .i32)) abiParameterForwardings.append( @@ -181,6 +183,8 @@ public struct ImportTS { } """ ) + case .caseEnum, .rawValueEnum, .associatedValueEnum, .namespaceEnum: + throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports") case .jsObject(let name): abiReturnType = .i32 if let name = name { diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 1483692f1..07712a83f 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -69,6 +69,8 @@ struct BridgeJSLink { var dtsClassLines: [String] = [] var namespacedFunctions: [ExportedFunction] = [] var namespacedClasses: [ExportedClass] = [] + var enumConstantLines: [String] = [] + var dtsEnumLines: [String] = [] if exportedSkeletons.contains(where: { $0.classes.count > 0 }) { classLines.append( @@ -96,6 +98,15 @@ struct BridgeJSLink { } } + if !skeleton.enums.isEmpty { + for enumDef in skeleton.enums { + let (jsEnum, dtsEnum) = try renderExportedEnum(enumDef) + enumConstantLines.append(contentsOf: jsEnum) + exportsLines.append("\(enumDef.name),") + dtsEnumLines.append(contentsOf: dtsEnum) + } + } + for function in skeleton.functions { var (js, dts) = renderExportedFunction(function: function) @@ -135,6 +146,7 @@ struct BridgeJSLink { .map { $0.indent(count: 12) }.joined(separator: "\n") exportsSection = """ \(classLines.map { $0.indent(count: 12) }.joined(separator: "\n")) + \(enumConstantLines.map { $0.indent(count: 12) }.joined(separator: "\n")) const exports = { \(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n")) }; @@ -147,6 +159,7 @@ struct BridgeJSLink { } else { exportsSection = """ \(classLines.map { $0.indent(count: 12) }.joined(separator: "\n")) + \(enumConstantLines.map { $0.indent(count: 12) }.joined(separator: "\n")) return { \(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n")) }; @@ -227,6 +240,7 @@ struct BridgeJSLink { var dtsLines: [String] = [] dtsLines.append(contentsOf: namespaceDeclarations()) dtsLines.append(contentsOf: dtsClassLines) + dtsLines.append(contentsOf: dtsEnumLines) dtsLines.append(contentsOf: generateImportedTypeDefinitions()) dtsLines.append("export type Exports = {") dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) }) @@ -437,6 +451,29 @@ struct BridgeJSLink { cleanupLines.append("swift.memory.release(\(bytesIdLabel));") parameterForwardings.append(bytesIdLabel) parameterForwardings.append("\(bytesLabel).length") + case .caseEnum(_): + // Case enum: JavaScript receives a number (0,1,2...), pass directly to WASM + parameterForwardings.append("\(param.name) | 0") + case .rawValueEnum(_, let rawType): + switch rawType { + case "String": + let bytesLabel = "\(param.name)Bytes" + let bytesIdLabel = "\(param.name)Id" + bodyLines.append("const \(bytesLabel) = textEncoder.encode(\(param.name));") + bodyLines.append("const \(bytesIdLabel) = swift.memory.retain(\(bytesLabel));") + cleanupLines.append("swift.memory.release(\(bytesIdLabel));") + parameterForwardings.append(bytesIdLabel) + parameterForwardings.append("\(bytesLabel).length") + case "Bool": + parameterForwardings.append("\(param.name) ? 1 : 0") + default: + parameterForwardings.append("\(param.name)") + } + case .associatedValueEnum: + parameterForwardings.append("0") + parameterForwardings.append("0") + case .namespaceEnum: + break case .jsObject: parameterForwardings.append("swift.memory.retain(\(param.name))") case .swiftHeapObject: @@ -468,6 +505,30 @@ struct BridgeJSLink { bodyLines.append("const ret = tmpRetString;") bodyLines.append("tmpRetString = undefined;") returnExpr = "ret" + case .caseEnum(_): + // Case enum: WASM returns Int32, use directly as JavaScript number + bodyLines.append("const ret = \(call);") + returnExpr = "ret" + case .rawValueEnum(_, let rawType): + switch rawType { + case "String": + bodyLines.append("\(call);") + bodyLines.append("const ret = tmpRetString;") + bodyLines.append("tmpRetString = undefined;") + returnExpr = "ret" + case "Bool": + bodyLines.append("const ret = \(call);") + returnExpr = "ret !== 0" + default: + bodyLines.append("const ret = \(call);") + returnExpr = "ret" + } + case .associatedValueEnum: + bodyLines.append("\(call);") + returnExpr = "\"\"" + case .namespaceEnum: + bodyLines.append("\(call);") + returnExpr = "undefined" case .int, .float, .double: bodyLines.append("const ret = \(call);") returnExpr = "ret" @@ -541,6 +602,86 @@ struct BridgeJSLink { "(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnTypeWithEffect)" } + func renderExportedEnum(_ enumDef: ExportedEnum) throws -> (js: [String], dts: [String]) { + var jsLines: [String] = [] + var dtsLines: [String] = [] + + switch enumDef.enumType { + case .simple: + jsLines.append("const \(enumDef.name) = {") + for (index, enumCase) in enumDef.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + jsLines.append(" \(caseName): \(index),".indent(count: 0)) + } + jsLines.append("};") + jsLines.append("") + + dtsLines.append("export const \(enumDef.name): {") + for (index, enumCase) in enumDef.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append(" readonly \(caseName): \(index);") + } + dtsLines.append("};") + dtsLines.append("export type \(enumDef.name) = typeof \(enumDef.name)[keyof typeof \(enumDef.name)];") + dtsLines.append("") + case .rawValue: + guard let rawType = enumDef.rawType else { + throw BridgeJSLinkError(message: "Raw value enum \(enumDef.name) is missing rawType") + } + + jsLines.append("const \(enumDef.name) = {") + for enumCase in enumDef.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + + switch rawType { + case "String": + formattedValue = "\"\(rawValue)\"" + case "Bool": + formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": + formattedValue = rawValue + default: + formattedValue = rawValue + } + + jsLines.append("\(caseName): \(formattedValue),".indent(count: 4)) + } + jsLines.append("};") + jsLines.append("") + + dtsLines.append("export const \(enumDef.name): {") + for enumCase in enumDef.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + + switch rawType { + case "String": + formattedValue = "\"\(rawValue)\"" + case "Bool": + formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": + formattedValue = rawValue + default: + formattedValue = rawValue + } + + dtsLines.append(" readonly \(caseName): \(formattedValue);") + } + dtsLines.append("};") + dtsLines.append("export type \(enumDef.name) = typeof \(enumDef.name)[keyof typeof \(enumDef.name)];") + dtsLines.append("") + + case .associatedValue, .namespace: + jsLines.append("// TODO: Implement \(enumDef.enumType) enum: \(enumDef.name)") + dtsLines.append("// TODO: Implement \(enumDef.enumType) enum: \(enumDef.name)") + } + + return (jsLines, dtsLines) + } + func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) { let thunkBuilder = ExportedThunkBuilder(effects: function.effects) for param in function.parameters { @@ -698,6 +839,24 @@ struct BridgeJSLink { bodyLines.append("const \(stringObjectName) = swift.memory.getObject(\(param.name));") bodyLines.append("swift.memory.release(\(param.name));") parameterForwardings.append(stringObjectName) + case .caseEnum(_): + parameterForwardings.append(param.name) + case .rawValueEnum(_, let rawType): + switch rawType { + case "String": + let stringObjectName = "\(param.name)Object" + bodyLines.append("const \(stringObjectName) = swift.memory.getObject(\(param.name));") + bodyLines.append("swift.memory.release(\(param.name));") + parameterForwardings.append(stringObjectName) + case "Bool": + parameterForwardings.append("\(param.name) !== 0") + default: + parameterForwardings.append(param.name) + } + case .associatedValueEnum: + parameterForwardings.append("\"\"") + case .namespaceEnum: + break case .jsObject: parameterForwardings.append("swift.memory.getObject(\(param.name))") default: @@ -769,6 +928,22 @@ struct BridgeJSLink { case .string: bodyLines.append("tmpRetBytes = textEncoder.encode(ret);") return "tmpRetBytes.length" + case .caseEnum(_): + return "ret" + case .rawValueEnum(_, let rawType): + switch rawType { + case "String": + bodyLines.append("tmpRetBytes = textEncoder.encode(ret);") + return "tmpRetBytes.length" + case "Bool": + return "ret ? 1 : 0" + default: + return "ret" + } + case .associatedValueEnum: + return "0" + case .namespaceEnum: + return "0" case .int, .float, .double: return "ret" case .bool: @@ -947,6 +1122,11 @@ extension String { func indent(count: Int) -> String { return String(repeating: " ", count: count) + self } + + var capitalizedFirstLetter: String { + guard !isEmpty else { return self } + return prefix(1).uppercased() + dropFirst() + } } extension BridgeType { @@ -968,6 +1148,14 @@ extension BridgeType { return name ?? "any" case .swiftHeapObject(let name): return name + case .caseEnum(let name): + return name + case .rawValueEnum(let name, _): + return name + case .associatedValueEnum(let name): + return name + case .namespaceEnum(let name): + return name } } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index a3c5b4014..e1f1b3ef7 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -2,8 +2,18 @@ // MARK: - Types +public enum Constants { + public static let supportedRawTypes = [ + "String", "Bool", "Int", "Int32", "Int64", "UInt", "UInt32", "UInt64", "Float", "Double", + ] +} + public enum BridgeType: Codable, Equatable { case int, float, double, string, bool, jsObject(String?), swiftHeapObject(String), void + case caseEnum(String) + case rawValueEnum(String, String) + case associatedValueEnum(String) + case namespaceEnum(String) } public enum WasmCoreType: String, Codable { @@ -32,6 +42,67 @@ public struct Effects: Codable { } } +// MARK: - Enum Skeleton + +public struct AssociatedValue: Codable, Equatable { + public let label: String? + public let type: BridgeType + + public init(label: String?, type: BridgeType) { + self.label = label + self.type = type + } +} + +public struct EnumCase: Codable, Equatable { + public let name: String + public let rawValue: String? + public let associatedValues: [AssociatedValue] + + public var isSimple: Bool { + associatedValues.isEmpty + } + + public init(name: String, rawValue: String?, associatedValues: [AssociatedValue]) { + self.name = name + self.rawValue = rawValue + self.associatedValues = associatedValues + } +} + +public struct ExportedEnum: Codable, Equatable { + public let name: String + public let cases: [EnumCase] + public let rawType: String? + public let namespace: [String]? + public let nestedTypes: [String] + + public var enumType: EnumType { + if cases.isEmpty { + return .namespace + } else if cases.allSatisfy(\.isSimple) { + return rawType != nil ? .rawValue : .simple + } else { + return .associatedValue + } + } + + public init(name: String, cases: [EnumCase], rawType: String?, namespace: [String]?, nestedTypes: [String]) { + self.name = name + self.cases = cases + self.rawType = rawType + self.namespace = namespace + self.nestedTypes = nestedTypes + } +} + +public enum EnumType: String, Codable { + case simple // enum Direction { case north, south, east } + case rawValue // enum Mode: String { case light = "light" } + case associatedValue // enum Result { case success(String), failure(Int) } + case namespace // enum Utils { } (empty, used as namespace) +} + // MARK: - Exported Skeleton public struct ExportedFunction: Codable { @@ -96,11 +167,13 @@ public struct ExportedSkeleton: Codable { public let moduleName: String public let functions: [ExportedFunction] public let classes: [ExportedClass] + public let enums: [ExportedEnum] - public init(moduleName: String, functions: [ExportedFunction], classes: [ExportedClass]) { + public init(moduleName: String, functions: [ExportedFunction], classes: [ExportedClass], enums: [ExportedEnum]) { self.moduleName = moduleName self.functions = functions self.classes = classes + self.enums = enums } } @@ -163,6 +236,8 @@ public struct ImportedModuleSkeleton: Codable { } } +// MARK: - BridgeType extension + extension BridgeType { public var abiReturnType: WasmCoreType? { switch self { @@ -176,6 +251,61 @@ extension BridgeType { case .swiftHeapObject: // UnsafeMutableRawPointer is returned as an i32 pointer return .pointer + case .caseEnum: + return .i32 + case .rawValueEnum(_, let rawType): + switch rawType { + case "String": + return nil // String uses special handling + case "Bool", "Int", "Int32", "UInt", "UInt32": + return .i32 + case "Int64", "UInt64": + return .i64 + case "Float": + return .f32 + case "Double": + return .f64 + default: + return nil + } + case .associatedValueEnum: + return nil + case .namespaceEnum: + return nil + } + } + + /// Returns the Swift type name for this bridge type + var swiftTypeName: String { + switch self { + case .void: return "Void" + case .bool: return "Bool" + case .int: return "Int" + case .float: return "Float" + case .double: return "Double" + case .string: return "String" + case .jsObject(let name): return name ?? "JSObject" + case .swiftHeapObject(let name): return name + case .caseEnum(let name): return name + case .rawValueEnum(let name, _): return name + case .associatedValueEnum(let name): return name + case .namespaceEnum(let name): return name + } + } + + /// Returns the TypeScript type name for this bridge type + var tsTypeName: String { + switch self { + case .void: return "void" + case .bool: return "boolean" + case .int, .float, .double: return "number" + case .string: return "string" + case .jsObject(let name): return name ?? "any" + case .swiftHeapObject(let name): return name + case .caseEnum(let name): return name + case .rawValueEnum(let name, _): return name + case .associatedValueEnum(let name): return name + case .namespaceEnum(let name): return name } } } diff --git a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json new file mode 100644 index 000000000..7ddef6370 --- /dev/null +++ b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "JavaScript", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "typescript": "^5.8.2" + }, + "bin": { + "ts2skeleton": "bin/ts2skeleton.js" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json index 48fb77cfc..6638cb8d4 100644 --- a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json +++ b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json @@ -1,7 +1,7 @@ { "type": "module", "dependencies": { - "typescript": "5.8.2" + "typescript": "^5.8.2" }, "bin": { "ts2skeleton": "./bin/ts2skeleton.js" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift new file mode 100644 index 000000000..b82ef33fe --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift @@ -0,0 +1,16 @@ +@JS enum Direction { + case north + case south + case east + case west +} + +@JS enum Status { + case loading + case success + case error +} + +@JS func setDirection(_ direction: Direction) +@JS func getDirection() -> Direction +@JS func processDirection(_ input: Direction) -> Status diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift new file mode 100644 index 000000000..15ea35845 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift @@ -0,0 +1,36 @@ +// Empty enum to act as namespace wrapper for classes +@JS enum Utils { + @JS class Converter { + @JS init() {} + + @JS func toString(value: Int) -> String { + return String(value) + } + } +} + +// TODO: Add namespace enum with static functions when supported + +@JS enum Networking { + @JS enum Method { + case get + case post + case put + case delete + } +} + +@JS enum Configuration { + @JS enum LogLevel: String { + case debug = "debug" + case info = "info" + case warning = "warning" + case error = "error" + } + + @JS enum Port: Int { + case http = 80 + case https = 443 + case development = 3000 + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift new file mode 100644 index 000000000..3948470fa --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift @@ -0,0 +1,104 @@ +@JS enum Theme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} + +@JS enum FeatureFlag: Bool { + case enabled = true + case disabled = false +} + +@JS enum HttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + +@JS enum Priority: Int32 { + case lowest = 1 + case low = 2 + case medium = 3 + case high = 4 + case highest = 5 +} + +@JS enum FileSize: Int64 { + case tiny = 1024 + case small = 10240 + case medium = 102400 + case large = 1048576 +} + +@JS enum UserId: UInt { + case guest = 0 + case user = 1000 + case admin = 9999 +} + +@JS enum TokenId: UInt32 { + case invalid = 0 + case session = 12345 + case refresh = 67890 +} + +@JS enum SessionId: UInt64 { + case none = 0 + case active = 9876543210 + case expired = 1234567890 +} + +@JS enum Precision: Float { + case rough = 0.1 + case normal = 0.01 + case fine = 0.001 +} + +@JS enum Ratio: Double { + case quarter = 0.25 + case half = 0.5 + case golden = 1.618 + case pi = 3.14159 +} + +@JS enum FeatureFlag: Bool { + case enabled = true + case disabled = false +} + +@JS func setTheme(_ theme: Theme) +@JS func getTheme() -> Theme + +@JS func setFeatureFlag(_ flag: FeatureFlag) +@JS func getFeatureFlag() -> FeatureFlag + +@JS func setHttpStatus(_ status: HttpStatus) +@JS func getHttpStatus() -> HttpStatus + +@JS func setPriority(_ priority: Priority) +@JS func getPriority() -> Priority + +@JS func setFileSize(_ size: FileSize) +@JS func getFileSize() -> FileSize + +@JS func setUserId(_ id: UserId) +@JS func getUserId() -> UserId + +@JS func setTokenId(_ token: TokenId) +@JS func getTokenId() -> TokenId + +@JS func setSessionId(_ session: SessionId) +@JS func getSessionId() -> SessionId + +@JS func setPrecision(_ precision: Precision) +@JS func getPrecision() -> Precision + +@JS func setRatio(_ ratio: Ratio) +@JS func getRatio() -> Ratio + +@JS func setFeatureFlag(_ featureFlag: FeatureFlag) +@JS func getFeatureFlag() -> FeatureFlag + +@JS func processTheme(_ theme: Theme) -> HttpStatus +@JS func convertPriority(_ status: HttpStatus) -> Priority +@JS func validateSession(_ session: SessionId) -> Theme diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/tsconfig.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/tsconfig.json new file mode 100644 index 000000000..86e2ba175 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "lib": ["es2017"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js index c122f1797..169952273 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js @@ -84,6 +84,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js index 1da2f58ee..6ced9f919 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { asyncReturnVoid: function bjs_asyncReturnVoid() { const retId = instance.exports.bjs_asyncReturnVoid(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js index 21d11fa4d..0e39bd381 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js @@ -128,6 +128,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts new file mode 100644 index 000000000..f4d2c90b3 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts @@ -0,0 +1,35 @@ +// 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 Direction: { + readonly North: 0; + readonly South: 1; + readonly East: 2; + readonly West: 3; +}; +export type Direction = typeof Direction[keyof typeof Direction]; + +export const Status: { + readonly Loading: 0; + readonly Success: 1; + readonly Error: 2; +}; +export type Status = typeof Status[keyof typeof Status]; + +export type Exports = { + setDirection(direction: Direction): void; + getDirection(): Direction; + processDirection(input: Direction): Status; +} +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/EnumCase.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js new file mode 100644 index 000000000..ed355d068 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js @@ -0,0 +1,96 @@ +// 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 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; + 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); + } + + + }, + 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; + + const Direction = { + North: 0, + South: 1, + East: 2, + West: 3, + }; + + const Status = { + Loading: 0, + Success: 1, + Error: 2, + }; + + return { + Direction, + Status, + setDirection: function bjs_setDirection(direction) { + instance.exports.bjs_setDirection(direction | 0); + }, + getDirection: function bjs_getDirection() { + const ret = instance.exports.bjs_getDirection(); + return ret; + }, + processDirection: function bjs_processDirection(input) { + const ret = instance.exports.bjs_processDirection(input | 0); + return ret; + }, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts new file mode 100644 index 000000000..5a1fb674f --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts @@ -0,0 +1,20 @@ +// 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`. + +// TODO: Implement namespace enum: Utils +// TODO: Implement namespace enum: Networking +// TODO: Implement namespace enum: Configuration +export type Exports = { +} +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/EnumNamespace.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js new file mode 100644 index 000000000..0136a85f7 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js @@ -0,0 +1,76 @@ +// 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 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; + 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); + } + + + }, + 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; + + // TODO: Implement namespace enum: Utils + // TODO: Implement namespace enum: Networking + // TODO: Implement namespace enum: Configuration + return { + Utils, + Networking, + Configuration, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts new file mode 100644 index 000000000..7f1ededcc --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts @@ -0,0 +1,121 @@ +// 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 Theme: { + readonly Light: "light"; + readonly Dark: "dark"; + readonly Auto: "auto"; +}; +export type Theme = typeof Theme[keyof typeof Theme]; + +export const FeatureFlag: { + readonly Enabled: true; + readonly Disabled: false; +}; +export type FeatureFlag = typeof FeatureFlag[keyof typeof FeatureFlag]; + +export const HttpStatus: { + readonly Ok: 200; + readonly NotFound: 404; + readonly ServerError: 500; +}; +export type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]; + +export const Priority: { + readonly Lowest: 1; + readonly Low: 2; + readonly Medium: 3; + readonly High: 4; + readonly Highest: 5; +}; +export type Priority = typeof Priority[keyof typeof Priority]; + +export const FileSize: { + readonly Tiny: 1024; + readonly Small: 10240; + readonly Medium: 102400; + readonly Large: 1048576; +}; +export type FileSize = typeof FileSize[keyof typeof FileSize]; + +export const UserId: { + readonly Guest: 0; + readonly User: 1000; + readonly Admin: 9999; +}; +export type UserId = typeof UserId[keyof typeof UserId]; + +export const TokenId: { + readonly Invalid: 0; + readonly Session: 12345; + readonly Refresh: 67890; +}; +export type TokenId = typeof TokenId[keyof typeof TokenId]; + +export const SessionId: { + readonly None: 0; + readonly Active: 9876543210; + readonly Expired: 1234567890; +}; +export type SessionId = typeof SessionId[keyof typeof SessionId]; + +export const Precision: { + readonly Rough: 0.1; + readonly Normal: 0.01; + readonly Fine: 0.001; +}; +export type Precision = typeof Precision[keyof typeof Precision]; + +export const Ratio: { + readonly Quarter: 0.25; + readonly Half: 0.5; + readonly Golden: 1.618; + readonly Pi: 3.14159; +}; +export type Ratio = typeof Ratio[keyof typeof Ratio]; + +export const FeatureFlag: { + readonly Enabled: true; + readonly Disabled: false; +}; +export type FeatureFlag = typeof FeatureFlag[keyof typeof FeatureFlag]; + +export type Exports = { + setTheme(theme: Theme): void; + getTheme(): Theme; + setFeatureFlag(flag: FeatureFlag): void; + getFeatureFlag(): FeatureFlag; + setHttpStatus(status: HttpStatus): void; + getHttpStatus(): HttpStatus; + setPriority(priority: Priority): void; + getPriority(): Priority; + setFileSize(size: FileSize): void; + getFileSize(): FileSize; + setUserId(id: UserId): void; + getUserId(): UserId; + setTokenId(token: TokenId): void; + getTokenId(): TokenId; + setSessionId(session: SessionId): void; + getSessionId(): SessionId; + setPrecision(precision: Precision): void; + getPrecision(): Precision; + setRatio(ratio: Ratio): void; + getRatio(): Ratio; + setFeatureFlag(featureFlag: FeatureFlag): void; + getFeatureFlag(): FeatureFlag; + processTheme(theme: Theme): HttpStatus; + convertPriority(status: HttpStatus): Priority; + validateSession(session: SessionId): Theme; +} +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/EnumRawType.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js new file mode 100644 index 000000000..e63be4653 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js @@ -0,0 +1,248 @@ +// 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 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; + 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); + } + + + }, + 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; + + const Theme = { + Light: "light", + Dark: "dark", + Auto: "auto", + }; + + const FeatureFlag = { + Enabled: true, + Disabled: false, + }; + + const HttpStatus = { + Ok: 200, + NotFound: 404, + ServerError: 500, + }; + + const Priority = { + Lowest: 1, + Low: 2, + Medium: 3, + High: 4, + Highest: 5, + }; + + const FileSize = { + Tiny: 1024, + Small: 10240, + Medium: 102400, + Large: 1048576, + }; + + const UserId = { + Guest: 0, + User: 1000, + Admin: 9999, + }; + + const TokenId = { + Invalid: 0, + Session: 12345, + Refresh: 67890, + }; + + const SessionId = { + None: 0, + Active: 9876543210, + Expired: 1234567890, + }; + + const Precision = { + Rough: 0.1, + Normal: 0.01, + Fine: 0.001, + }; + + const Ratio = { + Quarter: 0.25, + Half: 0.5, + Golden: 1.618, + Pi: 3.14159, + }; + + const FeatureFlag = { + Enabled: true, + Disabled: false, + }; + + return { + Theme, + FeatureFlag, + HttpStatus, + Priority, + FileSize, + UserId, + TokenId, + SessionId, + Precision, + Ratio, + FeatureFlag, + setTheme: function bjs_setTheme(theme) { + const themeBytes = textEncoder.encode(theme); + const themeId = swift.memory.retain(themeBytes); + instance.exports.bjs_setTheme(themeId, themeBytes.length); + swift.memory.release(themeId); + }, + getTheme: function bjs_getTheme() { + instance.exports.bjs_getTheme(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + setFeatureFlag: function bjs_setFeatureFlag(flag) { + instance.exports.bjs_setFeatureFlag(flag ? 1 : 0); + }, + getFeatureFlag: function bjs_getFeatureFlag() { + const ret = instance.exports.bjs_getFeatureFlag(); + return ret !== 0; + }, + setHttpStatus: function bjs_setHttpStatus(status) { + instance.exports.bjs_setHttpStatus(status); + }, + getHttpStatus: function bjs_getHttpStatus() { + const ret = instance.exports.bjs_getHttpStatus(); + return ret; + }, + setPriority: function bjs_setPriority(priority) { + instance.exports.bjs_setPriority(priority); + }, + getPriority: function bjs_getPriority() { + const ret = instance.exports.bjs_getPriority(); + return ret; + }, + setFileSize: function bjs_setFileSize(size) { + instance.exports.bjs_setFileSize(size); + }, + getFileSize: function bjs_getFileSize() { + const ret = instance.exports.bjs_getFileSize(); + return ret; + }, + setUserId: function bjs_setUserId(id) { + instance.exports.bjs_setUserId(id); + }, + getUserId: function bjs_getUserId() { + const ret = instance.exports.bjs_getUserId(); + return ret; + }, + setTokenId: function bjs_setTokenId(token) { + instance.exports.bjs_setTokenId(token); + }, + getTokenId: function bjs_getTokenId() { + const ret = instance.exports.bjs_getTokenId(); + return ret; + }, + setSessionId: function bjs_setSessionId(session) { + instance.exports.bjs_setSessionId(session); + }, + getSessionId: function bjs_getSessionId() { + const ret = instance.exports.bjs_getSessionId(); + return ret; + }, + setPrecision: function bjs_setPrecision(precision) { + instance.exports.bjs_setPrecision(precision); + }, + getPrecision: function bjs_getPrecision() { + const ret = instance.exports.bjs_getPrecision(); + return ret; + }, + setRatio: function bjs_setRatio(ratio) { + instance.exports.bjs_setRatio(ratio); + }, + getRatio: function bjs_getRatio() { + const ret = instance.exports.bjs_getRatio(); + return ret; + }, + setFeatureFlag: function bjs_setFeatureFlag(featureFlag) { + instance.exports.bjs_setFeatureFlag(featureFlag ? 1 : 0); + }, + getFeatureFlag: function bjs_getFeatureFlag() { + const ret = instance.exports.bjs_getFeatureFlag(); + return ret !== 0; + }, + processTheme: function bjs_processTheme(theme) { + const themeBytes = textEncoder.encode(theme); + const themeId = swift.memory.retain(themeBytes); + const ret = instance.exports.bjs_processTheme(themeId, themeBytes.length); + swift.memory.release(themeId); + return ret; + }, + convertPriority: function bjs_convertPriority(status) { + const ret = instance.exports.bjs_convertPriority(status); + return ret; + }, + validateSession: function bjs_validateSession(session) { + instance.exports.bjs_validateSession(session); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js index f81c7e475..874bebd3e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js @@ -90,6 +90,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js index 394d996b6..7e955b1a0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js @@ -194,6 +194,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js index 6915a61a5..cdf5aa250 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js @@ -145,6 +145,7 @@ export async function createInstantiator(options, swift) { return ret; } } + const exports = { Greeter, Converter, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js index 4873fc33c..40ac2cda0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { check: function bjs_check(a, b, c, d) { instance.exports.bjs_check(a, b, c, d); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js index 3b93b2dd0..1cee664bd 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js @@ -70,6 +70,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js index 53332b97b..7daa5bbbb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { checkInt: function bjs_checkInt() { const ret = instance.exports.bjs_checkInt(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js index 1892eb46e..19af42bd2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js @@ -81,6 +81,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js index ea47fb555..bb5cabd8e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { checkString: function bjs_checkString(a) { const aBytes = textEncoder.encode(a); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js index 16ed1081e..524a04664 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js @@ -81,6 +81,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js index f98cea559..67c635657 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { checkString: function bjs_checkString() { instance.exports.bjs_checkString(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js index 3220ae7b0..5830cd3d3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js @@ -72,6 +72,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js index ab4caba34..0a1da2c5f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js @@ -114,6 +114,7 @@ export async function createInstantiator(options, swift) { swift.memory.release(nameId); } } + return { Greeter, takeGreeter: function bjs_takeGreeter(greeter) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js index 705c6a370..15e5d4d88 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js @@ -132,6 +132,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js index b2089962c..95b558a18 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { throwsSomething: function bjs_throwsSomething() { instance.exports.bjs_throwsSomething(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js index 2eb9dee50..dccc193eb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js @@ -70,6 +70,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index c7d622ea0..cc0605999 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -119,6 +119,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js index c200c077d..91017e566 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js @@ -63,6 +63,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { check: function bjs_check() { instance.exports.bjs_check(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js index ca4976888..77087ebaa 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js @@ -70,6 +70,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; + return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json index 8e7154516..c0d5347dc 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json new file mode 100644 index 000000000..3a29a1a1c --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json @@ -0,0 +1,132 @@ +{ + "classes" : [ + + ], + "enums" : [ + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "north" + }, + { + "associatedValues" : [ + + ], + "name" : "south" + }, + { + "associatedValues" : [ + + ], + "name" : "east" + }, + { + "associatedValues" : [ + + ], + "name" : "west" + } + ], + "name" : "Direction", + "nestedTypes" : [ + + ] + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "loading" + }, + { + "associatedValues" : [ + + ], + "name" : "success" + }, + { + "associatedValues" : [ + + ], + "name" : "error" + } + ], + "name" : "Status", + "nestedTypes" : [ + + ] + } + ], + "functions" : [ + { + "abiName" : "bjs_setDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setDirection", + "parameters" : [ + { + "label" : "_", + "name" : "direction", + "type" : { + "caseEnum" : { + "_0" : "Direction" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getDirection", + "parameters" : [ + + ], + "returnType" : { + "caseEnum" : { + "_0" : "Direction" + } + } + }, + { + "abiName" : "bjs_processDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "processDirection", + "parameters" : [ + { + "label" : "_", + "name" : "input", + "type" : { + "caseEnum" : { + "_0" : "Direction" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "Status" + } + } + } + ], + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift new file mode 100644 index 000000000..affe3d3df --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift @@ -0,0 +1,39 @@ +// 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 + +@_expose(wasm, "bjs_setDirection") +@_cdecl("bjs_setDirection") +public func _bjs_setDirection(direction: Int32) -> Void { + #if arch(wasm32) + setDirection(_: Direction(rawValue: Int(direction))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getDirection") +@_cdecl("bjs_getDirection") +public func _bjs_getDirection() -> Int32 { + #if arch(wasm32) + let ret = getDirection() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_processDirection") +@_cdecl("bjs_processDirection") +public func _bjs_processDirection(input: Int32) -> Int32 { + #if arch(wasm32) + let ret = processDirection(_: Direction(rawValue: Int(input))!) + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json new file mode 100644 index 000000000..fc427dec6 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json @@ -0,0 +1,39 @@ +{ + "classes" : [ + + ], + "enums" : [ + { + "cases" : [ + + ], + "name" : "Utils", + "nestedTypes" : [ + "Converter" + ] + }, + { + "cases" : [ + + ], + "name" : "Networking", + "nestedTypes" : [ + "Method" + ] + }, + { + "cases" : [ + + ], + "name" : "Configuration", + "nestedTypes" : [ + "LogLevel", + "Port" + ] + } + ], + "functions" : [ + + ], + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift new file mode 100644 index 000000000..e241d1805 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift @@ -0,0 +1,7 @@ +// 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 \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json new file mode 100644 index 000000000..5b88d2755 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json @@ -0,0 +1,894 @@ +{ + "classes" : [ + + ], + "enums" : [ + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "light", + "rawValue" : "light" + }, + { + "associatedValues" : [ + + ], + "name" : "dark", + "rawValue" : "dark" + }, + { + "associatedValues" : [ + + ], + "name" : "auto", + "rawValue" : "auto" + } + ], + "name" : "Theme", + "nestedTypes" : [ + + ], + "rawType" : "String" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "enabled", + "rawValue" : "true" + }, + { + "associatedValues" : [ + + ], + "name" : "disabled", + "rawValue" : "false" + } + ], + "name" : "FeatureFlag", + "nestedTypes" : [ + + ], + "rawType" : "Bool" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "ok", + "rawValue" : "200" + }, + { + "associatedValues" : [ + + ], + "name" : "notFound", + "rawValue" : "404" + }, + { + "associatedValues" : [ + + ], + "name" : "serverError", + "rawValue" : "500" + } + ], + "name" : "HttpStatus", + "nestedTypes" : [ + + ], + "rawType" : "Int" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "lowest", + "rawValue" : "1" + }, + { + "associatedValues" : [ + + ], + "name" : "low", + "rawValue" : "2" + }, + { + "associatedValues" : [ + + ], + "name" : "medium", + "rawValue" : "3" + }, + { + "associatedValues" : [ + + ], + "name" : "high", + "rawValue" : "4" + }, + { + "associatedValues" : [ + + ], + "name" : "highest", + "rawValue" : "5" + } + ], + "name" : "Priority", + "nestedTypes" : [ + + ], + "rawType" : "Int32" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "tiny", + "rawValue" : "1024" + }, + { + "associatedValues" : [ + + ], + "name" : "small", + "rawValue" : "10240" + }, + { + "associatedValues" : [ + + ], + "name" : "medium", + "rawValue" : "102400" + }, + { + "associatedValues" : [ + + ], + "name" : "large", + "rawValue" : "1048576" + } + ], + "name" : "FileSize", + "nestedTypes" : [ + + ], + "rawType" : "Int64" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "guest", + "rawValue" : "0" + }, + { + "associatedValues" : [ + + ], + "name" : "user", + "rawValue" : "1000" + }, + { + "associatedValues" : [ + + ], + "name" : "admin", + "rawValue" : "9999" + } + ], + "name" : "UserId", + "nestedTypes" : [ + + ], + "rawType" : "UInt" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "invalid", + "rawValue" : "0" + }, + { + "associatedValues" : [ + + ], + "name" : "session", + "rawValue" : "12345" + }, + { + "associatedValues" : [ + + ], + "name" : "refresh", + "rawValue" : "67890" + } + ], + "name" : "TokenId", + "nestedTypes" : [ + + ], + "rawType" : "UInt32" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "none", + "rawValue" : "0" + }, + { + "associatedValues" : [ + + ], + "name" : "active", + "rawValue" : "9876543210" + }, + { + "associatedValues" : [ + + ], + "name" : "expired", + "rawValue" : "1234567890" + } + ], + "name" : "SessionId", + "nestedTypes" : [ + + ], + "rawType" : "UInt64" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "rough", + "rawValue" : "0.1" + }, + { + "associatedValues" : [ + + ], + "name" : "normal", + "rawValue" : "0.01" + }, + { + "associatedValues" : [ + + ], + "name" : "fine", + "rawValue" : "0.001" + } + ], + "name" : "Precision", + "nestedTypes" : [ + + ], + "rawType" : "Float" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "quarter", + "rawValue" : "0.25" + }, + { + "associatedValues" : [ + + ], + "name" : "half", + "rawValue" : "0.5" + }, + { + "associatedValues" : [ + + ], + "name" : "golden", + "rawValue" : "1.618" + }, + { + "associatedValues" : [ + + ], + "name" : "pi", + "rawValue" : "3.14159" + } + ], + "name" : "Ratio", + "nestedTypes" : [ + + ], + "rawType" : "Double" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "enabled", + "rawValue" : "true" + }, + { + "associatedValues" : [ + + ], + "name" : "disabled", + "rawValue" : "false" + } + ], + "name" : "FeatureFlag", + "nestedTypes" : [ + + ], + "rawType" : "Bool" + } + ], + "functions" : [ + { + "abiName" : "bjs_setTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTheme", + "parameters" : [ + { + "label" : "_", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTheme", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + }, + { + "abiName" : "bjs_setFeatureFlag", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setFeatureFlag", + "parameters" : [ + { + "label" : "_", + "name" : "flag", + "type" : { + "rawValueEnum" : { + "_0" : "FeatureFlag", + "_1" : "Bool" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getFeatureFlag", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getFeatureFlag", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "FeatureFlag", + "_1" : "Bool" + } + } + }, + { + "abiName" : "bjs_setHttpStatus", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setHttpStatus", + "parameters" : [ + { + "label" : "_", + "name" : "status", + "type" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getHttpStatus", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getHttpStatus", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + }, + { + "abiName" : "bjs_setPriority", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setPriority", + "parameters" : [ + { + "label" : "_", + "name" : "priority", + "type" : { + "rawValueEnum" : { + "_0" : "Priority", + "_1" : "Int32" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getPriority", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getPriority", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Priority", + "_1" : "Int32" + } + } + }, + { + "abiName" : "bjs_setFileSize", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setFileSize", + "parameters" : [ + { + "label" : "_", + "name" : "size", + "type" : { + "rawValueEnum" : { + "_0" : "FileSize", + "_1" : "Int64" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getFileSize", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getFileSize", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "FileSize", + "_1" : "Int64" + } + } + }, + { + "abiName" : "bjs_setUserId", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setUserId", + "parameters" : [ + { + "label" : "_", + "name" : "id", + "type" : { + "rawValueEnum" : { + "_0" : "UserId", + "_1" : "UInt" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getUserId", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getUserId", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "UserId", + "_1" : "UInt" + } + } + }, + { + "abiName" : "bjs_setTokenId", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTokenId", + "parameters" : [ + { + "label" : "_", + "name" : "token", + "type" : { + "rawValueEnum" : { + "_0" : "TokenId", + "_1" : "UInt32" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getTokenId", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTokenId", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "TokenId", + "_1" : "UInt32" + } + } + }, + { + "abiName" : "bjs_setSessionId", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setSessionId", + "parameters" : [ + { + "label" : "_", + "name" : "session", + "type" : { + "rawValueEnum" : { + "_0" : "SessionId", + "_1" : "UInt64" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getSessionId", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getSessionId", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "SessionId", + "_1" : "UInt64" + } + } + }, + { + "abiName" : "bjs_setPrecision", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setPrecision", + "parameters" : [ + { + "label" : "_", + "name" : "precision", + "type" : { + "rawValueEnum" : { + "_0" : "Precision", + "_1" : "Float" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getPrecision", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getPrecision", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Precision", + "_1" : "Float" + } + } + }, + { + "abiName" : "bjs_setRatio", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setRatio", + "parameters" : [ + { + "label" : "_", + "name" : "ratio", + "type" : { + "rawValueEnum" : { + "_0" : "Ratio", + "_1" : "Double" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getRatio", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getRatio", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Ratio", + "_1" : "Double" + } + } + }, + { + "abiName" : "bjs_setFeatureFlag", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setFeatureFlag", + "parameters" : [ + { + "label" : "_", + "name" : "featureFlag", + "type" : { + "rawValueEnum" : { + "_0" : "FeatureFlag", + "_1" : "Bool" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getFeatureFlag", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getFeatureFlag", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "FeatureFlag", + "_1" : "Bool" + } + } + }, + { + "abiName" : "bjs_processTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "processTheme", + "parameters" : [ + { + "label" : "_", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + }, + { + "abiName" : "bjs_convertPriority", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "convertPriority", + "parameters" : [ + { + "label" : "_", + "name" : "status", + "type" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Priority", + "_1" : "Int32" + } + } + }, + { + "abiName" : "bjs_validateSession", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "validateSession", + "parameters" : [ + { + "label" : "_", + "name" : "session", + "type" : { + "rawValueEnum" : { + "_0" : "SessionId", + "_1" : "UInt64" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + } + ], + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift new file mode 100644 index 000000000..185c4ac80 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift @@ -0,0 +1,283 @@ +// 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 + +@_expose(wasm, "bjs_setTheme") +@_cdecl("bjs_setTheme") +public func _bjs_setTheme(themeBytes: Int32, themeLen: Int32) -> Void { + #if arch(wasm32) + let theme = String(unsafeUninitializedCapacity: Int(themeLen)) { b in + _swift_js_init_memory(themeBytes, b.baseAddress.unsafelyUnwrapped) + return Int(themeLen) + } + setTheme(_: Theme(rawValue: theme)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTheme") +@_cdecl("bjs_getTheme") +public func _bjs_getTheme() -> Void { + #if arch(wasm32) + let ret = getTheme() + return ret.rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setFeatureFlag") +@_cdecl("bjs_setFeatureFlag") +public func _bjs_setFeatureFlag(flag: Int32) -> Void { + #if arch(wasm32) + setFeatureFlag(_: FeatureFlag(rawValue: flag != 0)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getFeatureFlag") +@_cdecl("bjs_getFeatureFlag") +public func _bjs_getFeatureFlag() -> Int32 { + #if arch(wasm32) + let ret = getFeatureFlag() + return Int32(ret.rawValue ? 1 : 0) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setHttpStatus") +@_cdecl("bjs_setHttpStatus") +public func _bjs_setHttpStatus(status: Int32) -> Void { + #if arch(wasm32) + setHttpStatus(_: HttpStatus(rawValue: Int(status))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getHttpStatus") +@_cdecl("bjs_getHttpStatus") +public func _bjs_getHttpStatus() -> Int32 { + #if arch(wasm32) + let ret = getHttpStatus() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setPriority") +@_cdecl("bjs_setPriority") +public func _bjs_setPriority(priority: Int32) -> Void { + #if arch(wasm32) + setPriority(_: Priority(rawValue: Int32(priority))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getPriority") +@_cdecl("bjs_getPriority") +public func _bjs_getPriority() -> Int32 { + #if arch(wasm32) + let ret = getPriority() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setFileSize") +@_cdecl("bjs_setFileSize") +public func _bjs_setFileSize(size: Int64) -> Void { + #if arch(wasm32) + setFileSize(_: FileSize(rawValue: Int64(size))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getFileSize") +@_cdecl("bjs_getFileSize") +public func _bjs_getFileSize() -> Int64 { + #if arch(wasm32) + let ret = getFileSize() + return Int64(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setUserId") +@_cdecl("bjs_setUserId") +public func _bjs_setUserId(id: Int32) -> Void { + #if arch(wasm32) + setUserId(_: UserId(rawValue: UInt(bitPattern: id))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getUserId") +@_cdecl("bjs_getUserId") +public func _bjs_getUserId() -> Int32 { + #if arch(wasm32) + let ret = getUserId() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setTokenId") +@_cdecl("bjs_setTokenId") +public func _bjs_setTokenId(token: Int32) -> Void { + #if arch(wasm32) + setTokenId(_: TokenId(rawValue: UInt32(bitPattern: token))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTokenId") +@_cdecl("bjs_getTokenId") +public func _bjs_getTokenId() -> Int32 { + #if arch(wasm32) + let ret = getTokenId() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setSessionId") +@_cdecl("bjs_setSessionId") +public func _bjs_setSessionId(session: Int64) -> Void { + #if arch(wasm32) + setSessionId(_: SessionId(rawValue: UInt64(bitPattern: Int64(session)))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getSessionId") +@_cdecl("bjs_getSessionId") +public func _bjs_getSessionId() -> Int64 { + #if arch(wasm32) + let ret = getSessionId() + return Int64(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setPrecision") +@_cdecl("bjs_setPrecision") +public func _bjs_setPrecision(precision: Float32) -> Void { + #if arch(wasm32) + setPrecision(_: Precision(rawValue: Float(precision))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getPrecision") +@_cdecl("bjs_getPrecision") +public func _bjs_getPrecision() -> Float32 { + #if arch(wasm32) + let ret = getPrecision() + return Float32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setRatio") +@_cdecl("bjs_setRatio") +public func _bjs_setRatio(ratio: Float64) -> Void { + #if arch(wasm32) + setRatio(_: Ratio(rawValue: Double(ratio))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getRatio") +@_cdecl("bjs_getRatio") +public func _bjs_getRatio() -> Float64 { + #if arch(wasm32) + let ret = getRatio() + return Float64(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setFeatureFlag") +@_cdecl("bjs_setFeatureFlag") +public func _bjs_setFeatureFlag(featureFlag: Int32) -> Void { + #if arch(wasm32) + setFeatureFlag(_: FeatureFlag(rawValue: featureFlag != 0)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getFeatureFlag") +@_cdecl("bjs_getFeatureFlag") +public func _bjs_getFeatureFlag() -> Int32 { + #if arch(wasm32) + let ret = getFeatureFlag() + return Int32(ret.rawValue ? 1 : 0) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_processTheme") +@_cdecl("bjs_processTheme") +public func _bjs_processTheme(themeBytes: Int32, themeLen: Int32) -> Int32 { + #if arch(wasm32) + let theme = String(unsafeUninitializedCapacity: Int(themeLen)) { b in + _swift_js_init_memory(themeBytes, b.baseAddress.unsafelyUnwrapped) + return Int(themeLen) + } + let ret = processTheme(_: Theme(rawValue: theme)!) + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_convertPriority") +@_cdecl("bjs_convertPriority") +public func _bjs_convertPriority(status: Int32) -> Int32 { + #if arch(wasm32) + let ret = convertPriority(_: HttpStatus(rawValue: Int(status))!) + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_validateSession") +@_cdecl("bjs_validateSession") +public func _bjs_validateSession(session: Int64) -> Void { + #if arch(wasm32) + let ret = validateSession(_: SessionId(rawValue: UInt64(bitPattern: Int64(session)))!) + return ret.rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json index 1d1b0fbef..79ff94d8b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json @@ -111,6 +111,9 @@ "Foundation" ] } + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json index 7ba4d9dcf..c58f3c8e5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json index 54e00ea5c..ee29313ba 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json index c2286d122..22df1dc5e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json index 23331875f..75439e363 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json index 489f1cd5b..3f621eb69 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json @@ -63,6 +63,9 @@ ], "name" : "Greeter" } + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json index 9acf5b201..cc3184fb5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json index 12c735315..413fe084b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { From 46c64dbbfc3c550106b42711c08cc16adacfbc0f Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 19 Aug 2025 12:46:32 +0200 Subject: [PATCH 2/4] BridgeJS: Namespace enum implementation, refactor to parse enums using default SwiftSyntax methods --- .../Sources/BridgeJSCore/ExportSwift.swift | 498 +++++++++------- .../Sources/BridgeJSLink/BridgeJSLink.swift | 541 ++++++++++++------ .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 106 ++-- .../Inputs/EnumNamespace.swift | 36 +- .../Inputs/EnumRawType.swift | 5 - .../EnumNamespace.Export.d.ts | 82 ++- .../BridgeJSLinkTests/EnumNamespace.Export.js | 150 ++++- .../BridgeJSLinkTests/EnumRawType.Export.d.ts | 6 - .../BridgeJSLinkTests/EnumRawType.Export.js | 6 - .../BridgeJSLinkTests/Namespaces.Export.d.ts | 10 +- .../ExportSwiftTests/EnumCase.json | 8 +- .../ExportSwiftTests/EnumNamespace.json | 260 ++++++++- .../ExportSwiftTests/EnumNamespace.swift | 110 +++- .../ExportSwiftTests/EnumRawType.json | 83 +-- .../ExportSwiftTests/Namespaces.json | 9 +- .../ExportSwiftTests/SwiftClass.json | 3 +- 16 files changed, 1353 insertions(+), 560 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index fba27aa1b..211f71390 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -75,9 +75,27 @@ public class ExportSwift { var exportedEnumByName: [String: ExportedEnum] = [:] var errors: [DiagnosticError] = [] + /// Creates a unique key for a class by combining name and namespace + private func classKey(name: String, namespace: [String]?) -> String { + if let namespace = namespace, !namespace.isEmpty { + return "\(namespace.joined(separator: ".")).\(name)" + } else { + return name + } + } + + /// Temporary storage for enum data during visitor traversal since EnumCaseDeclSyntax lacks parent context + struct CurrentEnum { + var name: String? + var cases: [EnumCase] = [] + var rawType: String? + } + var currentEnum = CurrentEnum() + enum State { case topLevel - case classBody(name: String) + case classBody(name: String, key: String) + case enumBody(name: String) } struct StateStack { @@ -124,15 +142,22 @@ public class ExportSwift { override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { switch state { case .topLevel: - if let exportedFunction = visitFunction(node: node) { + if let exportedFunction = visitFunction( + node: node + ) { exportedFunctions.append(exportedFunction) } return .skipChildren - case .classBody(let name): - if let exportedFunction = visitFunction(node: node) { - exportedClassByName[name]?.methods.append(exportedFunction) + case .classBody(_, let classKey): + if let exportedFunction = visitFunction( + node: node + ) { + exportedClassByName[classKey]?.methods.append(exportedFunction) } return .skipChildren + case .enumBody: + diagnose(node: node, message: "Functions are not supported inside enums") + return .skipChildren } } @@ -177,8 +202,14 @@ public class ExportSwift { switch state { case .topLevel: abiName = "bjs_\(name)" - case .classBody(let className): + case .classBody(let className, _): abiName = "bjs_\(className)_\(name)" + case .enumBody: + abiName = "" + diagnose( + node: node, + message: "Functions are not supported inside enums" + ) } guard let effects = collectEffects(signature: node.signature) else { @@ -238,8 +269,12 @@ public class ExportSwift { override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { return .skipChildren } - guard case .classBody(let name) = state else { - diagnose(node: node, message: "@JS init must be inside a @JS class") + guard case .classBody(let className, _) = state else { + if case .enumBody(_) = state { + diagnose(node: node, message: "Initializers are not supported inside enums") + } else { + diagnose(node: node, message: "@JS init must be inside a @JS class") + } return .skipChildren } @@ -269,52 +304,72 @@ public class ExportSwift { } let constructor = ExportedConstructor( - abiName: "bjs_\(name)_init", + abiName: "bjs_\(className)_init", parameters: parameters, effects: effects ) - exportedClassByName[name]?.constructor = constructor + if case .classBody(_, let classKey) = state { + exportedClassByName[classKey]?.constructor = constructor + } return .skipChildren } override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { let name = node.name.text - stateStack.push(state: .classBody(name: name)) + guard let jsAttribute = node.attributes.firstJSAttribute else { + if case .enumBody(_) = state { + return .skipChildren + } + return .skipChildren + } - guard let jsAttribute = node.attributes.firstJSAttribute else { return .skipChildren } + let attributeNamespace = extractNamespace(from: jsAttribute) + let computedNamespace = computeNamespace(for: node) - let namespace = extractNamespace(from: jsAttribute) - exportedClassByName[name] = ExportedClass( + if computedNamespace != nil && attributeNamespace != nil { + diagnose( + node: jsAttribute, + message: "Nested classes cannot specify their own namespace", + hint: "Remove the namespace from @JS attribute - nested classes inherit namespace from parent" + ) + return .skipChildren + } + + let effectiveNamespace = computedNamespace ?? attributeNamespace + + let swiftCallName = ExportSwift.computeSwiftCallName(for: node, itemName: name) + let exportedClass = ExportedClass( name: name, + swiftCallName: swiftCallName, constructor: nil, methods: [], - namespace: namespace + namespace: effectiveNamespace ) - exportedClassNames.append(name) + let uniqueKey = classKey(name: name, namespace: effectiveNamespace) + + stateStack.push(state: .classBody(name: name, key: uniqueKey)) + exportedClassByName[uniqueKey] = exportedClass + exportedClassNames.append(uniqueKey) return .visitChildren } + override func visitPost(_ node: ClassDeclSyntax) { stateStack.pop() } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - guard let jsAttribute = node.attributes.firstJSAttribute else { + guard node.attributes.hasJSAttribute() else { + if case .enumBody(_) = state { + return .skipChildren + } return .skipChildren } - let name = node.name.text - let namespace = extractNamespace(from: jsAttribute) - - if let exportedEnum = parseEnum(node: node, namespace: namespace) { - exportedEnumByName[name] = exportedEnum - exportedEnumNames.append(name) + guard let jsAttribute = node.attributes.firstJSAttribute else { + return .skipChildren } - return .skipChildren - } - - private func parseEnum(node: EnumDeclSyntax, namespace: [String]?) -> ExportedEnum? { let name = node.name.text let rawType: String? = node.inheritanceClause?.inheritedTypes.first { inheritedType in @@ -322,68 +377,138 @@ public class ExportSwift { return Constants.supportedRawTypes.contains(typeName) }?.type.trimmedDescription - var cases: [EnumCase] = [] - var nestedTypes: [String] = [] - - for member in node.memberBlock.members { - if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { - for element in caseDecl.elements { - let caseName = element.name.text - let rawValue: String? - if let stringLiteral = element.rawValue?.value.as(StringLiteralExprSyntax.self) { - rawValue = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text - } else if let boolLiteral = element.rawValue?.value.as(BooleanLiteralExprSyntax.self) { - rawValue = boolLiteral.literal.text - } else if let intLiteral = element.rawValue?.value.as(IntegerLiteralExprSyntax.self) { - rawValue = intLiteral.literal.text - } else if let floatLiteral = element.rawValue?.value.as(FloatLiteralExprSyntax.self) { - rawValue = floatLiteral.literal.text - } else { - // Other unsupported type or no raw value - rawValue = nil - } + let attributeNamespace = extractNamespace(from: jsAttribute) + let computedNamespace = computeNamespace(for: node) - var associatedValues: [AssociatedValue] = [] - if let parameterClause = element.parameterClause { - for param in parameterClause.parameters { - guard let bridgeType = parent.lookupType(for: param.type) else { - diagnose( - node: param.type, - message: "Unsupported associated value type: \(param.type.trimmedDescription)", - hint: "Only primitive types and types defined in the same module are allowed" - ) - continue - } - - let label = param.firstName?.text - associatedValues.append(AssociatedValue(label: label, type: bridgeType)) - } - } + if computedNamespace != nil && attributeNamespace != nil { + diagnose( + node: jsAttribute, + message: "Nested enums cannot specify their own namespace", + hint: "Remove the namespace from @JS attribute - nested enums inherit namespace from parent" + ) + return .skipChildren + } + + currentEnum.name = name + currentEnum.cases = [] + currentEnum.rawType = rawType + + stateStack.push(state: .enumBody(name: name)) - cases.append( - EnumCase( - name: caseName, - rawValue: rawValue, - associatedValues: associatedValues + return .visitChildren + } + + override func visitPost(_ node: EnumDeclSyntax) { + guard let jsAttribute = node.attributes.firstJSAttribute, + let enumName = currentEnum.name + else { + return + } + + let attributeNamespace = extractNamespace(from: jsAttribute) + let computedNamespace = computeNamespace(for: node) + + let effectiveNamespace: [String]? + if computedNamespace == nil && attributeNamespace != nil { + effectiveNamespace = attributeNamespace + } else { + effectiveNamespace = computedNamespace + } + + let swiftCallName = ExportSwift.computeSwiftCallName(for: node, itemName: enumName) + let exportedEnum = ExportedEnum( + name: enumName, + swiftCallName: swiftCallName, + cases: currentEnum.cases, + rawType: currentEnum.rawType, + namespace: effectiveNamespace + ) + exportedEnumByName[enumName] = exportedEnum + exportedEnumNames.append(enumName) + + currentEnum = CurrentEnum() + stateStack.pop() + } + + override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { + for element in node.elements { + let caseName = element.name.text + let rawValue: String? + var associatedValues: [AssociatedValue] = [] + + if currentEnum.rawType != nil { + if let stringLiteral = element.rawValue?.value.as(StringLiteralExprSyntax.self) { + rawValue = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text + } else if let boolLiteral = element.rawValue?.value.as(BooleanLiteralExprSyntax.self) { + rawValue = boolLiteral.literal.text + } else if let intLiteral = element.rawValue?.value.as(IntegerLiteralExprSyntax.self) { + rawValue = intLiteral.literal.text + } else if let floatLiteral = element.rawValue?.value.as(FloatLiteralExprSyntax.self) { + rawValue = floatLiteral.literal.text + } else { + rawValue = nil + } + } else { + rawValue = nil + } + if let parameterClause = element.parameterClause { + for param in parameterClause.parameters { + guard let bridgeType = parent.lookupType(for: param.type) else { + diagnose( + node: param.type, + message: "Unsupported associated value type: \(param.type.trimmedDescription)", + hint: "Only primitive types and types defined in the same module are allowed" ) - ) + continue + } + + let label = param.firstName?.text + associatedValues.append(AssociatedValue(label: label, type: bridgeType)) } - } else if let nestedEnum = member.decl.as(EnumDeclSyntax.self) { - // Track nested enums for namespace enums - nestedTypes.append(nestedEnum.name.text) - } else if let nestedClass = member.decl.as(ClassDeclSyntax.self) { - // Track nested classes for namespace enums - nestedTypes.append(nestedClass.name.text) } + let enumCase = EnumCase( + name: caseName, + rawValue: rawValue, + associatedValues: associatedValues + ) + + currentEnum.cases.append(enumCase) } - return ExportedEnum( - name: name, - cases: cases, - rawType: rawType, - namespace: namespace, - nestedTypes: nestedTypes - ) + return .visitChildren + } + + /// Computes namespace by walking up the AST hierarchy to find parent namespace enums + /// If parent enum is a namespace enum (no cases) then it will be used as part of namespace for given node + /// + /// + /// Method allows for explicit namespace for top level enum, it will be used as base namespace and will concat enum name + private func computeNamespace(for node: some SyntaxProtocol) -> [String]? { + var namespace: [String] = [] + var currentNode: Syntax? = node.parent + + while let parent = currentNode { + if let enumDecl = parent.as(EnumDeclSyntax.self), + enumDecl.attributes.hasJSAttribute() + { + let isNamespaceEnum = !enumDecl.memberBlock.members.contains { member in + member.decl.is(EnumCaseDeclSyntax.self) + } + if isNamespaceEnum { + namespace.insert(enumDecl.name.text, at: 0) + + if let jsAttribute = enumDecl.attributes.firstJSAttribute, + let explicitNamespace = extractNamespace(from: jsAttribute) + { + namespace = explicitNamespace + namespace + break + } + } + } + currentNode = parent.parent + } + + return namespace.isEmpty ? nil : namespace } } @@ -404,11 +529,32 @@ public class ExportSwift { return collector.errors } + /// Computes the full Swift call name by walking up the AST hierarchy to find all parent enums + /// This generates the qualified name needed for Swift code generation (e.g., "Networking.API.HTTPServer") + private static func computeSwiftCallName(for node: some SyntaxProtocol, itemName: String) -> String { + var swiftPath: [String] = [] + var currentNode: Syntax? = node.parent + + while let parent = currentNode { + if let enumDecl = parent.as(EnumDeclSyntax.self), + enumDecl.attributes.hasJSAttribute() + { + swiftPath.insert(enumDecl.name.text, at: 0) + } + currentNode = parent.parent + } + + if swiftPath.isEmpty { + return itemName + } else { + return swiftPath.joined(separator: ".") + "." + itemName + } + } + func lookupType(for type: TypeSyntax) -> BridgeType? { if let primitive = BridgeType(swiftType: type.trimmedDescription) { return primitive } - guard let identifier = type.as(IdentifierTypeSyntax.self) else { return nil } @@ -416,10 +562,34 @@ public class ExportSwift { guard let typeDecl = typeDeclResolver.lookupType(for: identifier) else { return nil } - - // Check if it's an enum if let enumDecl = typeDecl.as(EnumDeclSyntax.self) { - return classifyEnumType(enumDecl) + let enumName = enumDecl.name.text + if let existingEnum = exportedEnums.first(where: { $0.name == enumName }) { + switch existingEnum.enumType { + case .simple: + return .caseEnum(existingEnum.swiftCallName) + case .rawValue: + let rawType = SwiftEnumRawType.from(existingEnum.rawType!)! + return .rawValueEnum(existingEnum.swiftCallName, rawType) + case .associatedValue: + return .associatedValueEnum(existingEnum.swiftCallName) + case .namespace: + return .namespaceEnum(existingEnum.swiftCallName) + } + } + let swiftCallName = ExportSwift.computeSwiftCallName(for: enumDecl, itemName: enumDecl.name.text) + let rawTypeString = enumDecl.inheritanceClause?.inheritedTypes.first { inheritedType in + let typeName = inheritedType.type.trimmedDescription + return Constants.supportedRawTypes.contains(typeName) + }?.type.trimmedDescription + + if let rawTypeString = rawTypeString, + let rawType = SwiftEnumRawType.from(rawTypeString) + { + return .rawValueEnum(swiftCallName, rawType) + } else { + return .caseEnum(swiftCallName) + } } guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else { @@ -450,53 +620,10 @@ public class ExportSwift { for klass in exportedClasses { decls.append(contentsOf: renderSingleExportedClass(klass: klass)) } - // Note: Enums don't need Swift glue code generation - they're used directly let format = BasicFormat() return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") } - /// Classifies an enum declaration into the appropriate BridgeType - private func classifyEnumType(_ enumDecl: EnumDeclSyntax) -> BridgeType? { - let enumName = enumDecl.name.text - - // Check for raw value type - let rawType = enumDecl.inheritanceClause?.inheritedTypes.first { inheritedType in - let typeName = inheritedType.type.trimmedDescription - return Constants.supportedRawTypes.contains(typeName) - }?.type.trimmedDescription - - // Count cases and check for associated values - var hasCases = false - var hasAssociatedValues = false - - for member in enumDecl.memberBlock.members { - if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { - hasCases = true - for element in caseDecl.elements { - if element.parameterClause != nil { - hasAssociatedValues = true - break - } - } - if hasAssociatedValues { break } - } - } - - // Classify based on structure - if !hasCases { - // Empty enum - used as namespace - return .namespaceEnum(enumName) - } else if hasAssociatedValues { - // Has associated values - return .associatedValueEnum(enumName) - } else if let rawType = rawType { - // Has raw values - return .rawValueEnum(enumName, rawType) - } else { - return .caseEnum(enumName) - } - } - class ExportedThunkBuilder { var body: [CodeBlockItemSyntax] = [] var abiParameterForwardings: [LabeledExprSyntax] = [] @@ -578,7 +705,7 @@ public class ExportSwift { ) abiParameterSignatures.append((param.name, .i32)) case .rawValueEnum(let enumName, let rawType): - if rawType == "String" { + if rawType == .string { let bytesLabel = "\(param.name)Bytes" let lengthLabel = "\(param.name)Len" let prepare: CodeBlockItemSyntax = """ @@ -597,22 +724,19 @@ public class ExportSwift { abiParameterSignatures.append((bytesLabel, .i32)) abiParameterSignatures.append((lengthLabel, .i32)) } else { - // Numeric raw types - use correct WASM ABI type let conversionExpr: String switch rawType { - case "Bool": - // Bool requires special conversion from Int32 (0/1) to Bool (false/true) + case .bool: conversionExpr = "\(enumName)(rawValue: \(param.name) != 0)!" - case "UInt", "UInt32", "UInt64": - // Unsigned types: use bitPattern to handle potential negative Int32 values safely - if rawType == "UInt64" { - conversionExpr = "\(enumName)(rawValue: \(rawType)(bitPattern: Int64(\(param.name))))!" + case .uint, .uint32, .uint64: + if rawType == .uint64 { + conversionExpr = + "\(enumName)(rawValue: \(rawType.rawValue)(bitPattern: Int64(\(param.name))))!" } else { - conversionExpr = "\(enumName)(rawValue: \(rawType)(bitPattern: \(param.name)))!" + conversionExpr = "\(enumName)(rawValue: \(rawType.rawValue)(bitPattern: \(param.name)))!" } default: - // Signed integer and float types: direct conversion - conversionExpr = "\(enumName)(rawValue: \(rawType)(\(param.name)))!" + conversionExpr = "\(enumName)(rawValue: \(rawType.rawValue)(\(param.name)))!" } abiParameterForwardings.append( @@ -621,38 +745,12 @@ public class ExportSwift { expression: ExprSyntax(stringLiteral: conversionExpr) ) ) - switch rawType { - case "Bool", "Int", "Int32", "UInt", "UInt32": - abiParameterSignatures.append((param.name, .i32)) - case "Int64", "UInt64": - abiParameterSignatures.append((param.name, .i64)) - case "Float": - abiParameterSignatures.append((param.name, .f32)) - case "Double": - abiParameterSignatures.append((param.name, .f64)) - default: - abiParameterSignatures.append((param.name, .i32)) // Fallback + if let wasmType = rawType.wasmCoreType { + abiParameterSignatures.append((param.name, wasmType)) } } - case .associatedValueEnum(let enumName): - let bytesLabel = "\(param.name)Bytes" - let lengthLabel = "\(param.name)Len" - let prepare: CodeBlockItemSyntax = """ - let \(raw: param.name)JsonString = String(unsafeUninitializedCapacity: Int(\(raw: lengthLabel))) { b in - _swift_js_init_memory(\(raw: bytesLabel), b.baseAddress.unsafelyUnwrapped) - return Int(\(raw: lengthLabel)) - } - let \(raw: param.name) = try! JSONDecoder().decode(\(raw: enumName).self, from: \(raw: param.name)JsonString.data(using: .utf8)!) - """ - append(prepare) - abiParameterForwardings.append( - LabeledExprSyntax( - label: param.label, - expression: ExprSyntax("\(raw: param.name)") - ) - ) - abiParameterSignatures.append((bytesLabel, .i32)) - abiParameterSignatures.append((lengthLabel, .i32)) + case .associatedValueEnum(_): + break case .namespaceEnum: break case .jsObject(nil): @@ -758,20 +856,7 @@ public class ExportSwift { case .caseEnum: abiReturnType = .i32 case .rawValueEnum(_, let rawType): - switch rawType { - case "String": - abiReturnType = nil - case "Bool", "Int", "Int32", "UInt", "UInt32": - abiReturnType = .i32 - case "Int64", "UInt64": - abiReturnType = .i64 - case "Float": - abiReturnType = .f32 - case "Double": - abiReturnType = .f64 - default: - abiReturnType = nil - } + abiReturnType = rawType == .string ? nil : rawType.wasmCoreType case .associatedValueEnum: abiReturnType = nil case .namespaceEnum: @@ -802,7 +887,7 @@ public class ExportSwift { abiReturnType = .i32 append("return Int32(ret.rawValue)") case .rawValueEnum(_, let rawType): - if rawType == "String" { + if rawType == .string { append( """ return ret.rawValue.withUTF8 { ptr in @@ -812,32 +897,22 @@ public class ExportSwift { ) } else { switch rawType { - case "Bool": + case .bool: append("return Int32(ret.rawValue ? 1 : 0)") - case "Int", "Int32", "UInt", "UInt32": + case .int, .int32, .uint, .uint32: append("return Int32(ret.rawValue)") - case "Int64", "UInt64": + case .int64, .uint64: append("return Int64(ret.rawValue)") - case "Float": + case .float: append("return Float32(ret.rawValue)") - case "Double": + case .double: append("return Float64(ret.rawValue)") default: - append("return Int32(ret.rawValue)") // Fallback + append("return Int32(ret.rawValue)") } } - case .associatedValueEnum: - append( - """ - let jsonData = try! JSONEncoder().encode(ret) - return String(data: jsonData, encoding: .utf8)!.withUTF8 { ptr in - _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) - } - """ - ) - case .namespaceEnum: - abiReturnType = .i32 - append("return 0") + case .associatedValueEnum: break; + case .namespaceEnum: break; case .jsObject(nil): append( """ @@ -987,25 +1062,26 @@ public class ExportSwift { /// ``` func renderSingleExportedClass(klass: ExportedClass) -> [DeclSyntax] { var decls: [DeclSyntax] = [] + if let constructor = klass.constructor { let builder = ExportedThunkBuilder(effects: constructor.effects) for param in constructor.parameters { builder.liftParameter(param: param) } - builder.call(name: klass.name, returnType: .swiftHeapObject(klass.name)) - builder.lowerReturnValue(returnType: .swiftHeapObject(klass.name)) + builder.call(name: klass.swiftCallName, returnType: BridgeType.swiftHeapObject(klass.name)) + builder.lowerReturnValue(returnType: BridgeType.swiftHeapObject(klass.name)) decls.append(builder.render(abiName: constructor.abiName)) } for method in klass.methods { let builder = ExportedThunkBuilder(effects: method.effects) builder.liftParameter( - param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name)) + param: Parameter(label: nil, name: "_self", type: BridgeType.swiftHeapObject(klass.swiftCallName)) ) for param in method.parameters { builder.liftParameter(param: param) } builder.callMethod( - klassName: klass.name, + klassName: klass.swiftCallName, methodName: method.name, returnType: method.returnType ) @@ -1019,7 +1095,7 @@ public class ExportSwift { @_expose(wasm, "bjs_\(raw: klass.name)_deinit") @_cdecl("bjs_\(raw: klass.name)_deinit") public func _bjs_\(raw: klass.name)_deinit(pointer: UnsafeMutableRawPointer) { - Unmanaged<\(raw: klass.name)>.fromOpaque(pointer).release() + Unmanaged<\(raw: klass.swiftCallName)>.fromOpaque(pointer).release() } """ ) @@ -1051,7 +1127,7 @@ public class ExportSwift { let externFunctionName = "bjs_\(klass.name)_wrap" return """ - extension \(raw: klass.name): ConvertibleToJSValue { + extension \(raw: klass.swiftCallName): ConvertibleToJSValue { var jsValue: JSValue { @_extern(wasm, module: "\(raw: moduleName)", name: "\(raw: externFunctionName)") func \(raw: wrapFunctionName)(_: UnsafeMutableRawPointer) -> Int32 @@ -1062,6 +1138,10 @@ public class ExportSwift { } } +fileprivate enum Constants { + static let supportedRawTypes = SwiftEnumRawType.allCases.map { $0.rawValue } +} + extension AttributeListSyntax { fileprivate func hasJSAttribute() -> Bool { firstJSAttribute != nil diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 07712a83f..0f40af965 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -69,6 +69,7 @@ struct BridgeJSLink { var dtsClassLines: [String] = [] var namespacedFunctions: [ExportedFunction] = [] var namespacedClasses: [ExportedClass] = [] + var namespacedEnums: [ExportedEnum] = [] var enumConstantLines: [String] = [] var dtsEnumLines: [String] = [] @@ -99,10 +100,15 @@ struct BridgeJSLink { } if !skeleton.enums.isEmpty { - for enumDef in skeleton.enums { - let (jsEnum, dtsEnum) = try renderExportedEnum(enumDef) + for enumDefinition in skeleton.enums { + let (jsEnum, dtsEnum) = try renderExportedEnum(enumDefinition) enumConstantLines.append(contentsOf: jsEnum) - exportsLines.append("\(enumDef.name),") + if enumDefinition.enumType != .namespace { + exportsLines.append("\(enumDefinition.name),") + if enumDefinition.namespace != nil { + namespacedEnums.append(enumDefinition) + } + } dtsEnumLines.append(contentsOf: dtsEnum) } } @@ -135,13 +141,22 @@ struct BridgeJSLink { importObjectBuilders.append(importObjectBuilder) } - let hasNamespacedItems = !namespacedFunctions.isEmpty || !namespacedClasses.isEmpty + let hasNamespacedItems = !namespacedFunctions.isEmpty || !namespacedClasses.isEmpty || !namespacedEnums.isEmpty + + let namespaceBuilder = NamespaceBuilder() + let namespaceDeclarationsLines = namespaceBuilder.namespaceDeclarations( + exportedSkeletons: exportedSkeletons, + renderTSSignatureCallback: { parameters, returnType, effects in + self.renderTSSignature(parameters: parameters, returnType: returnType, effects: effects) + } + ) let exportsSection: String if hasNamespacedItems { - let namespaceSetupCode = renderGlobalNamespace( + let namespaceSetupCode = namespaceBuilder.renderGlobalNamespace( namespacedFunctions: namespacedFunctions, - namespacedClasses: namespacedClasses + namespacedClasses: namespacedClasses, + namespacedEnums: namespacedEnums ) .map { $0.indent(count: 12) }.joined(separator: "\n") exportsSection = """ @@ -238,7 +253,7 @@ struct BridgeJSLink { """ var dtsLines: [String] = [] - dtsLines.append(contentsOf: namespaceDeclarations()) + dtsLines.append(contentsOf: namespaceDeclarationsLines) dtsLines.append(contentsOf: dtsClassLines) dtsLines.append(contentsOf: dtsEnumLines) dtsLines.append(contentsOf: generateImportedTypeDefinitions()) @@ -332,102 +347,6 @@ struct BridgeJSLink { return typeDefinitions } - private func namespaceDeclarations() -> [String] { - var dtsLines: [String] = [] - var namespaceFunctions: [String: [ExportedFunction]] = [:] - var namespaceClasses: [String: [ExportedClass]] = [:] - - for skeleton in exportedSkeletons { - for function in skeleton.functions { - if let namespace = function.namespace { - let namespaceKey = namespace.joined(separator: ".") - if namespaceFunctions[namespaceKey] == nil { - namespaceFunctions[namespaceKey] = [] - } - namespaceFunctions[namespaceKey]?.append(function) - } - } - - for klass in skeleton.classes { - if let classNamespace = klass.namespace { - let namespaceKey = classNamespace.joined(separator: ".") - if namespaceClasses[namespaceKey] == nil { - namespaceClasses[namespaceKey] = [] - } - namespaceClasses[namespaceKey]?.append(klass) - } - } - } - - guard !namespaceFunctions.isEmpty || !namespaceClasses.isEmpty else { return dtsLines } - - dtsLines.append("export {};") - dtsLines.append("") - dtsLines.append("declare global {") - - let identBaseSize = 4 - - for (namespacePath, classes) in namespaceClasses.sorted(by: { $0.key < $1.key }) { - let parts = namespacePath.split(separator: ".").map(String.init) - - for i in 0.. (js: [String], dts: [String]) { + func renderExportedEnum(_ enumDefinition: ExportedEnum) throws -> (js: [String], dts: [String]) { var jsLines: [String] = [] var dtsLines: [String] = [] - switch enumDef.enumType { + switch enumDefinition.enumType { case .simple: - jsLines.append("const \(enumDef.name) = {") - for (index, enumCase) in enumDef.cases.enumerated() { + jsLines.append("const \(enumDefinition.name) = {") + for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter jsLines.append(" \(caseName): \(index),".indent(count: 0)) } jsLines.append("};") jsLines.append("") - dtsLines.append("export const \(enumDef.name): {") - for (index, enumCase) in enumDef.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append(" readonly \(caseName): \(index);") + if enumDefinition.namespace == nil { + dtsLines.append("export const \(enumDefinition.name): {") + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append(" readonly \(caseName): \(index);") + } + dtsLines.append("};") + dtsLines.append( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + ) + dtsLines.append("") } - dtsLines.append("};") - dtsLines.append("export type \(enumDef.name) = typeof \(enumDef.name)[keyof typeof \(enumDef.name)];") - dtsLines.append("") case .rawValue: - guard let rawType = enumDef.rawType else { - throw BridgeJSLinkError(message: "Raw value enum \(enumDef.name) is missing rawType") + guard let rawType = enumDefinition.rawType else { + throw BridgeJSLinkError(message: "Raw value enum \(enumDefinition.name) is missing rawType") } - jsLines.append("const \(enumDef.name) = {") - for enumCase in enumDef.cases { + jsLines.append("const \(enumDefinition.name) = {") + for enumCase in enumDefinition.cases { let caseName = enumCase.name.capitalizedFirstLetter let rawValue = enumCase.rawValue ?? enumCase.name let formattedValue: String - switch rawType { - case "String": - formattedValue = "\"\(rawValue)\"" - case "Bool": - formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case "Float", "Double": - formattedValue = rawValue - default: + if let rawTypeEnum = SwiftEnumRawType.from(rawType) { + switch rawTypeEnum { + case .string: + formattedValue = "\"\(rawValue)\"" + case .bool: + formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case .float, .double: + formattedValue = rawValue + default: + formattedValue = rawValue + } + } else { formattedValue = rawValue } @@ -651,32 +575,38 @@ struct BridgeJSLink { jsLines.append("};") jsLines.append("") - dtsLines.append("export const \(enumDef.name): {") - for enumCase in enumDef.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue: String + if enumDefinition.namespace == nil { + dtsLines.append("export const \(enumDefinition.name): {") + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + + switch rawType { + case "String": + formattedValue = "\"\(rawValue)\"" + case "Bool": + formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": + formattedValue = rawValue + default: + formattedValue = rawValue + } - switch rawType { - case "String": - formattedValue = "\"\(rawValue)\"" - case "Bool": - formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case "Float", "Double": - formattedValue = rawValue - default: - formattedValue = rawValue + dtsLines.append(" readonly \(caseName): \(formattedValue);") } - - dtsLines.append(" readonly \(caseName): \(formattedValue);") + dtsLines.append("};") + dtsLines.append( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + ) + dtsLines.append("") } - dtsLines.append("};") - dtsLines.append("export type \(enumDef.name) = typeof \(enumDef.name)[keyof typeof \(enumDef.name)];") - dtsLines.append("") - case .associatedValue, .namespace: - jsLines.append("// TODO: Implement \(enumDef.enumType) enum: \(enumDef.name)") - dtsLines.append("// TODO: Implement \(enumDef.enumType) enum: \(enumDef.name)") + case .associatedValue: + jsLines.append("// TODO: Implement \(enumDefinition.enumType) enum: \(enumDefinition.name)") + dtsLines.append("// TODO: Implement \(enumDefinition.enumType) enum: \(enumDefinition.name)") + case .namespace: + break } return (jsLines, dtsLines) @@ -774,8 +704,11 @@ struct BridgeJSLink { return (jsLines, dtsTypeLines, dtsExportEntryLines) } - func renderGlobalNamespace(namespacedFunctions: [ExportedFunction], namespacedClasses: [ExportedClass]) -> [String] - { + func renderGlobalNamespace( + namespacedFunctions: [ExportedFunction], + namespacedClasses: [ExportedClass], + namespacedEnums: [ExportedEnum] + ) -> [String] { var lines: [String] = [] var uniqueNamespaces: [String] = [] var seen = Set() @@ -788,10 +721,15 @@ struct BridgeJSLink { namespacedClasses .compactMap { $0.namespace } ) + let enumNamespacePaths: Set<[String]> = Set( + namespacedEnums + .compactMap { $0.namespace } + ) let allNamespacePaths = functionNamespacePaths .union(classNamespacePaths) + .union(enumNamespacePaths) allNamespacePaths.forEach { namespacePath in namespacePath.makeIterator().enumerated().forEach { (index, _) in @@ -813,6 +751,11 @@ struct BridgeJSLink { lines.append("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);") } + namespacedEnums.forEach { enumDefinition in + let namespacePath: String = enumDefinition.namespace?.joined(separator: ".") ?? "" + lines.append("globalThis.\(namespacePath).\(enumDefinition.name) = exports.\(enumDefinition.name);") + } + namespacedFunctions.forEach { function in let namespacePath: String = function.namespace?.joined(separator: ".") ?? "" lines.append("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);") @@ -843,12 +786,12 @@ struct BridgeJSLink { parameterForwardings.append(param.name) case .rawValueEnum(_, let rawType): switch rawType { - case "String": + case .string: let stringObjectName = "\(param.name)Object" bodyLines.append("const \(stringObjectName) = swift.memory.getObject(\(param.name));") bodyLines.append("swift.memory.release(\(param.name));") parameterForwardings.append(stringObjectName) - case "Bool": + case .bool: parameterForwardings.append("\(param.name) !== 0") default: parameterForwardings.append(param.name) @@ -932,18 +875,18 @@ struct BridgeJSLink { return "ret" case .rawValueEnum(_, let rawType): switch rawType { - case "String": + case .string: bodyLines.append("tmpRetBytes = textEncoder.encode(ret);") return "tmpRetBytes.length" - case "Bool": + case .bool: return "ret ? 1 : 0" default: return "ret" } case .associatedValueEnum: - return "0" + return nil case .namespaceEnum: - return "0" + return nil case .int, .float, .double: return "ret" case .bool: @@ -979,6 +922,276 @@ struct BridgeJSLink { } } + struct NamespaceBuilder { + + /// Generates JavaScript code for setting up global namespace structure + /// + /// This function creates the necessary JavaScript code to properly expose namespaced + /// functions, classes, and enums on the global object (globalThis). It ensures that + /// nested namespace paths are created correctly and that all exported items are + /// accessible through their full namespace paths. + /// + /// For example, if you have @JS("Utils.Math") func add() it will generate code that + /// makes globalThis.Utils.Math.add accessible. + /// + /// - Parameters: + /// - namespacedFunctions: Functions annotated with @JS("namespace.path") + /// - namespacedClasses: Classes annotated with @JS("namespace.path") + /// - namespacedEnums: Enums annotated with @JS("namespace.path") + /// - Returns: Array of JavaScript code lines that set up the global namespace structure + func renderGlobalNamespace( + namespacedFunctions: [ExportedFunction], + namespacedClasses: [ExportedClass], + namespacedEnums: [ExportedEnum] + ) -> [String] { + var lines: [String] = [] + var uniqueNamespaces: [String] = [] + var seen = Set() + + let functionNamespacePaths: Set<[String]> = Set( + namespacedFunctions + .compactMap { $0.namespace } + ) + let classNamespacePaths: Set<[String]> = Set( + namespacedClasses + .compactMap { $0.namespace } + ) + let enumNamespacePaths: Set<[String]> = Set( + namespacedEnums + .compactMap { $0.namespace } + ) + + let allNamespacePaths = + functionNamespacePaths + .union(classNamespacePaths) + .union(enumNamespacePaths) + + allNamespacePaths.forEach { namespacePath in + namespacePath.makeIterator().enumerated().forEach { (index, _) in + let path = namespacePath[0...index].joined(separator: ".") + if seen.insert(path).inserted { + uniqueNamespaces.append(path) + } + } + } + + uniqueNamespaces.sorted().forEach { namespace in + lines.append("if (typeof globalThis.\(namespace) === 'undefined') {") + lines.append(" globalThis.\(namespace) = {};") + lines.append("}") + } + + namespacedClasses.forEach { klass in + let namespacePath: String = klass.namespace?.joined(separator: ".") ?? "" + lines.append("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);") + } + + namespacedEnums.forEach { enumDefinition in + let namespacePath: String = enumDefinition.namespace?.joined(separator: ".") ?? "" + lines.append("globalThis.\(namespacePath).\(enumDefinition.name) = exports.\(enumDefinition.name);") + } + + namespacedFunctions.forEach { function in + let namespacePath: String = function.namespace?.joined(separator: ".") ?? "" + lines.append("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);") + } + + return lines + } + + private struct NamespaceContent { + var functions: [ExportedFunction] = [] + var classes: [ExportedClass] = [] + var enums: [ExportedEnum] = [] + } + + private final class NamespaceNode { + let name: String + var children: [String: NamespaceNode] = [:] + var content: NamespaceContent = NamespaceContent() + + init(name: String) { + self.name = name + } + + func addChild(_ childName: String) -> NamespaceNode { + if let existing = children[childName] { + return existing + } + let newChild = NamespaceNode(name: childName) + children[childName] = newChild + return newChild + } + } + + /// Generates TypeScript declarations for all namespaces + /// + /// This function enables properly grouping all Swift code within given namespaces + /// regardless of location in Swift input files. It uses a tree-based structure to + /// properly create unique namespace declarations that avoid namespace duplication in TS and generate + /// predictable declarations in sorted order. + /// + /// The function collects all namespaced items (functions, classes, enums) from the + /// exported skeletons and builds a hierarchical namespace tree. It then traverses + /// this tree to generate TypeScript namespace declarations that mirror the Swift + /// namespace structure. + /// - Parameters: + /// - exportedSkeletons: Exported Swift structures to generate namespaces for + /// - renderTSSignatureCallback: closure to generate TS signature that aligns with rest of codebase + /// - Returns: Array of TypeScript declaration lines defining the global namespace structure + func namespaceDeclarations( + exportedSkeletons: [ExportedSkeleton], + renderTSSignatureCallback: @escaping ([Parameter], BridgeType, Effects) -> String + ) -> [String] { + var dtsLines: [String] = [] + + let rootNode = NamespaceNode(name: "") + + for skeleton in exportedSkeletons { + for function in skeleton.functions { + if let namespace = function.namespace { + var currentNode = rootNode + for part in namespace { + currentNode = currentNode.addChild(part) + } + currentNode.content.functions.append(function) + } + } + + for klass in skeleton.classes { + if let classNamespace = klass.namespace { + var currentNode = rootNode + for part in classNamespace { + currentNode = currentNode.addChild(part) + } + currentNode.content.classes.append(klass) + } + } + + for enumDefinition in skeleton.enums { + if let enumNamespace = enumDefinition.namespace, enumDefinition.enumType != .namespace { + var currentNode = rootNode + for part in enumNamespace { + currentNode = currentNode.addChild(part) + } + currentNode.content.enums.append(enumDefinition) + } + } + } + + guard !rootNode.children.isEmpty else { + return dtsLines + } + + dtsLines.append("export {};") + dtsLines.append("") + dtsLines.append("declare global {") + + let identBaseSize = 4 + + func generateNamespaceDeclarations(node: NamespaceNode, depth: Int) { + let sortedChildren = node.children.sorted { $0.key < $1.key } + + for (childName, childNode) in sortedChildren { + dtsLines.append("namespace \(childName) {".indent(count: identBaseSize * depth)) + + let contentDepth = depth + 1 + + let sortedClasses = childNode.content.classes.sorted { $0.name < $1.name } + for klass in sortedClasses { + dtsLines.append("class \(klass.name) {".indent(count: identBaseSize * contentDepth)) + + if let constructor = klass.constructor { + let constructorSignature = + "constructor(\(constructor.parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", ")));" + dtsLines.append("\(constructorSignature)".indent(count: identBaseSize * (contentDepth + 1))) + } + + let sortedMethods = klass.methods.sorted { $0.name < $1.name } + for method in sortedMethods { + let methodSignature = + "\(method.name)\(renderTSSignatureCallback(method.parameters, method.returnType, method.effects));" + dtsLines.append("\(methodSignature)".indent(count: identBaseSize * (contentDepth + 1))) + } + + dtsLines.append("}".indent(count: identBaseSize * contentDepth)) + } + + let sortedEnums = childNode.content.enums.sorted { $0.name < $1.name } + for enumDefinition in sortedEnums { + switch enumDefinition.enumType { + case .simple: + dtsLines.append( + "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) + ) + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append( + "readonly \(caseName): \(index);".indent(count: identBaseSize * (contentDepth + 1)) + ) + } + dtsLines.append("};".indent(count: identBaseSize * contentDepth)) + dtsLines.append( + "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + .indent( + count: identBaseSize * contentDepth + ) + ) + case .rawValue: + guard let rawType = enumDefinition.rawType else { continue } + dtsLines.append( + "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) + ) + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + switch rawType { + case "String": formattedValue = "\"\(rawValue)\"" + case "Bool": formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": formattedValue = rawValue + default: formattedValue = rawValue + } + dtsLines.append( + "readonly \(caseName): \(formattedValue);".indent( + count: identBaseSize * (contentDepth + 1) + ) + ) + } + dtsLines.append("};".indent(count: identBaseSize * contentDepth)) + dtsLines.append( + "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + .indent( + count: identBaseSize * contentDepth + ) + ) + case .associatedValue, .namespace: + continue + } + } + + let sortedFunctions = childNode.content.functions.sorted { $0.name < $1.name } + for function in sortedFunctions { + let signature = + "\(function.name)\(renderTSSignatureCallback(function.parameters, function.returnType, function.effects));" + dtsLines.append("\(signature)".indent(count: identBaseSize * contentDepth)) + } + + generateNamespaceDeclarations(node: childNode, depth: contentDepth) + + dtsLines.append("}".indent(count: identBaseSize * depth)) + } + } + + generateNamespaceDeclarations(node: rootNode, depth: 1) + + dtsLines.append("}") + dtsLines.append("") + + return dtsLines + } + } + func renderImportedFunction( importObjectBuilder: ImportObjectBuilder, function: ImportedFunctionSkeleton diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index e1f1b3ef7..89f2771e4 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -2,16 +2,10 @@ // MARK: - Types -public enum Constants { - public static let supportedRawTypes = [ - "String", "Bool", "Int", "Int32", "Int64", "UInt", "UInt32", "UInt64", "Float", "Double", - ] -} - public enum BridgeType: Codable, Equatable { case int, float, double, string, bool, jsObject(String?), swiftHeapObject(String), void case caseEnum(String) - case rawValueEnum(String, String) + case rawValueEnum(String, SwiftEnumRawType) case associatedValueEnum(String) case namespaceEnum(String) } @@ -20,6 +14,39 @@ public enum WasmCoreType: String, Codable { case i32, i64, f32, f64, pointer } +/// Represents supported Swift enum raw types with their WASM ABI properties +public enum SwiftEnumRawType: String, CaseIterable, Codable { + case string = "String" + case bool = "Bool" + case int = "Int" + case int32 = "Int32" + case int64 = "Int64" + case uint = "UInt" + case uint32 = "UInt32" + case uint64 = "UInt64" + case float = "Float" + case double = "Double" + + public var wasmCoreType: WasmCoreType? { + switch self { + case .string: + return nil // String has special handling with UTF-8 bytes and length + case .bool, .int, .int32, .uint, .uint32: + return .i32 + case .int64, .uint64: + return .i64 + case .float: + return .f32 + case .double: + return .f64 + } + } + + public static func from(_ rawTypeString: String) -> SwiftEnumRawType? { + return Self.allCases.first { $0.rawValue == rawTypeString } + } +} + public struct Parameter: Codable { public let label: String? public let name: String @@ -72,11 +99,10 @@ public struct EnumCase: Codable, Equatable { public struct ExportedEnum: Codable, Equatable { public let name: String + public let swiftCallName: String public let cases: [EnumCase] public let rawType: String? public let namespace: [String]? - public let nestedTypes: [String] - public var enumType: EnumType { if cases.isEmpty { return .namespace @@ -87,12 +113,18 @@ public struct ExportedEnum: Codable, Equatable { } } - public init(name: String, cases: [EnumCase], rawType: String?, namespace: [String]?, nestedTypes: [String]) { + public init( + name: String, + swiftCallName: String, + cases: [EnumCase], + rawType: String?, + namespace: [String]? + ) { self.name = name + self.swiftCallName = swiftCallName self.cases = cases self.rawType = rawType self.namespace = namespace - self.nestedTypes = nestedTypes } } @@ -132,17 +164,20 @@ public struct ExportedFunction: Codable { public struct ExportedClass: Codable { public var name: String + public var swiftCallName: String public var constructor: ExportedConstructor? public var methods: [ExportedFunction] public var namespace: [String]? public init( name: String, + swiftCallName: String, constructor: ExportedConstructor? = nil, methods: [ExportedFunction], namespace: [String]? = nil ) { self.name = name + self.swiftCallName = swiftCallName self.constructor = constructor self.methods = methods self.namespace = namespace @@ -254,58 +289,11 @@ extension BridgeType { case .caseEnum: return .i32 case .rawValueEnum(_, let rawType): - switch rawType { - case "String": - return nil // String uses special handling - case "Bool", "Int", "Int32", "UInt", "UInt32": - return .i32 - case "Int64", "UInt64": - return .i64 - case "Float": - return .f32 - case "Double": - return .f64 - default: - return nil - } + return rawType.wasmCoreType case .associatedValueEnum: return nil case .namespaceEnum: return nil } } - - /// Returns the Swift type name for this bridge type - var swiftTypeName: String { - switch self { - case .void: return "Void" - case .bool: return "Bool" - case .int: return "Int" - case .float: return "Float" - case .double: return "Double" - case .string: return "String" - case .jsObject(let name): return name ?? "JSObject" - case .swiftHeapObject(let name): return name - case .caseEnum(let name): return name - case .rawValueEnum(let name, _): return name - case .associatedValueEnum(let name): return name - case .namespaceEnum(let name): return name - } - } - - /// Returns the TypeScript type name for this bridge type - var tsTypeName: String { - switch self { - case .void: return "void" - case .bool: return "boolean" - case .int, .float, .double: return "number" - case .string: return "string" - case .jsObject(let name): return name ?? "any" - case .swiftHeapObject(let name): return name - case .caseEnum(let name): return name - case .rawValueEnum(let name, _): return name - case .associatedValueEnum(let name): return name - case .namespaceEnum(let name): return name - } - } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift index 15ea35845..68c666db4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift @@ -1,4 +1,5 @@ -// Empty enum to act as namespace wrapper for classes +// Empty enums to act as namespace wrappers for nested elements + @JS enum Utils { @JS class Converter { @JS init() {} @@ -9,14 +10,19 @@ } } -// TODO: Add namespace enum with static functions when supported - @JS enum Networking { - @JS enum Method { - case get - case post - case put - case delete + @JS enum API { + @JS enum Method { + case get + case post + case put + case delete + } + // Invalid to declare @JS(namespace) here as it would be conflicting with nesting + @JS class HTTPServer { + @JS init() {} + @JS func call(_ method: Method) + } } } @@ -34,3 +40,17 @@ case development = 3000 } } + +@JS(namespace: "Networking.APIV2") +enum Internal { + @JS enum SupportedMethod { + case get + case post + } + @JS class TestServer { + @JS init() {} + @JS func call(_ method: SupportedMethod) + } +} + +// TODO: Add namespace enum with static functions when supported diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift index 3948470fa..bf1a69ccf 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift @@ -61,11 +61,6 @@ case pi = 3.14159 } -@JS enum FeatureFlag: Bool { - case enabled = true - case disabled = false -} - @JS func setTheme(_ theme: Theme) @JS func getTheme() -> Theme diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts index 5a1fb674f..3d37ca6c9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts @@ -4,10 +4,86 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. -// TODO: Implement namespace enum: Utils -// TODO: Implement namespace enum: Networking -// TODO: Implement namespace enum: Configuration +export {}; + +declare global { + namespace Configuration { + const LogLevel: { + readonly Debug: "debug"; + readonly Info: "info"; + readonly Warning: "warning"; + readonly Error: "error"; + }; + type LogLevel = typeof LogLevel[keyof typeof LogLevel]; + const Port: { + readonly Http: 80; + readonly Https: 443; + readonly Development: 3000; + }; + type Port = typeof Port[keyof typeof Port]; + } + namespace Networking { + namespace API { + class HTTPServer { + constructor(); + call(method: Networking.API.Method): void; + } + const Method: { + readonly Get: 0; + readonly Post: 1; + readonly Put: 2; + readonly Delete: 3; + }; + type Method = typeof Method[keyof typeof Method]; + } + namespace APIV2 { + namespace Internal { + class TestServer { + constructor(); + call(method: Internal.SupportedMethod): void; + } + const SupportedMethod: { + readonly Get: 0; + readonly Post: 1; + }; + type SupportedMethod = typeof SupportedMethod[keyof typeof SupportedMethod]; + } + } + } + namespace Utils { + class Converter { + constructor(); + toString(value: number): string; + } + } +} + +/// 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 Converter extends SwiftHeapObject { + toString(value: number): string; +} +export interface HTTPServer extends SwiftHeapObject { + call(method: Networking.API.Method): void; +} +export interface TestServer extends SwiftHeapObject { + call(method: Internal.SupportedMethod): void; +} export type Exports = { + Converter: { + new(): Converter; + } + HTTPServer: { + new(): HTTPServer; + } + TestServer: { + new(): TestServer; + } } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js index 0136a85f7..530822a52 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js @@ -49,7 +49,22 @@ export async function createInstantiator(options, swift) { bjs["swift_js_release"] = function(id) { swift.memory.release(id); } - + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_Converter_wrap"] = function(pointer) { + const obj = Converter.__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_HTTPServer_wrap"] = function(pointer) { + const obj = HTTPServer.__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_TestServer_wrap"] = function(pointer) { + const obj = TestServer.__construct(pointer); + return swift.memory.retain(obj); + }; }, setInstance: (i) => { @@ -62,15 +77,132 @@ export async function createInstantiator(options, swift) { /** @param {WebAssembly.Instance} instance */ createExports: (instance) => { const js = swift.memory.heap; - - // TODO: Implement namespace enum: Utils - // TODO: Implement namespace enum: Networking - // TODO: Implement namespace enum: Configuration - return { - Utils, - Networking, - Configuration, + /// 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 Converter extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Converter_deinit, Converter.prototype); + } + + + constructor() { + const ret = instance.exports.bjs_Converter_init(); + return Converter.__construct(ret); + } + toString(value) { + instance.exports.bjs_Converter_toString(this.pointer, value); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + } + class HTTPServer extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_HTTPServer_deinit, HTTPServer.prototype); + } + + + constructor() { + const ret = instance.exports.bjs_HTTPServer_init(); + return HTTPServer.__construct(ret); + } + call(method) { + instance.exports.bjs_HTTPServer_call(this.pointer, method | 0); + } + } + class TestServer extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_TestServer_deinit, TestServer.prototype); + } + + + constructor() { + const ret = instance.exports.bjs_TestServer_init(); + return TestServer.__construct(ret); + } + call(method) { + instance.exports.bjs_TestServer_call(this.pointer, method | 0); + } + } + const Method = { + Get: 0, + Post: 1, + Put: 2, + Delete: 3, + }; + + const LogLevel = { + Debug: "debug", + Info: "info", + Warning: "warning", + Error: "error", }; + + const Port = { + Http: 80, + Https: 443, + Development: 3000, + }; + + const SupportedMethod = { + Get: 0, + Post: 1, + }; + + const exports = { + Converter, + HTTPServer, + TestServer, + Method, + LogLevel, + Port, + SupportedMethod, + }; + + if (typeof globalThis.Configuration === 'undefined') { + globalThis.Configuration = {}; + } + if (typeof globalThis.Networking === 'undefined') { + globalThis.Networking = {}; + } + if (typeof globalThis.Networking.API === 'undefined') { + globalThis.Networking.API = {}; + } + if (typeof globalThis.Networking.APIV2 === 'undefined') { + globalThis.Networking.APIV2 = {}; + } + if (typeof globalThis.Networking.APIV2.Internal === 'undefined') { + globalThis.Networking.APIV2.Internal = {}; + } + if (typeof globalThis.Utils === 'undefined') { + globalThis.Utils = {}; + } + globalThis.Utils.Converter = exports.Converter; + globalThis.Networking.API.HTTPServer = exports.HTTPServer; + globalThis.Networking.APIV2.Internal.TestServer = exports.TestServer; + globalThis.Networking.API.Method = exports.Method; + globalThis.Configuration.LogLevel = exports.LogLevel; + globalThis.Configuration.Port = exports.Port; + globalThis.Networking.APIV2.Internal.SupportedMethod = exports.SupportedMethod; + + return exports; }, } } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts index 7f1ededcc..d67ee1fee 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts @@ -77,12 +77,6 @@ export const Ratio: { }; export type Ratio = typeof Ratio[keyof typeof Ratio]; -export const FeatureFlag: { - readonly Enabled: true; - readonly Disabled: false; -}; -export type FeatureFlag = typeof FeatureFlag[keyof typeof FeatureFlag]; - export type Exports = { setTheme(theme: Theme): void; getTheme(): Theme; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js index e63be4653..f4615d99c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js @@ -126,11 +126,6 @@ export async function createInstantiator(options, swift) { Pi: 3.14159, }; - const FeatureFlag = { - Enabled: true, - Disabled: false, - }; - return { Theme, FeatureFlag, @@ -142,7 +137,6 @@ export async function createInstantiator(options, swift) { SessionId, Precision, Ratio, - FeatureFlag, setTheme: function bjs_setTheme(theme) { const themeBytes = textEncoder.encode(theme); const themeId = swift.memory.retain(themeBytes); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts index b2ccecc41..c6e403998 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts @@ -7,6 +7,11 @@ export {}; declare global { + namespace MyModule { + namespace Utils { + namespacedFunction(): string; + } + } namespace Utils { namespace Converters { class Converter { @@ -26,11 +31,6 @@ declare global { } } } - namespace MyModule { - namespace Utils { - function namespacedFunction(): string; - } - } } /// Represents a Swift heap object like a class instance or an actor instance. diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json index 3a29a1a1c..7527eef96 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json @@ -31,9 +31,7 @@ } ], "name" : "Direction", - "nestedTypes" : [ - - ] + "swiftCallName" : "Direction" }, { "cases" : [ @@ -57,9 +55,7 @@ } ], "name" : "Status", - "nestedTypes" : [ - - ] + "swiftCallName" : "Status" } ], "functions" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json index fc427dec6..f7dd38729 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json @@ -1,6 +1,137 @@ { "classes" : [ + { + "constructor" : { + "abiName" : "bjs_Converter_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_Converter_toString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "toString", + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "string" : { + } + } + } + ], + "name" : "Converter", + "namespace" : [ + "Utils" + ], + "swiftCallName" : "Utils.Converter" + }, + { + "constructor" : { + "abiName" : "bjs_HTTPServer_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_HTTPServer_call", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "call", + "parameters" : [ + { + "label" : "_", + "name" : "method", + "type" : { + "caseEnum" : { + "_0" : "Networking.API.Method" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "HTTPServer", + "namespace" : [ + "Networking", + "API" + ], + "swiftCallName" : "Networking.API.HTTPServer" + }, + { + "constructor" : { + "abiName" : "bjs_TestServer_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_TestServer_call", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "call", + "parameters" : [ + { + "label" : "_", + "name" : "method", + "type" : { + "caseEnum" : { + "_0" : "Internal.SupportedMethod" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "TestServer", + "namespace" : [ + "Networking", + "APIV2", + "Internal" + ], + "swiftCallName" : "Internal.TestServer" + } ], "enums" : [ { @@ -8,28 +139,133 @@ ], "name" : "Utils", - "nestedTypes" : [ - "Converter" - ] + "swiftCallName" : "Utils" }, { "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "get" + }, + { + "associatedValues" : [ + + ], + "name" : "post" + }, + { + "associatedValues" : [ + + ], + "name" : "put" + }, + { + "associatedValues" : [ + ], + "name" : "delete" + } ], - "name" : "Networking", - "nestedTypes" : [ - "Method" - ] + "name" : "Method", + "namespace" : [ + "Networking", + "API" + ], + "swiftCallName" : "Networking.API.Method" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "debug", + "rawValue" : "debug" + }, + { + "associatedValues" : [ + + ], + "name" : "info", + "rawValue" : "info" + }, + { + "associatedValues" : [ + + ], + "name" : "warning", + "rawValue" : "warning" + }, + { + "associatedValues" : [ + + ], + "name" : "error", + "rawValue" : "error" + } + ], + "name" : "LogLevel", + "namespace" : [ + "Configuration" + ], + "rawType" : "String", + "swiftCallName" : "Configuration.LogLevel" }, { "cases" : [ + { + "associatedValues" : [ + ], + "name" : "http", + "rawValue" : "80" + }, + { + "associatedValues" : [ + + ], + "name" : "https", + "rawValue" : "443" + }, + { + "associatedValues" : [ + + ], + "name" : "development", + "rawValue" : "3000" + } + ], + "name" : "Port", + "namespace" : [ + "Configuration" + ], + "rawType" : "Int", + "swiftCallName" : "Configuration.Port" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "get" + }, + { + "associatedValues" : [ + + ], + "name" : "post" + } + ], + "name" : "SupportedMethod", + "namespace" : [ + "Networking", + "APIV2", + "Internal" ], - "name" : "Configuration", - "nestedTypes" : [ - "LogLevel", - "Port" - ] + "swiftCallName" : "Internal.SupportedMethod" } ], "functions" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift index e241d1805..fa77613fa 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift @@ -4,4 +4,112 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. -@_spi(BridgeJS) import JavaScriptKit \ No newline at end of file +@_spi(BridgeJS) import JavaScriptKit + +@_expose(wasm, "bjs_Converter_init") +@_cdecl("bjs_Converter_init") +public func _bjs_Converter_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Utils.Converter() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Converter_toString") +@_cdecl("bjs_Converter_toString") +public func _bjs_Converter_toString(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().toString(value: Int(value)) + return ret.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Converter_deinit") +@_cdecl("bjs_Converter_deinit") +public func _bjs_Converter_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension Utils.Converter: ConvertibleToJSValue { + var jsValue: JSValue { + @_extern(wasm, module: "TestModule", name: "bjs_Converter_wrap") + func _bjs_Converter_wrap(_: UnsafeMutableRawPointer) -> Int32 + return .object(JSObject(id: UInt32(bitPattern: _bjs_Converter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +@_expose(wasm, "bjs_HTTPServer_init") +@_cdecl("bjs_HTTPServer_init") +public func _bjs_HTTPServer_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Networking.API.HTTPServer() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_HTTPServer_call") +@_cdecl("bjs_HTTPServer_call") +public func _bjs_HTTPServer_call(_self: UnsafeMutableRawPointer, method: Int32) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Networking.API.Method(rawValue: Int(method))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_HTTPServer_deinit") +@_cdecl("bjs_HTTPServer_deinit") +public func _bjs_HTTPServer_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension Networking.API.HTTPServer: ConvertibleToJSValue { + var jsValue: JSValue { + @_extern(wasm, module: "TestModule", name: "bjs_HTTPServer_wrap") + func _bjs_HTTPServer_wrap(_: UnsafeMutableRawPointer) -> Int32 + return .object(JSObject(id: UInt32(bitPattern: _bjs_HTTPServer_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +@_expose(wasm, "bjs_TestServer_init") +@_cdecl("bjs_TestServer_init") +public func _bjs_TestServer_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Internal.TestServer() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_TestServer_call") +@_cdecl("bjs_TestServer_call") +public func _bjs_TestServer_call(_self: UnsafeMutableRawPointer, method: Int32) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Internal.SupportedMethod(rawValue: Int(method))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_TestServer_deinit") +@_cdecl("bjs_TestServer_deinit") +public func _bjs_TestServer_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension Internal.TestServer: ConvertibleToJSValue { + var jsValue: JSValue { + @_extern(wasm, module: "TestModule", name: "bjs_TestServer_wrap") + func _bjs_TestServer_wrap(_: UnsafeMutableRawPointer) -> Int32 + return .object(JSObject(id: UInt32(bitPattern: _bjs_TestServer_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json index 5b88d2755..d85bc5501 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json @@ -28,10 +28,8 @@ } ], "name" : "Theme", - "nestedTypes" : [ - - ], - "rawType" : "String" + "rawType" : "String", + "swiftCallName" : "Theme" }, { "cases" : [ @@ -51,10 +49,8 @@ } ], "name" : "FeatureFlag", - "nestedTypes" : [ - - ], - "rawType" : "Bool" + "rawType" : "Bool", + "swiftCallName" : "FeatureFlag" }, { "cases" : [ @@ -81,10 +77,8 @@ } ], "name" : "HttpStatus", - "nestedTypes" : [ - - ], - "rawType" : "Int" + "rawType" : "Int", + "swiftCallName" : "HttpStatus" }, { "cases" : [ @@ -125,10 +119,8 @@ } ], "name" : "Priority", - "nestedTypes" : [ - - ], - "rawType" : "Int32" + "rawType" : "Int32", + "swiftCallName" : "Priority" }, { "cases" : [ @@ -162,10 +154,8 @@ } ], "name" : "FileSize", - "nestedTypes" : [ - - ], - "rawType" : "Int64" + "rawType" : "Int64", + "swiftCallName" : "FileSize" }, { "cases" : [ @@ -192,10 +182,8 @@ } ], "name" : "UserId", - "nestedTypes" : [ - - ], - "rawType" : "UInt" + "rawType" : "UInt", + "swiftCallName" : "UserId" }, { "cases" : [ @@ -222,10 +210,8 @@ } ], "name" : "TokenId", - "nestedTypes" : [ - - ], - "rawType" : "UInt32" + "rawType" : "UInt32", + "swiftCallName" : "TokenId" }, { "cases" : [ @@ -252,10 +238,8 @@ } ], "name" : "SessionId", - "nestedTypes" : [ - - ], - "rawType" : "UInt64" + "rawType" : "UInt64", + "swiftCallName" : "SessionId" }, { "cases" : [ @@ -282,10 +266,8 @@ } ], "name" : "Precision", - "nestedTypes" : [ - - ], - "rawType" : "Float" + "rawType" : "Float", + "swiftCallName" : "Precision" }, { "cases" : [ @@ -319,33 +301,8 @@ } ], "name" : "Ratio", - "nestedTypes" : [ - - ], - "rawType" : "Double" - }, - { - "cases" : [ - { - "associatedValues" : [ - - ], - "name" : "enabled", - "rawValue" : "true" - }, - { - "associatedValues" : [ - - ], - "name" : "disabled", - "rawValue" : "false" - } - ], - "name" : "FeatureFlag", - "nestedTypes" : [ - - ], - "rawType" : "Bool" + "rawType" : "Double", + "swiftCallName" : "Ratio" } ], "functions" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json index 79ff94d8b..a5e960beb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json @@ -41,7 +41,8 @@ "namespace" : [ "__Swift", "Foundation" - ] + ], + "swiftCallName" : "Greeter" }, { "constructor" : { @@ -84,7 +85,8 @@ "namespace" : [ "Utils", "Converters" - ] + ], + "swiftCallName" : "Converter" }, { "methods" : [ @@ -109,7 +111,8 @@ "namespace" : [ "__Swift", "Foundation" - ] + ], + "swiftCallName" : "UUID" } ], "enums" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json index 3f621eb69..7f8324ac1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json @@ -61,7 +61,8 @@ } } ], - "name" : "Greeter" + "name" : "Greeter", + "swiftCallName" : "Greeter" } ], "enums" : [ From 6333085e25dd8533abb3dd498823dc2d0b6f3274 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Wed, 20 Aug 2025 17:26:54 +0200 Subject: [PATCH 3/4] BridgeJS: Support TS enum style syntax for raw type string and numeric type + docs --- .../JavaScript/BridgeJS.ExportSwift.json | 3 + .../JavaScript/BridgeJS.ExportSwift.json | 9 +- .../Sources/BridgeJSCore/ExportSwift.swift | 84 +++- .../Sources/BridgeJSLink/BridgeJSLink.swift | 217 +++++++--- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 13 +- .../TS2Skeleton/JavaScript/package-lock.json | 28 -- .../TS2Skeleton/JavaScript/package.json | 2 +- .../BridgeJSToolTests/Inputs/EnumCase.swift | 10 + .../Inputs/EnumRawType.swift | 18 + .../ArrayParameter.Import.js | 1 - .../BridgeJSLinkTests/Async.Export.js | 1 - .../BridgeJSLinkTests/Async.Import.js | 1 - .../BridgeJSLinkTests/EnumCase.Export.d.ts | 9 + .../BridgeJSLinkTests/EnumCase.Export.js | 15 + .../BridgeJSLinkTests/EnumRawType.Export.d.ts | 16 + .../BridgeJSLinkTests/EnumRawType.Export.js | 33 ++ .../BridgeJSLinkTests/Interface.Import.js | 1 - .../MultipleImportedTypes.Import.js | 1 - .../BridgeJSLinkTests/Namespaces.Export.js | 1 - .../PrimitiveParameters.Export.js | 1 - .../PrimitiveParameters.Import.js | 1 - .../PrimitiveReturn.Export.js | 1 - .../PrimitiveReturn.Import.js | 1 - .../StringParameter.Export.js | 1 - .../StringParameter.Import.js | 1 - .../BridgeJSLinkTests/StringReturn.Export.js | 1 - .../BridgeJSLinkTests/StringReturn.Import.js | 1 - .../BridgeJSLinkTests/SwiftClass.Export.js | 1 - .../TS2SkeletonLike.Import.js | 1 - .../BridgeJSLinkTests/Throws.Export.js | 1 - .../BridgeJSLinkTests/TypeAlias.Import.js | 1 - .../TypeScriptClass.Import.js | 1 - .../VoidParameterVoidReturn.Export.js | 1 - .../VoidParameterVoidReturn.Import.js | 1 - .../ExportSwiftTests/EnumCase.json | 73 ++++ .../ExportSwiftTests/EnumCase.swift | 115 ++++- .../ExportSwiftTests/EnumNamespace.json | 5 + .../ExportSwiftTests/EnumNamespace.swift | 56 ++- .../ExportSwiftTests/EnumRawType.json | 152 +++++++ .../ExportSwiftTests/EnumRawType.swift | 48 +++ .../BridgeJS/Exporting-Swift-to-JavaScript.md | 407 +++++++++++++++++- Sources/JavaScriptKit/Macros.swift | 15 +- .../JavaScript/BridgeJS.ExportSwift.json | 9 +- 43 files changed, 1211 insertions(+), 147 deletions(-) delete mode 100644 Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json diff --git a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json index 2e94644d8..b00ec9abb 100644 --- a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -1,6 +1,9 @@ { "classes" : [ + ], + "enums" : [ + ], "functions" : [ { diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json index e83af9fe9..2b5ce07d9 100644 --- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -46,7 +46,8 @@ } } ], - "name" : "PlayBridgeJS" + "name" : "PlayBridgeJS", + "swiftCallName" : "PlayBridgeJS" }, { "methods" : [ @@ -115,8 +116,12 @@ } } ], - "name" : "PlayBridgeJSOutput" + "name" : "PlayBridgeJSOutput", + "swiftCallName" : "PlayBridgeJSOutput" } + ], + "enums" : [ + ], "functions" : [ diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 211f71390..0b5e7f208 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -267,6 +267,24 @@ public class ExportSwift { return namespaceString.split(separator: ".").map(String.init) } + private func extractEnumStyle( + from jsAttribute: AttributeSyntax + ) -> EnumEmitStyle? { + guard let arguments = jsAttribute.arguments?.as(LabeledExprListSyntax.self), + let styleArg = arguments.first(where: { $0.label?.text == "enumStyle" }) + else { + return nil + } + let text = styleArg.expression.trimmedDescription + if text.contains("tsEnum") { + return .tsEnum + } + if text.contains("const") { + return .const + } + return nil + } + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { return .skipChildren } guard case .classBody(let className, _) = state else { @@ -318,9 +336,6 @@ public class ExportSwift { let name = node.name.text guard let jsAttribute = node.attributes.firstJSAttribute else { - if case .enumBody(_) = state { - return .skipChildren - } return .skipChildren } @@ -355,14 +370,14 @@ public class ExportSwift { } override func visitPost(_ node: ClassDeclSyntax) { - stateStack.pop() + // Make sure we pop the state stack only if we're in a class body state (meaning we successfully pushed) + if case .classBody(_, _) = stateStack.current { + stateStack.pop() + } } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { - if case .enumBody(_) = state { - return .skipChildren - } return .skipChildren } @@ -402,6 +417,10 @@ public class ExportSwift { guard let jsAttribute = node.attributes.firstJSAttribute, let enumName = currentEnum.name else { + // Only pop if we have a valid enum that was processed + if case .enumBody(_) = stateStack.current { + stateStack.pop() + } return } @@ -415,13 +434,26 @@ public class ExportSwift { effectiveNamespace = computedNamespace } + let emitStyle = extractEnumStyle(from: jsAttribute) ?? .const + if case .tsEnum = emitStyle, + let raw = currentEnum.rawType, + let rawEnum = SwiftEnumRawType.from(raw), rawEnum == .bool + { + diagnose( + node: jsAttribute, + message: "TypeScript enum style is not supported for Bool raw-value enums", + hint: "Use enumStyle: .const or change the raw type to String or a numeric type" + ) + } + let swiftCallName = ExportSwift.computeSwiftCallName(for: node, itemName: enumName) let exportedEnum = ExportedEnum( name: enumName, swiftCallName: swiftCallName, cases: currentEnum.cases, rawType: currentEnum.rawType, - namespace: effectiveNamespace + namespace: effectiveNamespace, + emitStyle: emitStyle ) exportedEnumByName[enumName] = exportedEnum exportedEnumNames.append(enumName) @@ -614,6 +646,11 @@ public class ExportSwift { return nil } decls.append(Self.prelude) + + for enumDef in exportedEnums where enumDef.enumType == .simple { + decls.append(renderCaseEnumHelpers(enumDef)) + } + for function in exportedFunctions { decls.append(renderSingleExportedFunction(function: function)) } @@ -624,6 +661,32 @@ public class ExportSwift { return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") } + func renderCaseEnumHelpers(_ enumDef: ExportedEnum) -> DeclSyntax { + let typeName = enumDef.swiftCallName + var initCases: [String] = [] + var valueCases: [String] = [] + for (index, c) in enumDef.cases.enumerated() { + initCases.append("case \(index): self = .\(c.name)") + valueCases.append("case .\(c.name): return \(index)") + } + let initSwitch = (["switch bridgeJSRawValue {"] + initCases + ["default: return nil", "}"]).joined( + separator: "\n" + ) + let valueSwitch = (["switch self {"] + valueCases + ["}"]).joined(separator: "\n") + + return """ + extension \(raw: typeName) { + init?(bridgeJSRawValue: Int32) { + \(raw: initSwitch) + } + + var bridgeJSRawValue: Int32 { + \(raw: valueSwitch) + } + } + """ + } + class ExportedThunkBuilder { var body: [CodeBlockItemSyntax] = [] var abiParameterForwardings: [LabeledExprSyntax] = [] @@ -700,7 +763,7 @@ public class ExportSwift { abiParameterForwardings.append( LabeledExprSyntax( label: param.label, - expression: ExprSyntax("\(raw: enumName)(rawValue: Int(\(raw: param.name)))!") + expression: ExprSyntax("\(raw: enumName)(bridgeJSRawValue: \(raw: param.name))!") ) ) abiParameterSignatures.append((param.name, .i32)) @@ -770,7 +833,6 @@ public class ExportSwift { ) abiParameterSignatures.append((param.name, .i32)) case .swiftHeapObject: - // UnsafeMutableRawPointer is passed as an i32 pointer let objectExpr: ExprSyntax = "Unmanaged<\(raw: param.type.swiftType)>.fromOpaque(\(raw: param.name)).takeUnretainedValue()" abiParameterForwardings.append( @@ -885,7 +947,7 @@ public class ExportSwift { ) case .caseEnum: abiReturnType = .i32 - append("return Int32(ret.rawValue)") + append("return ret.bridgeJSRawValue") case .rawValueEnum(_, let rawType): if rawType == .string { append( diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 0f40af965..b2fcb00c2 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -159,25 +159,32 @@ struct BridgeJSLink { namespacedEnums: namespacedEnums ) .map { $0.indent(count: 12) }.joined(separator: "\n") + + let enumSection = + enumConstantLines.isEmpty + ? "" : enumConstantLines.map { $0.indent(count: 12) }.joined(separator: "\n") + "\n" + exportsSection = """ \(classLines.map { $0.indent(count: 12) }.joined(separator: "\n")) - \(enumConstantLines.map { $0.indent(count: 12) }.joined(separator: "\n")) - const exports = { + \(enumSection)\("const exports = {".indent(count: 12)) \(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n")) - }; + \("};".indent(count: 12)) \(namespaceSetupCode) - return exports; + \("return exports;".indent(count: 12)) }, """ } else { + let enumSection = + enumConstantLines.isEmpty + ? "" : enumConstantLines.map { $0.indent(count: 12) }.joined(separator: "\n") + "\n" + exportsSection = """ \(classLines.map { $0.indent(count: 12) }.joined(separator: "\n")) - \(enumConstantLines.map { $0.indent(count: 12) }.joined(separator: "\n")) - return { + \(enumSection)\("return {".indent(count: 12)) \(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n")) - }; + \("};".indent(count: 12)) }, """ } @@ -521,6 +528,7 @@ struct BridgeJSLink { func renderExportedEnum(_ enumDefinition: ExportedEnum) throws -> (js: [String], dts: [String]) { var jsLines: [String] = [] var dtsLines: [String] = [] + let style: EnumEmitStyle = enumDefinition.emitStyle switch enumDefinition.enumType { case .simple: @@ -533,16 +541,27 @@ struct BridgeJSLink { jsLines.append("") if enumDefinition.namespace == nil { - dtsLines.append("export const \(enumDefinition.name): {") - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append(" readonly \(caseName): \(index);") + switch style { + case .tsEnum: + dtsLines.append("export enum \(enumDefinition.name) {") + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append(" \(caseName) = \(index),") + } + dtsLines.append("}") + dtsLines.append("") + case .const: + dtsLines.append("export const \(enumDefinition.name): {") + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append(" readonly \(caseName): \(index);") + } + dtsLines.append("};") + dtsLines.append( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + ) + dtsLines.append("") } - dtsLines.append("};") - dtsLines.append( - "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - ) - dtsLines.append("") } case .rawValue: guard let rawType = enumDefinition.rawType else { @@ -576,30 +595,49 @@ struct BridgeJSLink { jsLines.append("") if enumDefinition.namespace == nil { - dtsLines.append("export const \(enumDefinition.name): {") - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue: String - - switch rawType { - case "String": - formattedValue = "\"\(rawValue)\"" - case "Bool": - formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case "Float", "Double": - formattedValue = rawValue - default: - formattedValue = rawValue + switch style { + case .tsEnum: + dtsLines.append("export enum \(enumDefinition.name) {") + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + switch rawType { + case "String": formattedValue = "\"\(rawValue)\"" + case "Bool": formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": formattedValue = rawValue + default: formattedValue = rawValue + } + dtsLines.append(" \(caseName) = \(formattedValue),") } + dtsLines.append("}") + dtsLines.append("") + case .const: + dtsLines.append("export const \(enumDefinition.name): {") + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + + switch rawType { + case "String": + formattedValue = "\"\(rawValue)\"" + case "Bool": + formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": + formattedValue = rawValue + default: + formattedValue = rawValue + } - dtsLines.append(" readonly \(caseName): \(formattedValue);") + dtsLines.append(" readonly \(caseName): \(formattedValue);") + } + dtsLines.append("};") + dtsLines.append( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + ) + dtsLines.append("") } - dtsLines.append("};") - dtsLines.append( - "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - ) - dtsLines.append("") } case .associatedValue: @@ -1119,52 +1157,89 @@ struct BridgeJSLink { let sortedEnums = childNode.content.enums.sorted { $0.name < $1.name } for enumDefinition in sortedEnums { + let style: EnumEmitStyle = enumDefinition.emitStyle switch enumDefinition.enumType { case .simple: - dtsLines.append( - "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) - ) - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter + switch style { + case .tsEnum: dtsLines.append( - "readonly \(caseName): \(index);".indent(count: identBaseSize * (contentDepth + 1)) + "enum \(enumDefinition.name) {".indent(count: identBaseSize * contentDepth) ) - } - dtsLines.append("};".indent(count: identBaseSize * contentDepth)) - dtsLines.append( - "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - .indent( - count: identBaseSize * contentDepth + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append( + "\(caseName) = \(index),".indent(count: identBaseSize * (contentDepth + 1)) ) - ) + } + dtsLines.append("}".indent(count: identBaseSize * contentDepth)) + case .const: + dtsLines.append( + "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) + ) + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append( + "readonly \(caseName): \(index);".indent( + count: identBaseSize * (contentDepth + 1) + ) + ) + } + dtsLines.append("};".indent(count: identBaseSize * contentDepth)) + dtsLines.append( + "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + .indent(count: identBaseSize * contentDepth) + ) + } case .rawValue: guard let rawType = enumDefinition.rawType else { continue } - dtsLines.append( - "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) - ) - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue: String - switch rawType { - case "String": formattedValue = "\"\(rawValue)\"" - case "Bool": formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case "Float", "Double": formattedValue = rawValue - default: formattedValue = rawValue + switch style { + case .tsEnum: + dtsLines.append( + "enum \(enumDefinition.name) {".indent(count: identBaseSize * contentDepth) + ) + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + switch rawType { + case "String": formattedValue = "\"\(rawValue)\"" + case "Bool": formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": formattedValue = rawValue + default: formattedValue = rawValue + } + dtsLines.append( + "\(caseName) = \(formattedValue),".indent( + count: identBaseSize * (contentDepth + 1) + ) + ) } + dtsLines.append("}".indent(count: identBaseSize * contentDepth)) + case .const: dtsLines.append( - "readonly \(caseName): \(formattedValue);".indent( - count: identBaseSize * (contentDepth + 1) + "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) + ) + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + switch rawType { + case "String": formattedValue = "\"\(rawValue)\"" + case "Bool": formattedValue = rawValue.lowercased() == "true" ? "true" : "false" + case "Float", "Double": formattedValue = rawValue + default: formattedValue = rawValue + } + dtsLines.append( + "readonly \(caseName): \(formattedValue);".indent( + count: identBaseSize * (contentDepth + 1) + ) ) + } + dtsLines.append("};".indent(count: identBaseSize * contentDepth)) + dtsLines.append( + "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + .indent(count: identBaseSize * contentDepth) ) } - dtsLines.append("};".indent(count: identBaseSize * contentDepth)) - dtsLines.append( - "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - .indent( - count: identBaseSize * contentDepth - ) - ) case .associatedValue, .namespace: continue } diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 89f2771e4..0d872160e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -14,7 +14,6 @@ public enum WasmCoreType: String, Codable { case i32, i64, f32, f64, pointer } -/// Represents supported Swift enum raw types with their WASM ABI properties public enum SwiftEnumRawType: String, CaseIterable, Codable { case string = "String" case bool = "Bool" @@ -30,7 +29,7 @@ public enum SwiftEnumRawType: String, CaseIterable, Codable { public var wasmCoreType: WasmCoreType? { switch self { case .string: - return nil // String has special handling with UTF-8 bytes and length + return nil case .bool, .int, .int32, .uint, .uint32: return .i32 case .int64, .uint64: @@ -97,12 +96,18 @@ public struct EnumCase: Codable, Equatable { } } +public enum EnumEmitStyle: String, Codable { + case const + case tsEnum +} + public struct ExportedEnum: Codable, Equatable { public let name: String public let swiftCallName: String public let cases: [EnumCase] public let rawType: String? public let namespace: [String]? + public let emitStyle: EnumEmitStyle public var enumType: EnumType { if cases.isEmpty { return .namespace @@ -118,13 +123,15 @@ public struct ExportedEnum: Codable, Equatable { swiftCallName: String, cases: [EnumCase], rawType: String?, - namespace: [String]? + namespace: [String]?, + emitStyle: EnumEmitStyle ) { self.name = name self.swiftCallName = swiftCallName self.cases = cases self.rawType = rawType self.namespace = namespace + self.emitStyle = emitStyle } } diff --git a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json deleted file mode 100644 index 7ddef6370..000000000 --- a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package-lock.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "JavaScript", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "typescript": "^5.8.2" - }, - "bin": { - "ts2skeleton": "bin/ts2skeleton.js" - } - }, - "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json index 6638cb8d4..48fb77cfc 100644 --- a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json +++ b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/package.json @@ -1,7 +1,7 @@ { "type": "module", "dependencies": { - "typescript": "^5.8.2" + "typescript": "5.8.2" }, "bin": { "ts2skeleton": "./bin/ts2skeleton.js" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift index b82ef33fe..6d5d6b55e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift @@ -14,3 +14,13 @@ @JS func setDirection(_ direction: Direction) @JS func getDirection() -> Direction @JS func processDirection(_ input: Direction) -> Status + +@JS(enumStyle: .tsEnum) enum TSDirection { + case north + case south + case east + case west +} + +@JS func setTSDirection(_ direction: TSDirection) +@JS func getTSDirection() -> TSDirection diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift index bf1a69ccf..799df1649 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift @@ -4,6 +4,12 @@ case auto = "auto" } +@JS(enumStyle: .tsEnum) enum TSTheme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} + @JS enum FeatureFlag: Bool { case enabled = true case disabled = false @@ -15,6 +21,12 @@ case serverError = 500 } +@JS(enumStyle: .tsEnum) enum TSHttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + @JS enum Priority: Int32 { case lowest = 1 case low = 2 @@ -64,12 +76,18 @@ @JS func setTheme(_ theme: Theme) @JS func getTheme() -> Theme +@JS func setTSTheme(_ theme: TSTheme) +@JS func getTSTheme() -> TSTheme + @JS func setFeatureFlag(_ flag: FeatureFlag) @JS func getFeatureFlag() -> FeatureFlag @JS func setHttpStatus(_ status: HttpStatus) @JS func getHttpStatus() -> HttpStatus +@JS func setTSHttpStatus(_ status: TSHttpStatus) +@JS func getTSHttpStatus() -> TSHttpStatus + @JS func setPriority(_ priority: Priority) @JS func getPriority() -> Priority diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js index 169952273..c122f1797 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js @@ -84,7 +84,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js index 6ced9f919..1da2f58ee 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { asyncReturnVoid: function bjs_asyncReturnVoid() { const retId = instance.exports.bjs_asyncReturnVoid(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js index 0e39bd381..21d11fa4d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.Import.js @@ -128,7 +128,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts index f4d2c90b3..2d45e9982 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts @@ -19,10 +19,19 @@ export const Status: { }; export type Status = typeof Status[keyof typeof Status]; +export enum TSDirection { + North = 0, + South = 1, + East = 2, + West = 3, +} + export type Exports = { setDirection(direction: Direction): void; getDirection(): Direction; processDirection(input: Direction): Status; + setTSDirection(direction: TSDirection): void; + getTSDirection(): TSDirection; } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js index ed355d068..d9f4b87db 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js @@ -76,9 +76,17 @@ export async function createInstantiator(options, swift) { Error: 2, }; + const TSDirection = { + North: 0, + South: 1, + East: 2, + West: 3, + }; + return { Direction, Status, + TSDirection, setDirection: function bjs_setDirection(direction) { instance.exports.bjs_setDirection(direction | 0); }, @@ -90,6 +98,13 @@ export async function createInstantiator(options, swift) { const ret = instance.exports.bjs_processDirection(input | 0); return ret; }, + setTSDirection: function bjs_setTSDirection(direction) { + instance.exports.bjs_setTSDirection(direction | 0); + }, + getTSDirection: function bjs_getTSDirection() { + const ret = instance.exports.bjs_getTSDirection(); + return ret; + }, }; }, } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts index d67ee1fee..51b020ad9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts @@ -11,6 +11,12 @@ export const Theme: { }; export type Theme = typeof Theme[keyof typeof Theme]; +export enum TSTheme { + Light = "light", + Dark = "dark", + Auto = "auto", +} + export const FeatureFlag: { readonly Enabled: true; readonly Disabled: false; @@ -24,6 +30,12 @@ export const HttpStatus: { }; export type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]; +export enum TSHttpStatus { + Ok = 200, + NotFound = 404, + ServerError = 500, +} + export const Priority: { readonly Lowest: 1; readonly Low: 2; @@ -80,10 +92,14 @@ export type Ratio = typeof Ratio[keyof typeof Ratio]; export type Exports = { setTheme(theme: Theme): void; getTheme(): Theme; + setTSTheme(theme: TSTheme): void; + getTSTheme(): TSTheme; setFeatureFlag(flag: FeatureFlag): void; getFeatureFlag(): FeatureFlag; setHttpStatus(status: HttpStatus): void; getHttpStatus(): HttpStatus; + setTSHttpStatus(status: TSHttpStatus): void; + getTSHttpStatus(): TSHttpStatus; setPriority(priority: Priority): void; getPriority(): Priority; setFileSize(size: FileSize): void; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js index f4615d99c..e8b7adcf9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js @@ -69,6 +69,12 @@ export async function createInstantiator(options, swift) { Auto: "auto", }; + const TSTheme = { + Light: "light", + Dark: "dark", + Auto: "auto", + }; + const FeatureFlag = { Enabled: true, Disabled: false, @@ -80,6 +86,12 @@ export async function createInstantiator(options, swift) { ServerError: 500, }; + const TSHttpStatus = { + Ok: 200, + NotFound: 404, + ServerError: 500, + }; + const Priority = { Lowest: 1, Low: 2, @@ -128,8 +140,10 @@ export async function createInstantiator(options, swift) { return { Theme, + TSTheme, FeatureFlag, HttpStatus, + TSHttpStatus, Priority, FileSize, UserId, @@ -149,6 +163,18 @@ export async function createInstantiator(options, swift) { tmpRetString = undefined; return ret; }, + setTSTheme: function bjs_setTSTheme(theme) { + const themeBytes = textEncoder.encode(theme); + const themeId = swift.memory.retain(themeBytes); + instance.exports.bjs_setTSTheme(themeId, themeBytes.length); + swift.memory.release(themeId); + }, + getTSTheme: function bjs_getTSTheme() { + instance.exports.bjs_getTSTheme(); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + }, setFeatureFlag: function bjs_setFeatureFlag(flag) { instance.exports.bjs_setFeatureFlag(flag ? 1 : 0); }, @@ -163,6 +189,13 @@ export async function createInstantiator(options, swift) { const ret = instance.exports.bjs_getHttpStatus(); return ret; }, + setTSHttpStatus: function bjs_setTSHttpStatus(status) { + instance.exports.bjs_setTSHttpStatus(status); + }, + getTSHttpStatus: function bjs_getTSHttpStatus() { + const ret = instance.exports.bjs_getTSHttpStatus(); + return ret; + }, setPriority: function bjs_setPriority(priority) { instance.exports.bjs_setPriority(priority); }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js index 874bebd3e..f81c7e475 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js @@ -90,7 +90,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js index 7e955b1a0..394d996b6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MultipleImportedTypes.Import.js @@ -194,7 +194,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js index cdf5aa250..6915a61a5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js @@ -145,7 +145,6 @@ export async function createInstantiator(options, swift) { return ret; } } - const exports = { Greeter, Converter, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js index 40ac2cda0..4873fc33c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { check: function bjs_check(a, b, c, d) { instance.exports.bjs_check(a, b, c, d); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js index 1cee664bd..3b93b2dd0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js @@ -70,7 +70,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js index 7daa5bbbb..53332b97b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { checkInt: function bjs_checkInt() { const ret = instance.exports.bjs_checkInt(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js index 19af42bd2..1892eb46e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js @@ -81,7 +81,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js index bb5cabd8e..ea47fb555 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { checkString: function bjs_checkString(a) { const aBytes = textEncoder.encode(a); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js index 524a04664..16ed1081e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js @@ -81,7 +81,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js index 67c635657..f98cea559 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { checkString: function bjs_checkString() { instance.exports.bjs_checkString(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js index 5830cd3d3..3220ae7b0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js @@ -72,7 +72,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js index 0a1da2c5f..ab4caba34 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js @@ -114,7 +114,6 @@ export async function createInstantiator(options, swift) { swift.memory.release(nameId); } } - return { Greeter, takeGreeter: function bjs_takeGreeter(greeter) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js index 15e5d4d88..705c6a370 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TS2SkeletonLike.Import.js @@ -132,7 +132,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js index 95b558a18..b2089962c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { throwsSomething: function bjs_throwsSomething() { instance.exports.bjs_throwsSomething(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js index dccc193eb..2eb9dee50 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js @@ -70,7 +70,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index cc0605999..c7d622ea0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -119,7 +119,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js index 91017e566..c200c077d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js @@ -63,7 +63,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { check: function bjs_check() { instance.exports.bjs_check(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js index 77087ebaa..ca4976888 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js @@ -70,7 +70,6 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - return { }; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json index 7527eef96..efb6e805c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json @@ -30,6 +30,7 @@ "name" : "west" } ], + "emitStyle" : "const", "name" : "Direction", "swiftCallName" : "Direction" }, @@ -54,8 +55,40 @@ "name" : "error" } ], + "emitStyle" : "const", "name" : "Status", "swiftCallName" : "Status" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "north" + }, + { + "associatedValues" : [ + + ], + "name" : "south" + }, + { + "associatedValues" : [ + + ], + "name" : "east" + }, + { + "associatedValues" : [ + + ], + "name" : "west" + } + ], + "emitStyle" : "tsEnum", + "name" : "TSDirection", + "swiftCallName" : "TSDirection" } ], "functions" : [ @@ -122,6 +155,46 @@ "_0" : "Status" } } + }, + { + "abiName" : "bjs_setTSDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTSDirection", + "parameters" : [ + { + "label" : "_", + "name" : "direction", + "type" : { + "caseEnum" : { + "_0" : "TSDirection" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getTSDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTSDirection", + "parameters" : [ + + ], + "returnType" : { + "caseEnum" : { + "_0" : "TSDirection" + } + } } ], "moduleName" : "TestModule" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift index affe3d3df..363ade829 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift @@ -6,11 +6,97 @@ @_spi(BridgeJS) import JavaScriptKit +extension Direction { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} + +extension Status { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .loading + case 1: + self = .success + case 2: + self = .error + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .loading: + return 0 + case .success: + return 1 + case .error: + return 2 + } + } +} + +extension TSDirection { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} + @_expose(wasm, "bjs_setDirection") @_cdecl("bjs_setDirection") public func _bjs_setDirection(direction: Int32) -> Void { #if arch(wasm32) - setDirection(_: Direction(rawValue: Int(direction))!) + setDirection(_: Direction(bridgeJSRawValue: direction)!) #else fatalError("Only available on WebAssembly") #endif @@ -21,7 +107,7 @@ public func _bjs_setDirection(direction: Int32) -> Void { public func _bjs_getDirection() -> Int32 { #if arch(wasm32) let ret = getDirection() - return Int32(ret.rawValue) + return ret.bridgeJSRawValue #else fatalError("Only available on WebAssembly") #endif @@ -31,8 +117,29 @@ public func _bjs_getDirection() -> Int32 { @_cdecl("bjs_processDirection") public func _bjs_processDirection(input: Int32) -> Int32 { #if arch(wasm32) - let ret = processDirection(_: Direction(rawValue: Int(input))!) - return Int32(ret.rawValue) + let ret = processDirection(_: Direction(bridgeJSRawValue: input)!) + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setTSDirection") +@_cdecl("bjs_setTSDirection") +public func _bjs_setTSDirection(direction: Int32) -> Void { + #if arch(wasm32) + setTSDirection(_: TSDirection(bridgeJSRawValue: direction)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTSDirection") +@_cdecl("bjs_getTSDirection") +public func _bjs_getTSDirection() -> Int32 { + #if arch(wasm32) + let ret = getTSDirection() + return ret.bridgeJSRawValue #else fatalError("Only available on WebAssembly") #endif diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json index f7dd38729..a9483455e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json @@ -138,6 +138,7 @@ "cases" : [ ], + "emitStyle" : "const", "name" : "Utils", "swiftCallName" : "Utils" }, @@ -168,6 +169,7 @@ "name" : "delete" } ], + "emitStyle" : "const", "name" : "Method", "namespace" : [ "Networking", @@ -206,6 +208,7 @@ "rawValue" : "error" } ], + "emitStyle" : "const", "name" : "LogLevel", "namespace" : [ "Configuration" @@ -237,6 +240,7 @@ "rawValue" : "3000" } ], + "emitStyle" : "const", "name" : "Port", "namespace" : [ "Configuration" @@ -259,6 +263,7 @@ "name" : "post" } ], + "emitStyle" : "const", "name" : "SupportedMethod", "namespace" : [ "Networking", diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift index fa77613fa..9517ad808 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift @@ -6,6 +6,58 @@ @_spi(BridgeJS) import JavaScriptKit +extension Networking.API.Method { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .get + case 1: + self = .post + case 2: + self = .put + case 3: + self = .delete + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .get: + return 0 + case .post: + return 1 + case .put: + return 2 + case .delete: + return 3 + } + } +} + +extension Internal.SupportedMethod { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .get + case 1: + self = .post + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .get: + return 0 + case .post: + return 1 + } + } +} + @_expose(wasm, "bjs_Converter_init") @_cdecl("bjs_Converter_init") public func _bjs_Converter_init() -> UnsafeMutableRawPointer { @@ -59,7 +111,7 @@ public func _bjs_HTTPServer_init() -> UnsafeMutableRawPointer { @_cdecl("bjs_HTTPServer_call") public func _bjs_HTTPServer_call(_self: UnsafeMutableRawPointer, method: Int32) -> Void { #if arch(wasm32) - Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Networking.API.Method(rawValue: Int(method))!) + Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Networking.API.Method(bridgeJSRawValue: method)!) #else fatalError("Only available on WebAssembly") #endif @@ -94,7 +146,7 @@ public func _bjs_TestServer_init() -> UnsafeMutableRawPointer { @_cdecl("bjs_TestServer_call") public func _bjs_TestServer_call(_self: UnsafeMutableRawPointer, method: Int32) -> Void { #if arch(wasm32) - Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Internal.SupportedMethod(rawValue: Int(method))!) + Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Internal.SupportedMethod(bridgeJSRawValue: method)!) #else fatalError("Only available on WebAssembly") #endif diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json index d85bc5501..09ce5d6e9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json @@ -27,6 +27,7 @@ "rawValue" : "auto" } ], + "emitStyle" : "const", "name" : "Theme", "rawType" : "String", "swiftCallName" : "Theme" @@ -36,6 +37,35 @@ { "associatedValues" : [ + ], + "name" : "light", + "rawValue" : "light" + }, + { + "associatedValues" : [ + + ], + "name" : "dark", + "rawValue" : "dark" + }, + { + "associatedValues" : [ + + ], + "name" : "auto", + "rawValue" : "auto" + } + ], + "emitStyle" : "tsEnum", + "name" : "TSTheme", + "rawType" : "String", + "swiftCallName" : "TSTheme" + }, + { + "cases" : [ + { + "associatedValues" : [ + ], "name" : "enabled", "rawValue" : "true" @@ -48,6 +78,7 @@ "rawValue" : "false" } ], + "emitStyle" : "const", "name" : "FeatureFlag", "rawType" : "Bool", "swiftCallName" : "FeatureFlag" @@ -76,6 +107,7 @@ "rawValue" : "500" } ], + "emitStyle" : "const", "name" : "HttpStatus", "rawType" : "Int", "swiftCallName" : "HttpStatus" @@ -85,6 +117,35 @@ { "associatedValues" : [ + ], + "name" : "ok", + "rawValue" : "200" + }, + { + "associatedValues" : [ + + ], + "name" : "notFound", + "rawValue" : "404" + }, + { + "associatedValues" : [ + + ], + "name" : "serverError", + "rawValue" : "500" + } + ], + "emitStyle" : "tsEnum", + "name" : "TSHttpStatus", + "rawType" : "Int", + "swiftCallName" : "TSHttpStatus" + }, + { + "cases" : [ + { + "associatedValues" : [ + ], "name" : "lowest", "rawValue" : "1" @@ -118,6 +179,7 @@ "rawValue" : "5" } ], + "emitStyle" : "const", "name" : "Priority", "rawType" : "Int32", "swiftCallName" : "Priority" @@ -153,6 +215,7 @@ "rawValue" : "1048576" } ], + "emitStyle" : "const", "name" : "FileSize", "rawType" : "Int64", "swiftCallName" : "FileSize" @@ -181,6 +244,7 @@ "rawValue" : "9999" } ], + "emitStyle" : "const", "name" : "UserId", "rawType" : "UInt", "swiftCallName" : "UserId" @@ -209,6 +273,7 @@ "rawValue" : "67890" } ], + "emitStyle" : "const", "name" : "TokenId", "rawType" : "UInt32", "swiftCallName" : "TokenId" @@ -237,6 +302,7 @@ "rawValue" : "1234567890" } ], + "emitStyle" : "const", "name" : "SessionId", "rawType" : "UInt64", "swiftCallName" : "SessionId" @@ -265,6 +331,7 @@ "rawValue" : "0.001" } ], + "emitStyle" : "const", "name" : "Precision", "rawType" : "Float", "swiftCallName" : "Precision" @@ -300,6 +367,7 @@ "rawValue" : "3.14159" } ], + "emitStyle" : "const", "name" : "Ratio", "rawType" : "Double", "swiftCallName" : "Ratio" @@ -348,6 +416,48 @@ } } }, + { + "abiName" : "bjs_setTSTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTSTheme", + "parameters" : [ + { + "label" : "_", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "TSTheme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getTSTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTSTheme", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "TSTheme", + "_1" : "String" + } + } + }, { "abiName" : "bjs_setFeatureFlag", "effects" : { @@ -432,6 +542,48 @@ } } }, + { + "abiName" : "bjs_setTSHttpStatus", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTSHttpStatus", + "parameters" : [ + { + "label" : "_", + "name" : "status", + "type" : { + "rawValueEnum" : { + "_0" : "TSHttpStatus", + "_1" : "Int" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getTSHttpStatus", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTSHttpStatus", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "TSHttpStatus", + "_1" : "Int" + } + } + }, { "abiName" : "bjs_setPriority", "effects" : { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift index 185c4ac80..84ad98215 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift @@ -33,6 +33,33 @@ public func _bjs_getTheme() -> Void { #endif } +@_expose(wasm, "bjs_setTSTheme") +@_cdecl("bjs_setTSTheme") +public func _bjs_setTSTheme(themeBytes: Int32, themeLen: Int32) -> Void { + #if arch(wasm32) + let theme = String(unsafeUninitializedCapacity: Int(themeLen)) { b in + _swift_js_init_memory(themeBytes, b.baseAddress.unsafelyUnwrapped) + return Int(themeLen) + } + setTSTheme(_: TSTheme(rawValue: theme)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTSTheme") +@_cdecl("bjs_getTSTheme") +public func _bjs_getTSTheme() -> Void { + #if arch(wasm32) + let ret = getTSTheme() + return ret.rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_setFeatureFlag") @_cdecl("bjs_setFeatureFlag") public func _bjs_setFeatureFlag(flag: Int32) -> Void { @@ -75,6 +102,27 @@ public func _bjs_getHttpStatus() -> Int32 { #endif } +@_expose(wasm, "bjs_setTSHttpStatus") +@_cdecl("bjs_setTSHttpStatus") +public func _bjs_setTSHttpStatus(status: Int32) -> Void { + #if arch(wasm32) + setTSHttpStatus(_: TSHttpStatus(rawValue: Int(status))!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTSHttpStatus") +@_cdecl("bjs_getTSHttpStatus") +public func _bjs_getTSHttpStatus() -> Int32 { + #if arch(wasm32) + let ret = getTSHttpStatus() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_setPriority") @_cdecl("bjs_setPriority") public func _bjs_setPriority(priority: Int32) -> Void { 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 6ce307721..ad7932f13 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 @@ -50,9 +50,10 @@ swift package --swift-sdk $SWIFT_SDK_ID js ``` This command will: + 1. Process all Swift files with `@JS` annotations 2. Generate JavaScript bindings and TypeScript type definitions (`.d.ts`) for your exported Swift code -4. Output everything to the `.build/plugins/PackageToJS/outputs/` directory +3. Output everything to the `.build/plugins/PackageToJS/outputs/` directory > Note: For larger projects, you may want to generate the BridgeJS code ahead of time to improve build performance. See for more information. @@ -163,6 +164,410 @@ export type Exports = { } ``` + +### Enum Support + +BridgeJS supports two output styles for enums, controlled by the `enumStyle` parameter: + +- **`.const` (default)**: Generates const objects with union types +- **`.tsEnum`**: Generates native TypeScript enum declarations - **only available for case enums and raw value enums with String or numeric raw types** + +Examples output of both styles can be found below. + +#### Case Enums + +**Swift Definition:** + +```swift +@JS enum Direction { + case north + case south + case east + case west +} + +@JS(enumStyle: .tsEnum) enum TSDirection { + case north + case south + case east + case west +} + +@JS enum Status { + case loading + case success + case error +} +``` + +**Generated TypeScript Declaration:** + +```typescript +// Const object style (default) +const Direction: { + readonly North: 0; + readonly South: 1; + readonly East: 2; + readonly West: 3; +}; +type Direction = typeof Direction[keyof typeof Direction]; + +// Native TypeScript enum style +enum TSDirection { + North = 0, + South = 1, + East = 2, + West = 3, +} + +const Status: { + readonly Loading: 0; + readonly Success: 1; + readonly Error: 2; +}; +type Status = typeof Status[keyof typeof Status]; +``` + +**Usage in TypeScript:** + +```typescript +const direction: Direction = exports.Direction.North; +const tsDirection: TSDirection = exports.TSDirection.North; +const status: Status = exports.Status.Loading; + +exports.setDirection(exports.Direction.South); +exports.setTSDirection(exports.TSDirection.East); +const currentDirection: Direction = exports.getDirection(); +const currentTSDirection: TSDirection = exports.getTSDirection(); + +const result: Status = exports.processDirection(exports.Direction.East); + +function handleDirection(direction: Direction) { + switch (direction) { + case exports.Direction.North: + console.log("Going north"); + break; + case exports.Direction.South: + console.log("Going south"); + break; + // TypeScript will warn about missing cases + } +} +``` + +BridgeJS also generates convenience initializers and computed properties for each case-style enum, allowing the rest of the Swift glue code to remain minimal and consistent. This avoids repetitive switch statements in every function that passes enum values between JavaScript and Swift. + +```swift +extension Direction { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} +... +@_expose(wasm, "bjs_setDirection") +@_cdecl("bjs_setDirection") +public func _bjs_setDirection(direction: Int32) -> Void { + #if arch(wasm32) + setDirection(_: Direction(bridgeJSRawValue: direction)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getDirection") +@_cdecl("bjs_getDirection") +public func _bjs_getDirection() -> Int32 { + #if arch(wasm32) + let ret = getDirection() + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} +``` + +#### Raw Value Enums + +##### String Raw Values + +**Swift Definition:** + +```swift +// Default const object style +@JS enum Theme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} + +// Native TypeScript enum style +@JS(enumStyle: .tsEnum) enum TSTheme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} +``` + +**Generated TypeScript Declaration:** + +```typescript +// Const object style (default) +const Theme: { + readonly Light: "light"; + readonly Dark: "dark"; + readonly Auto: "auto"; +}; +type Theme = typeof Theme[keyof typeof Theme]; + +// Native TypeScript enum style +enum TSTheme { + Light = "light", + Dark = "dark", + Auto = "auto", +} +``` + +**Usage in TypeScript:** + +```typescript +// Both styles work similarly in usage +const theme: Theme = exports.Theme.Dark; +const tsTheme: TSTheme = exports.TSTheme.Dark; + +exports.setTheme(exports.Theme.Light); +const currentTheme: Theme = exports.getTheme(); + +const status: HttpStatus = exports.processTheme(exports.Theme.Auto); +``` + +##### Integer Raw Values + +**Swift Definition:** + +```swift +// Default const object style +@JS enum HttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + +// Native TypeScript enum style +@JS(enumStyle: .tsEnum) enum TSHttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + +@JS enum Priority: Int32 { + case lowest = 1 + case low = 2 + case medium = 3 + case high = 4 + case highest = 5 +} +``` + +**Generated TypeScript Declaration:** + +```typescript +// Const object style (default) +const HttpStatus: { + readonly Ok: 200; + readonly NotFound: 404; + readonly ServerError: 500; +}; +type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]; + +// Native TypeScript enum style +enum TSHttpStatus { + Ok = 200, + NotFound = 404, + ServerError = 500, +} + +const Priority: { + readonly Lowest: 1; + readonly Low: 2; + readonly Medium: 3; + readonly High: 4; + readonly Highest: 5; +}; +type Priority = typeof Priority[keyof typeof Priority]; +``` + +**Usage in TypeScript:** + +```typescript +const status: HttpStatus = exports.HttpStatus.Ok; +const tsStatus: TSHttpStatus = exports.TSHttpStatus.Ok; +const priority: Priority = exports.Priority.High; + +exports.setHttpStatus(exports.HttpStatus.NotFound); +exports.setPriority(exports.Priority.Medium); + +const convertedPriority: Priority = exports.convertPriority(exports.HttpStatus.Ok); +``` + +### Namespace Enums + +Namespace enums are empty enums (containing no cases) used for organizing related types and functions into hierarchical namespaces. + +**Swift Definition:** + +```swift +@JS enum Utils { + @JS class Converter { + @JS init() {} + + @JS func toString(value: Int) -> String { + return String(value) + } + } +} + +// Nested namespace enums with no @JS(namespace:) macro used +@JS enum Networking { + @JS enum API { + @JS enum Method { + case get + case post + } + + @JS class HTTPServer { + @JS init() {} + @JS func call(_ method: Method) + } + } +} + +// Top level enum can still use explicit namespace via @JS(namespace:) +@JS(namespace: "Networking.APIV2") +enum Internal { + @JS enum SupportedMethod { + case get + case post + } + + @JS class TestServer { + @JS init() {} + @JS func call(_ method: SupportedMethod) + } +} +``` + +**Generated TypeScript Declaration:** + +```typescript +declare global { + namespace Utils { + class Converter { + constructor(); + toString(value: number): string; + } + } + namespace Networking { + namespace API { + class HTTPServer { + constructor(); + call(method: Networking.API.Method): void; + } + const Method: { + readonly Get: 0; + readonly Post: 1; + }; + type Method = typeof Method[keyof typeof Method]; + } + namespace APIV2 { + namespace Internal { + class TestServer { + constructor(); + call(method: Internal.SupportedMethod): void; + } + const SupportedMethod: { + readonly Get: 0; + readonly Post: 1; + }; + type SupportedMethod = typeof SupportedMethod[keyof typeof SupportedMethod]; + } + } + } +} +``` + +**Usage in TypeScript:** + +```typescript +// Access nested classes through namespaces +const converter = new globalThis.Utils.Converter(); +const result: string = converter.toString(42) + +const server = new globalThis.Networking.API.HTTPServer(); +const method: Networking.API.Method = globalThis.Networking.API.Method.Get; +server.call(method) + +const testServer = new globalThis.Networking.APIV2.Internal.TestServer(); +const supportedMethod: Internal.SupportedMethod = globalThis.Networking.APIV2.Internal.SupportedMethod.Post; +testServer.call(supportedMethod); +``` + +Things to remember when using enums for namespacing: + +1. Only enums with no cases will be used for namespaces +2. Top-level enums can use `@JS(namespace: "Custom.Path")` to place themselves in custom namespaces, which will be used as "base namespace" for all nested elements as well +3. Classes and enums nested within namespace enums **cannot** use `@JS(namespace:)` - this would create conflicting namespace declarations + +**Invalid Usage:** + +```swift +@JS enum Utils { + // Invalid - nested items cannot specify their own namespace + @JS(namespace: "Custom") class Helper { + @JS init() {} + } +} +``` + +**Valid Usage:** + +```swift +// Valid - top-level enum with explicit namespace +@JS(namespace: "Custom.Utils") +enum Helper { + @JS class Converter { + @JS init() {} + } +} +``` + +#### Associated Value Enums + +Associated value enums are not currently supported, but are planned for future releases. + ## Using Namespaces The `@JS` macro supports organizing your exported Swift code into namespaces using dot-separated strings. This allows you to create hierarchical structures in JavaScript that mirror your Swift code organization. diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index dac264ff9..b8a44a08c 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -1,3 +1,11 @@ +/// Controls how Swift enums annotated with `@JS` are emitted to TypeScript. +/// - `const`: Emit the current BridgeJS style: a `const` object with literal members plus a type alias. +/// - `tsEnum`: Emit a TypeScript `enum` declaration (only valid for simple enums and raw-value enums with String or numeric raw types). +public enum JSEnumStyle: String { + case const + case tsEnum +} + /// A macro that exposes Swift functions, classes, and methods to JavaScript. /// /// Apply this macro to Swift declarations that you want to make callable from JavaScript: @@ -90,7 +98,12 @@ /// /// - Parameter namespace: A dot-separated string that defines the namespace hierarchy in JavaScript. /// Each segment becomes a nested object in the resulting JavaScript structure. +/// - Parameter enumStyle: Controls how enums are emitted to TypeScript for this declaration: +/// use `.const` (default) to emit a const object + type alias, +/// or `.tsEnum` to emit a TypeScript `enum`. +/// `.tsEnum` is supported for case enums and raw-value enums with String or numeric raw types. +/// Bool raw-value enums are not supported with `.tsEnum` and will produce an error. /// /// - Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases. @attached(peer) -public macro JS(namespace: String? = nil) = Builtin.ExternalMacro +public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const) = Builtin.ExternalMacro diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json index 97b86cecb..19c77f92f 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -61,7 +61,8 @@ } } ], - "name" : "Greeter" + "name" : "Greeter", + "swiftCallName" : "Greeter" }, { "methods" : [ @@ -123,8 +124,12 @@ } } ], - "name" : "Calculator" + "name" : "Calculator", + "swiftCallName" : "Calculator" } + ], + "enums" : [ + ], "functions" : [ { From 59353155798f49e793e3e5f75d208443443a1c4c Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Fri, 22 Aug 2025 11:13:37 +0200 Subject: [PATCH 4/4] BridgeJS: Runtime tests, string enum fixes and code review feedback --- .../Sources/BridgeJSCore/ExportSwift.swift | 10 +- .../Sources/BridgeJSCore/ImportTS.swift | 2 +- .../Sources/BridgeJSLink/BridgeJSLink.swift | 101 ++- .../Inputs/EnumNamespace.swift | 4 +- .../BridgeJSLinkTests/EnumCase.Export.js | 44 +- .../BridgeJSLinkTests/EnumNamespace.Export.js | 83 ++- .../BridgeJSLinkTests/EnumRawType.Export.js | 163 ++-- .../ExportSwiftTests/EnumRawType.swift | 9 +- .../BridgeJSRuntimeTests/ExportAPITests.swift | 147 ++++ .../Generated/BridgeJS.ExportSwift.swift | 402 ++++++++++ .../JavaScript/BridgeJS.ExportSwift.json | 702 ++++++++++++++++++ Tests/prelude.mjs | 87 +++ 12 files changed, 1585 insertions(+), 169 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 0b5e7f208..a5f2e1080 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -865,7 +865,12 @@ public class ExportSwift { return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr).jsValue"))) } - let retMutability = returnType == .string ? "var" : "let" + let retMutability: String + if returnType == .string { + retMutability = "var" + } else { + retMutability = "let" + } if returnType == .void { return CodeBlockItemSyntax(item: .init(ExpressionStmtSyntax(expression: callExpr))) } else { @@ -952,7 +957,8 @@ public class ExportSwift { if rawType == .string { append( """ - return ret.rawValue.withUTF8 { ptr in + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) } """ diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 9c4679e98..bcbae469e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -505,7 +505,7 @@ public struct ImportTS { } } -extension String { +fileprivate extension String { func capitalizedFirstLetter() -> String { guard !isEmpty else { return self } return prefix(1).uppercased() + dropFirst() diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index b2fcb00c2..046cb92d5 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -72,6 +72,8 @@ struct BridgeJSLink { var namespacedEnums: [ExportedEnum] = [] var enumConstantLines: [String] = [] var dtsEnumLines: [String] = [] + var topLevelEnumLines: [String] = [] + var topLevelDtsEnumLines: [String] = [] if exportedSkeletons.contains(where: { $0.classes.count > 0 }) { classLines.append( @@ -102,14 +104,29 @@ struct BridgeJSLink { if !skeleton.enums.isEmpty { for enumDefinition in skeleton.enums { let (jsEnum, dtsEnum) = try renderExportedEnum(enumDefinition) - enumConstantLines.append(contentsOf: jsEnum) - if enumDefinition.enumType != .namespace { + + switch enumDefinition.enumType { + case .namespace: + break + case .simple, .rawValue: + var exportedJsEnum = jsEnum + if !exportedJsEnum.isEmpty && exportedJsEnum[0].hasPrefix("const ") { + exportedJsEnum[0] = "export " + exportedJsEnum[0] + } + topLevelEnumLines.append(contentsOf: exportedJsEnum) + topLevelDtsEnumLines.append(contentsOf: dtsEnum) + + if enumDefinition.namespace != nil { + namespacedEnums.append(enumDefinition) + } + case .associatedValue: + enumConstantLines.append(contentsOf: jsEnum) exportsLines.append("\(enumDefinition.name),") if enumDefinition.namespace != nil { namespacedEnums.append(enumDefinition) } + dtsEnumLines.append(contentsOf: dtsEnum) } - dtsEnumLines.append(contentsOf: dtsEnum) } } @@ -153,10 +170,11 @@ struct BridgeJSLink { let exportsSection: String if hasNamespacedItems { + let namespacedEnumsForExports = namespacedEnums.filter { $0.enumType == .associatedValue } let namespaceSetupCode = namespaceBuilder.renderGlobalNamespace( namespacedFunctions: namespacedFunctions, namespacedClasses: namespacedClasses, - namespacedEnums: namespacedEnums + namespacedEnums: namespacedEnumsForExports ) .map { $0.indent(count: 12) }.joined(separator: "\n") @@ -189,6 +207,14 @@ struct BridgeJSLink { """ } + let topLevelEnumsSection = topLevelEnumLines.isEmpty ? "" : topLevelEnumLines.joined(separator: "\n") + "\n\n" + + let topLevelNamespaceCode = namespaceBuilder.renderTopLevelEnumNamespaceAssignments( + namespacedEnums: namespacedEnums + ) + let namespaceAssignmentsSection = + topLevelNamespaceCode.isEmpty ? "" : topLevelNamespaceCode.joined(separator: "\n") + "\n\n" + let outputJs = """ // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. @@ -196,7 +222,7 @@ struct BridgeJSLink { // To update this file, just rebuild your project or run // `swift package bridge-js`. - export async function createInstantiator(options, swift) { + \(topLevelEnumsSection)\(namespaceAssignmentsSection)export async function createInstantiator(options, swift) { let instance; let memory; let setException; @@ -270,6 +296,9 @@ struct BridgeJSLink { dtsLines.append("export type Imports = {") dtsLines.append(contentsOf: importObjectBuilders.flatMap { $0.dtsImportLines }.map { $0.indent(count: 4) }) dtsLines.append("}") + let topLevelDtsEnumsSection = + topLevelDtsEnumLines.isEmpty ? "" : topLevelDtsEnumLines.joined(separator: "\n") + "\n" + let outputDts = """ // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. @@ -277,7 +306,7 @@ struct BridgeJSLink { // To update this file, just rebuild your project or run // `swift package bridge-js`. - \(dtsLines.joined(separator: "\n")) + \(topLevelDtsEnumsSection)\(dtsLines.joined(separator: "\n")) export function createInstantiator(options: { imports: Imports; }, swift: any): Promise<{ @@ -535,7 +564,7 @@ struct BridgeJSLink { jsLines.append("const \(enumDefinition.name) = {") for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - jsLines.append(" \(caseName): \(index),".indent(count: 0)) + jsLines.append("\(caseName): \(index),".indent(count: 4)) } jsLines.append("};") jsLines.append("") @@ -546,7 +575,7 @@ struct BridgeJSLink { dtsLines.append("export enum \(enumDefinition.name) {") for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append(" \(caseName) = \(index),") + dtsLines.append("\(caseName) = \(index),".indent(count: 4)) } dtsLines.append("}") dtsLines.append("") @@ -554,7 +583,7 @@ struct BridgeJSLink { dtsLines.append("export const \(enumDefinition.name): {") for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append(" readonly \(caseName): \(index);") + dtsLines.append("readonly \(caseName): \(index);".indent(count: 4)) } dtsLines.append("};") dtsLines.append( @@ -608,7 +637,7 @@ struct BridgeJSLink { case "Float", "Double": formattedValue = rawValue default: formattedValue = rawValue } - dtsLines.append(" \(caseName) = \(formattedValue),") + dtsLines.append("\(caseName) = \(formattedValue),".indent(count: 4)) } dtsLines.append("}") dtsLines.append("") @@ -630,7 +659,7 @@ struct BridgeJSLink { formattedValue = rawValue } - dtsLines.append(" readonly \(caseName): \(formattedValue);") + dtsLines.append("readonly \(caseName): \(formattedValue);".indent(count: 4)) } dtsLines.append("};") dtsLines.append( @@ -780,7 +809,7 @@ struct BridgeJSLink { uniqueNamespaces.sorted().forEach { namespace in lines.append("if (typeof globalThis.\(namespace) === 'undefined') {") - lines.append(" globalThis.\(namespace) = {};") + lines.append("globalThis.\(namespace) = {};".indent(count: 4)) lines.append("}") } @@ -790,8 +819,10 @@ struct BridgeJSLink { } namespacedEnums.forEach { enumDefinition in - let namespacePath: String = enumDefinition.namespace?.joined(separator: ".") ?? "" - lines.append("globalThis.\(namespacePath).\(enumDefinition.name) = exports.\(enumDefinition.name);") + if enumDefinition.enumType == .associatedValue { + let namespacePath: String = enumDefinition.namespace?.joined(separator: ".") ?? "" + lines.append("globalThis.\(namespacePath).\(enumDefinition.name) = exports.\(enumDefinition.name);") + } } namespacedFunctions.forEach { function in @@ -1015,7 +1046,7 @@ struct BridgeJSLink { uniqueNamespaces.sorted().forEach { namespace in lines.append("if (typeof globalThis.\(namespace) === 'undefined') {") - lines.append(" globalThis.\(namespace) = {};") + lines.append("globalThis.\(namespace) = {};".indent(count: 4)) lines.append("}") } @@ -1037,6 +1068,44 @@ struct BridgeJSLink { return lines } + func renderTopLevelEnumNamespaceAssignments(namespacedEnums: [ExportedEnum]) -> [String] { + let topLevelNamespacedEnums = namespacedEnums.filter { $0.enumType == .simple || $0.enumType == .rawValue } + + guard !topLevelNamespacedEnums.isEmpty else { return [] } + + var lines: [String] = [] + var uniqueNamespaces: [String] = [] + var seen = Set() + + for enumDef in topLevelNamespacedEnums { + guard let namespacePath = enumDef.namespace else { continue } + namespacePath.enumerated().forEach { (index, _) in + let path = namespacePath[0...index].joined(separator: ".") + if !seen.contains(path) { + seen.insert(path) + uniqueNamespaces.append(path) + } + } + } + + for namespace in uniqueNamespaces { + lines.append("if (typeof globalThis.\(namespace) === 'undefined') {") + lines.append("globalThis.\(namespace) = {};".indent(count: 4)) + lines.append("}") + } + + if !lines.isEmpty { + lines.append("") + } + + for enumDef in topLevelNamespacedEnums { + let namespacePath = enumDef.namespace?.joined(separator: ".") ?? "" + lines.append("globalThis.\(namespacePath).\(enumDef.name) = \(enumDef.name);") + } + + return lines + } + private struct NamespaceContent { var functions: [ExportedFunction] = [] var classes: [ExportedClass] = [] @@ -1410,7 +1479,9 @@ extension String { func indent(count: Int) -> String { return String(repeating: " ", count: count) + self } +} +fileprivate extension String { var capitalizedFirstLetter: String { guard !isEmpty else { return self } return prefix(1).uppercased() + dropFirst() diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift index 68c666db4..26a4e9c3a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift @@ -21,7 +21,7 @@ // Invalid to declare @JS(namespace) here as it would be conflicting with nesting @JS class HTTPServer { @JS init() {} - @JS func call(_ method: Method) + @JS func call(_ method: Method) {} } } } @@ -49,7 +49,7 @@ enum Internal { } @JS class TestServer { @JS init() {} - @JS func call(_ method: SupportedMethod) + @JS func call(_ method: SupportedMethod) {} } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js index d9f4b87db..3e080948f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js @@ -4,6 +4,27 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. +export const Direction = { + North: 0, + South: 1, + East: 2, + West: 3, +}; + +export const Status = { + Loading: 0, + Success: 1, + Error: 2, +}; + +export const TSDirection = { + North: 0, + South: 1, + East: 2, + West: 3, +}; + + export async function createInstantiator(options, swift) { let instance; let memory; @@ -63,30 +84,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - const Direction = { - North: 0, - South: 1, - East: 2, - West: 3, - }; - - const Status = { - Loading: 0, - Success: 1, - Error: 2, - }; - - const TSDirection = { - North: 0, - South: 1, - East: 2, - West: 3, - }; - return { - Direction, - Status, - TSDirection, setDirection: function bjs_setDirection(direction) { instance.exports.bjs_setDirection(direction | 0); }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js index 530822a52..12613dd89 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js @@ -4,6 +4,53 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. +export const Method = { + Get: 0, + Post: 1, + Put: 2, + Delete: 3, +}; + +export const LogLevel = { + Debug: "debug", + Info: "info", + Warning: "warning", + Error: "error", +}; + +export const Port = { + Http: 80, + Https: 443, + Development: 3000, +}; + +export const SupportedMethod = { + Get: 0, + Post: 1, +}; + + +if (typeof globalThis.Networking === 'undefined') { + globalThis.Networking = {}; +} +if (typeof globalThis.Networking.API === 'undefined') { + globalThis.Networking.API = {}; +} +if (typeof globalThis.Configuration === 'undefined') { + globalThis.Configuration = {}; +} +if (typeof globalThis.Networking.APIV2 === 'undefined') { + globalThis.Networking.APIV2 = {}; +} +if (typeof globalThis.Networking.APIV2.Internal === 'undefined') { + globalThis.Networking.APIV2.Internal = {}; +} + +globalThis.Networking.API.Method = Method; +globalThis.Configuration.LogLevel = LogLevel; +globalThis.Configuration.Port = Port; +globalThis.Networking.APIV2.Internal.SupportedMethod = SupportedMethod; + export async function createInstantiator(options, swift) { let instance; let memory; @@ -141,44 +188,12 @@ export async function createInstantiator(options, swift) { instance.exports.bjs_TestServer_call(this.pointer, method | 0); } } - const Method = { - Get: 0, - Post: 1, - Put: 2, - Delete: 3, - }; - - const LogLevel = { - Debug: "debug", - Info: "info", - Warning: "warning", - Error: "error", - }; - - const Port = { - Http: 80, - Https: 443, - Development: 3000, - }; - - const SupportedMethod = { - Get: 0, - Post: 1, - }; - const exports = { Converter, HTTPServer, TestServer, - Method, - LogLevel, - Port, - SupportedMethod, }; - if (typeof globalThis.Configuration === 'undefined') { - globalThis.Configuration = {}; - } if (typeof globalThis.Networking === 'undefined') { globalThis.Networking = {}; } @@ -197,10 +212,6 @@ export async function createInstantiator(options, swift) { globalThis.Utils.Converter = exports.Converter; globalThis.Networking.API.HTTPServer = exports.HTTPServer; globalThis.Networking.APIV2.Internal.TestServer = exports.TestServer; - globalThis.Networking.API.Method = exports.Method; - globalThis.Configuration.LogLevel = exports.LogLevel; - globalThis.Configuration.Port = exports.Port; - globalThis.Networking.APIV2.Internal.SupportedMethod = exports.SupportedMethod; return exports; }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js index e8b7adcf9..68a2b19fa 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js @@ -4,6 +4,82 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. +export const Theme = { + Light: "light", + Dark: "dark", + Auto: "auto", +}; + +export const TSTheme = { + Light: "light", + Dark: "dark", + Auto: "auto", +}; + +export const FeatureFlag = { + Enabled: true, + Disabled: false, +}; + +export const HttpStatus = { + Ok: 200, + NotFound: 404, + ServerError: 500, +}; + +export const TSHttpStatus = { + Ok: 200, + NotFound: 404, + ServerError: 500, +}; + +export const Priority = { + Lowest: 1, + Low: 2, + Medium: 3, + High: 4, + Highest: 5, +}; + +export const FileSize = { + Tiny: 1024, + Small: 10240, + Medium: 102400, + Large: 1048576, +}; + +export const UserId = { + Guest: 0, + User: 1000, + Admin: 9999, +}; + +export const TokenId = { + Invalid: 0, + Session: 12345, + Refresh: 67890, +}; + +export const SessionId = { + None: 0, + Active: 9876543210, + Expired: 1234567890, +}; + +export const Precision = { + Rough: 0.1, + Normal: 0.01, + Fine: 0.001, +}; + +export const Ratio = { + Quarter: 0.25, + Half: 0.5, + Golden: 1.618, + Pi: 3.14159, +}; + + export async function createInstantiator(options, swift) { let instance; let memory; @@ -63,94 +139,7 @@ export async function createInstantiator(options, swift) { createExports: (instance) => { const js = swift.memory.heap; - const Theme = { - Light: "light", - Dark: "dark", - Auto: "auto", - }; - - const TSTheme = { - Light: "light", - Dark: "dark", - Auto: "auto", - }; - - const FeatureFlag = { - Enabled: true, - Disabled: false, - }; - - const HttpStatus = { - Ok: 200, - NotFound: 404, - ServerError: 500, - }; - - const TSHttpStatus = { - Ok: 200, - NotFound: 404, - ServerError: 500, - }; - - const Priority = { - Lowest: 1, - Low: 2, - Medium: 3, - High: 4, - Highest: 5, - }; - - const FileSize = { - Tiny: 1024, - Small: 10240, - Medium: 102400, - Large: 1048576, - }; - - const UserId = { - Guest: 0, - User: 1000, - Admin: 9999, - }; - - const TokenId = { - Invalid: 0, - Session: 12345, - Refresh: 67890, - }; - - const SessionId = { - None: 0, - Active: 9876543210, - Expired: 1234567890, - }; - - const Precision = { - Rough: 0.1, - Normal: 0.01, - Fine: 0.001, - }; - - const Ratio = { - Quarter: 0.25, - Half: 0.5, - Golden: 1.618, - Pi: 3.14159, - }; - return { - Theme, - TSTheme, - FeatureFlag, - HttpStatus, - TSHttpStatus, - Priority, - FileSize, - UserId, - TokenId, - SessionId, - Precision, - Ratio, setTheme: function bjs_setTheme(theme) { const themeBytes = textEncoder.encode(theme); const themeId = swift.memory.retain(themeBytes); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift index 84ad98215..991b5c6c2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift @@ -25,7 +25,8 @@ public func _bjs_setTheme(themeBytes: Int32, themeLen: Int32) -> Void { public func _bjs_getTheme() -> Void { #if arch(wasm32) let ret = getTheme() - return ret.rawValue.withUTF8 { ptr in + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) } #else @@ -52,7 +53,8 @@ public func _bjs_setTSTheme(themeBytes: Int32, themeLen: Int32) -> Void { public func _bjs_getTSTheme() -> Void { #if arch(wasm32) let ret = getTSTheme() - return ret.rawValue.withUTF8 { ptr in + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) } #else @@ -322,7 +324,8 @@ public func _bjs_convertPriority(status: Int32) -> Int32 { public func _bjs_validateSession(session: Int64) -> Void { #if arch(wasm32) let ret = validateSession(_: SessionId(rawValue: UInt64(bitPattern: Int64(session)))!) - return ret.rawValue.withUTF8 { ptr in + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) } #else diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 307fa21ed..bd080623c 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -123,6 +123,153 @@ struct TestError: Error { return greeter.jsValue.object! } +// MARK: - Enum Tests + +@JS enum Direction { + case north + case south + case east + case west +} + +@JS enum Status { + case loading + case success + case error +} + +@JS enum Theme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} + +@JS enum HttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + +@JS(enumStyle: .tsEnum) enum TSDirection { + case north + case south + case east + case west +} + +@JS(enumStyle: .tsEnum) enum TSTheme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} + +@JS func setDirection(_ direction: Direction) -> Direction { + return direction +} + +@JS func getDirection() -> Direction { + return .north +} + +@JS func processDirection(_ input: Direction) -> Status { + switch input { + case .north, .south: return .success + case .east, .west: return .loading + } +} + +@JS func setTheme(_ theme: Theme) -> Theme { + return theme +} + +@JS func getTheme() -> Theme { + return .light +} + +@JS func setHttpStatus(_ status: HttpStatus) -> HttpStatus { + return status +} + +@JS func getHttpStatus() -> HttpStatus { + return .ok +} + +@JS func processTheme(_ theme: Theme) -> HttpStatus { + switch theme { + case .light: return .ok + case .dark: return .notFound + case .auto: return .serverError + } +} + +@JS func setTSDirection(_ direction: TSDirection) -> TSDirection { + return direction +} + +@JS func getTSDirection() -> TSDirection { + return .north +} + +@JS func setTSTheme(_ theme: TSTheme) -> TSTheme { + return theme +} + +@JS func getTSTheme() -> TSTheme { + return .light +} + +@JS enum Utils { + @JS class Converter { + @JS init() {} + + @JS func toString(value: Int) -> String { + return String(value) + } + } +} + +@JS enum Networking { + @JS enum API { + @JS enum Method { + case get + case post + case put + case delete + } + @JS class HTTPServer { + @JS init() {} + @JS func call(_ method: Method) {} + } + } +} + +@JS enum Configuration { + @JS enum LogLevel: String { + case debug = "debug" + case info = "info" + case warning = "warning" + case error = "error" + } + + @JS enum Port: Int { + case http = 80 + case https = 443 + case development = 3000 + } +} + +@JS(namespace: "Networking.APIV2") +enum Internal { + @JS enum SupportedMethod { + case get + case post + } + @JS class TestServer { + @JS init() {} + @JS func call(_ method: SupportedMethod) {} + } +} + class ExportAPITests: XCTestCase { func testAll() { var hasDeinitGreeter = false diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift index 579dd36b8..15e1cfc50 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift @@ -6,6 +6,144 @@ @_spi(BridgeJS) import JavaScriptKit +extension Direction { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} + +extension Status { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .loading + case 1: + self = .success + case 2: + self = .error + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .loading: + return 0 + case .success: + return 1 + case .error: + return 2 + } + } +} + +extension TSDirection { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} + +extension Networking.API.Method { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .get + case 1: + self = .post + case 2: + self = .put + case 3: + self = .delete + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .get: + return 0 + case .post: + return 1 + case .put: + return 2 + case .delete: + return 3 + } + } +} + +extension Internal.SupportedMethod { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .get + case 1: + self = .post + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .get: + return 0 + case .post: + return 1 + } + } +} + @_expose(wasm, "bjs_roundTripVoid") @_cdecl("bjs_roundTripVoid") public func _bjs_roundTripVoid() -> Void { @@ -477,6 +615,162 @@ public func _bjs_testSwiftClassAsJSValue(greeter: UnsafeMutableRawPointer) -> In #endif } +@_expose(wasm, "bjs_setDirection") +@_cdecl("bjs_setDirection") +public func _bjs_setDirection(direction: Int32) -> Int32 { + #if arch(wasm32) + let ret = setDirection(_: Direction(bridgeJSRawValue: direction)!) + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getDirection") +@_cdecl("bjs_getDirection") +public func _bjs_getDirection() -> Int32 { + #if arch(wasm32) + let ret = getDirection() + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_processDirection") +@_cdecl("bjs_processDirection") +public func _bjs_processDirection(input: Int32) -> Int32 { + #if arch(wasm32) + let ret = processDirection(_: Direction(bridgeJSRawValue: input)!) + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setTheme") +@_cdecl("bjs_setTheme") +public func _bjs_setTheme(themeBytes: Int32, themeLen: Int32) -> Void { + #if arch(wasm32) + let theme = String(unsafeUninitializedCapacity: Int(themeLen)) { b in + _swift_js_init_memory(themeBytes, b.baseAddress.unsafelyUnwrapped) + return Int(themeLen) + } + let ret = setTheme(_: Theme(rawValue: theme)!) + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTheme") +@_cdecl("bjs_getTheme") +public func _bjs_getTheme() -> Void { + #if arch(wasm32) + let ret = getTheme() + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setHttpStatus") +@_cdecl("bjs_setHttpStatus") +public func _bjs_setHttpStatus(status: Int32) -> Int32 { + #if arch(wasm32) + let ret = setHttpStatus(_: HttpStatus(rawValue: Int(status))!) + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getHttpStatus") +@_cdecl("bjs_getHttpStatus") +public func _bjs_getHttpStatus() -> Int32 { + #if arch(wasm32) + let ret = getHttpStatus() + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_processTheme") +@_cdecl("bjs_processTheme") +public func _bjs_processTheme(themeBytes: Int32, themeLen: Int32) -> Int32 { + #if arch(wasm32) + let theme = String(unsafeUninitializedCapacity: Int(themeLen)) { b in + _swift_js_init_memory(themeBytes, b.baseAddress.unsafelyUnwrapped) + return Int(themeLen) + } + let ret = processTheme(_: Theme(rawValue: theme)!) + return Int32(ret.rawValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setTSDirection") +@_cdecl("bjs_setTSDirection") +public func _bjs_setTSDirection(direction: Int32) -> Int32 { + #if arch(wasm32) + let ret = setTSDirection(_: TSDirection(bridgeJSRawValue: direction)!) + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTSDirection") +@_cdecl("bjs_getTSDirection") +public func _bjs_getTSDirection() -> Int32 { + #if arch(wasm32) + let ret = getTSDirection() + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setTSTheme") +@_cdecl("bjs_setTSTheme") +public func _bjs_setTSTheme(themeBytes: Int32, themeLen: Int32) -> Void { + #if arch(wasm32) + let theme = String(unsafeUninitializedCapacity: Int(themeLen)) { b in + _swift_js_init_memory(themeBytes, b.baseAddress.unsafelyUnwrapped) + return Int(themeLen) + } + let ret = setTSTheme(_: TSTheme(rawValue: theme)!) + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getTSTheme") +@_cdecl("bjs_getTSTheme") +public func _bjs_getTSTheme() -> Void { + #if arch(wasm32) + let ret = getTSTheme() + var rawValue = ret.rawValue + return rawValue.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_Greeter_init") @_cdecl("bjs_Greeter_init") public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer { @@ -567,4 +861,112 @@ extension Calculator: ConvertibleToJSValue { func _bjs_Calculator_wrap(_: UnsafeMutableRawPointer) -> Int32 return .object(JSObject(id: UInt32(bitPattern: _bjs_Calculator_wrap(Unmanaged.passRetained(self).toOpaque())))) } +} + +@_expose(wasm, "bjs_Converter_init") +@_cdecl("bjs_Converter_init") +public func _bjs_Converter_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Utils.Converter() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Converter_toString") +@_cdecl("bjs_Converter_toString") +public func _bjs_Converter_toString(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().toString(value: Int(value)) + return ret.withUTF8 { ptr in + _swift_js_return_string(ptr.baseAddress, Int32(ptr.count)) + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Converter_deinit") +@_cdecl("bjs_Converter_deinit") +public func _bjs_Converter_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension Utils.Converter: ConvertibleToJSValue { + var jsValue: JSValue { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Converter_wrap") + func _bjs_Converter_wrap(_: UnsafeMutableRawPointer) -> Int32 + return .object(JSObject(id: UInt32(bitPattern: _bjs_Converter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +@_expose(wasm, "bjs_HTTPServer_init") +@_cdecl("bjs_HTTPServer_init") +public func _bjs_HTTPServer_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Networking.API.HTTPServer() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_HTTPServer_call") +@_cdecl("bjs_HTTPServer_call") +public func _bjs_HTTPServer_call(_self: UnsafeMutableRawPointer, method: Int32) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Networking.API.Method(bridgeJSRawValue: method)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_HTTPServer_deinit") +@_cdecl("bjs_HTTPServer_deinit") +public func _bjs_HTTPServer_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension Networking.API.HTTPServer: ConvertibleToJSValue { + var jsValue: JSValue { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_HTTPServer_wrap") + func _bjs_HTTPServer_wrap(_: UnsafeMutableRawPointer) -> Int32 + return .object(JSObject(id: UInt32(bitPattern: _bjs_HTTPServer_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +@_expose(wasm, "bjs_TestServer_init") +@_cdecl("bjs_TestServer_init") +public func _bjs_TestServer_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = Internal.TestServer() + return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_TestServer_call") +@_cdecl("bjs_TestServer_call") +public func _bjs_TestServer_call(_self: UnsafeMutableRawPointer, method: Int32) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(_self).takeUnretainedValue().call(_: Internal.SupportedMethod(bridgeJSRawValue: method)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_TestServer_deinit") +@_cdecl("bjs_TestServer_deinit") +public func _bjs_TestServer_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension Internal.TestServer: ConvertibleToJSValue { + var jsValue: JSValue { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_TestServer_wrap") + func _bjs_TestServer_wrap(_: UnsafeMutableRawPointer) -> Int32 + return .object(JSObject(id: UInt32(bitPattern: _bjs_TestServer_wrap(Unmanaged.passRetained(self).toOpaque())))) + } } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json index 19c77f92f..b4642d8ae 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -126,10 +126,453 @@ ], "name" : "Calculator", "swiftCallName" : "Calculator" + }, + { + "constructor" : { + "abiName" : "bjs_Converter_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_Converter_toString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "toString", + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "Converter", + "namespace" : [ + "Utils" + ], + "swiftCallName" : "Utils.Converter" + }, + { + "constructor" : { + "abiName" : "bjs_HTTPServer_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_HTTPServer_call", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "call", + "parameters" : [ + { + "label" : "_", + "name" : "method", + "type" : { + "caseEnum" : { + "_0" : "Networking.API.Method" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "HTTPServer", + "namespace" : [ + "Networking", + "API" + ], + "swiftCallName" : "Networking.API.HTTPServer" + }, + { + "constructor" : { + "abiName" : "bjs_TestServer_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_TestServer_call", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "call", + "parameters" : [ + { + "label" : "_", + "name" : "method", + "type" : { + "caseEnum" : { + "_0" : "Internal.SupportedMethod" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "TestServer", + "namespace" : [ + "Networking", + "APIV2", + "Internal" + ], + "swiftCallName" : "Internal.TestServer" } ], "enums" : [ + { + "cases" : [ + { + "associatedValues" : [ + ], + "name" : "north" + }, + { + "associatedValues" : [ + + ], + "name" : "south" + }, + { + "associatedValues" : [ + + ], + "name" : "east" + }, + { + "associatedValues" : [ + + ], + "name" : "west" + } + ], + "emitStyle" : "const", + "name" : "Direction", + "swiftCallName" : "Direction" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "loading" + }, + { + "associatedValues" : [ + + ], + "name" : "success" + }, + { + "associatedValues" : [ + + ], + "name" : "error" + } + ], + "emitStyle" : "const", + "name" : "Status", + "swiftCallName" : "Status" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "light", + "rawValue" : "light" + }, + { + "associatedValues" : [ + + ], + "name" : "dark", + "rawValue" : "dark" + }, + { + "associatedValues" : [ + + ], + "name" : "auto", + "rawValue" : "auto" + } + ], + "emitStyle" : "const", + "name" : "Theme", + "rawType" : "String", + "swiftCallName" : "Theme" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "ok", + "rawValue" : "200" + }, + { + "associatedValues" : [ + + ], + "name" : "notFound", + "rawValue" : "404" + }, + { + "associatedValues" : [ + + ], + "name" : "serverError", + "rawValue" : "500" + } + ], + "emitStyle" : "const", + "name" : "HttpStatus", + "rawType" : "Int", + "swiftCallName" : "HttpStatus" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "north" + }, + { + "associatedValues" : [ + + ], + "name" : "south" + }, + { + "associatedValues" : [ + + ], + "name" : "east" + }, + { + "associatedValues" : [ + + ], + "name" : "west" + } + ], + "emitStyle" : "tsEnum", + "name" : "TSDirection", + "swiftCallName" : "TSDirection" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "light", + "rawValue" : "light" + }, + { + "associatedValues" : [ + + ], + "name" : "dark", + "rawValue" : "dark" + }, + { + "associatedValues" : [ + + ], + "name" : "auto", + "rawValue" : "auto" + } + ], + "emitStyle" : "tsEnum", + "name" : "TSTheme", + "rawType" : "String", + "swiftCallName" : "TSTheme" + }, + { + "cases" : [ + + ], + "emitStyle" : "const", + "name" : "Utils", + "swiftCallName" : "Utils" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "get" + }, + { + "associatedValues" : [ + + ], + "name" : "post" + }, + { + "associatedValues" : [ + + ], + "name" : "put" + }, + { + "associatedValues" : [ + + ], + "name" : "delete" + } + ], + "emitStyle" : "const", + "name" : "Method", + "namespace" : [ + "Networking", + "API" + ], + "swiftCallName" : "Networking.API.Method" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "debug", + "rawValue" : "debug" + }, + { + "associatedValues" : [ + + ], + "name" : "info", + "rawValue" : "info" + }, + { + "associatedValues" : [ + + ], + "name" : "warning", + "rawValue" : "warning" + }, + { + "associatedValues" : [ + + ], + "name" : "error", + "rawValue" : "error" + } + ], + "emitStyle" : "const", + "name" : "LogLevel", + "namespace" : [ + "Configuration" + ], + "rawType" : "String", + "swiftCallName" : "Configuration.LogLevel" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "http", + "rawValue" : "80" + }, + { + "associatedValues" : [ + + ], + "name" : "https", + "rawValue" : "443" + }, + { + "associatedValues" : [ + + ], + "name" : "development", + "rawValue" : "3000" + } + ], + "emitStyle" : "const", + "name" : "Port", + "namespace" : [ + "Configuration" + ], + "rawType" : "Int", + "swiftCallName" : "Configuration.Port" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "get" + }, + { + "associatedValues" : [ + + ], + "name" : "post" + } + ], + "emitStyle" : "const", + "name" : "SupportedMethod", + "namespace" : [ + "Networking", + "APIV2", + "Internal" + ], + "swiftCallName" : "Internal.SupportedMethod" + } ], "functions" : [ { @@ -782,6 +1225,265 @@ } } + }, + { + "abiName" : "bjs_setDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setDirection", + "parameters" : [ + { + "label" : "_", + "name" : "direction", + "type" : { + "caseEnum" : { + "_0" : "Direction" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "Direction" + } + } + }, + { + "abiName" : "bjs_getDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getDirection", + "parameters" : [ + + ], + "returnType" : { + "caseEnum" : { + "_0" : "Direction" + } + } + }, + { + "abiName" : "bjs_processDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "processDirection", + "parameters" : [ + { + "label" : "_", + "name" : "input", + "type" : { + "caseEnum" : { + "_0" : "Direction" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "Status" + } + } + }, + { + "abiName" : "bjs_setTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTheme", + "parameters" : [ + { + "label" : "_", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + }, + { + "abiName" : "bjs_getTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTheme", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + }, + { + "abiName" : "bjs_setHttpStatus", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setHttpStatus", + "parameters" : [ + { + "label" : "_", + "name" : "status", + "type" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + }, + { + "abiName" : "bjs_getHttpStatus", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getHttpStatus", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + }, + { + "abiName" : "bjs_processTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "processTheme", + "parameters" : [ + { + "label" : "_", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "Theme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "HttpStatus", + "_1" : "Int" + } + } + }, + { + "abiName" : "bjs_setTSDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTSDirection", + "parameters" : [ + { + "label" : "_", + "name" : "direction", + "type" : { + "caseEnum" : { + "_0" : "TSDirection" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "TSDirection" + } + } + }, + { + "abiName" : "bjs_getTSDirection", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTSDirection", + "parameters" : [ + + ], + "returnType" : { + "caseEnum" : { + "_0" : "TSDirection" + } + } + }, + { + "abiName" : "bjs_setTSTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "setTSTheme", + "parameters" : [ + { + "label" : "_", + "name" : "theme", + "type" : { + "rawValueEnum" : { + "_0" : "TSTheme", + "_1" : "String" + } + } + } + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "TSTheme", + "_1" : "String" + } + } + }, + { + "abiName" : "bjs_getTSTheme", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "getTSTheme", + "parameters" : [ + + ], + "returnType" : { + "rawValueEnum" : { + "_0" : "TSTheme", + "_1" : "String" + } + } } ], "moduleName" : "BridgeJSRuntimeTests" diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 88de303a0..6954d87ca 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,5 +1,9 @@ // @ts-check +import { + Direction, Status, Theme, HttpStatus, TSDirection, TSTheme +} from '../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.js'; + /** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ export async function setupOptions(options, context) { Error.stackTraceLimit = 100; @@ -165,6 +169,89 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { } catch (error) { assert.fail("Expected no error"); } + + assert.equal(Direction.North, 0); + assert.equal(Direction.South, 1); + assert.equal(Direction.East, 2); + assert.equal(Direction.West, 3); + assert.equal(Status.Loading, 0); + assert.equal(Status.Success, 1); + assert.equal(Status.Error, 2); + + assert.equal(exports.setDirection(Direction.North), Direction.North); + assert.equal(exports.setDirection(Direction.South), Direction.South); + assert.equal(exports.getDirection(), Direction.North); + assert.equal(exports.processDirection(Direction.North), Status.Success); + assert.equal(exports.processDirection(Direction.East), Status.Loading); + + assert.equal(Theme.Light, "light"); + assert.equal(Theme.Dark, "dark"); + assert.equal(Theme.Auto, "auto"); + assert.equal(HttpStatus.Ok, 200); + assert.equal(HttpStatus.NotFound, 404); + assert.equal(HttpStatus.ServerError, 500); + + assert.equal(exports.setTheme(Theme.Light), Theme.Light); + assert.equal(exports.setTheme(Theme.Dark), Theme.Dark); + assert.equal(exports.getTheme(), Theme.Light); + assert.equal(exports.setHttpStatus(HttpStatus.Ok), HttpStatus.Ok); + assert.equal(exports.getHttpStatus(), HttpStatus.Ok); + assert.equal(exports.processTheme(Theme.Light), HttpStatus.Ok); + assert.equal(exports.processTheme(Theme.Dark), HttpStatus.NotFound); + + assert.equal(TSDirection.North, 0); + assert.equal(TSDirection.South, 1); + assert.equal(TSDirection.East, 2); + assert.equal(TSDirection.West, 3); + assert.equal(TSTheme.Light, "light"); + assert.equal(TSTheme.Dark, "dark"); + assert.equal(TSTheme.Auto, "auto"); + + assert.equal(exports.setTSDirection(TSDirection.North), TSDirection.North); + assert.equal(exports.getTSDirection(), TSDirection.North); + assert.equal(exports.setTSTheme(TSTheme.Light), TSTheme.Light); + assert.equal(exports.getTSTheme(), TSTheme.Light); + + assert.equal(globalThis.Networking.API.Method.Get, 0); + assert.equal(globalThis.Networking.API.Method.Post, 1); + assert.equal(globalThis.Networking.API.Method.Put, 2); + assert.equal(globalThis.Networking.API.Method.Delete, 3); + assert.equal(globalThis.Configuration.LogLevel.Debug, "debug"); + assert.equal(globalThis.Configuration.LogLevel.Info, "info"); + assert.equal(globalThis.Configuration.LogLevel.Warning, "warning"); + assert.equal(globalThis.Configuration.LogLevel.Error, "error"); + assert.equal(globalThis.Configuration.Port.Http, 80); + assert.equal(globalThis.Configuration.Port.Https, 443); + assert.equal(globalThis.Configuration.Port.Development, 3000); + assert.equal(globalThis.Networking.APIV2.Internal.SupportedMethod.Get, 0); + assert.equal(globalThis.Networking.APIV2.Internal.SupportedMethod.Post, 1); + + const converter = new exports.Converter(); + assert.equal(converter.toString(42), "42"); + assert.equal(converter.toString(123), "123"); + converter.release(); + + const httpServer = new exports.HTTPServer(); + httpServer.call(globalThis.Networking.API.Method.Get); + httpServer.call(globalThis.Networking.API.Method.Post); + httpServer.release(); + + const testServer = new exports.TestServer(); + testServer.call(globalThis.Networking.APIV2.Internal.SupportedMethod.Get); + testServer.call(globalThis.Networking.APIV2.Internal.SupportedMethod.Post); + testServer.release(); + + const globalConverter = new globalThis.Utils.Converter(); + assert.equal(globalConverter.toString(99), "99"); + globalConverter.release(); + + const globalHttpServer = new globalThis.Networking.API.HTTPServer(); + globalHttpServer.call(globalThis.Networking.API.Method.Get); + globalHttpServer.release(); + + const globalTestServer = new globalThis.Networking.APIV2.Internal.TestServer(); + globalTestServer.call(globalThis.Networking.APIV2.Internal.SupportedMethod.Post); + globalTestServer.release(); } /** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */