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 e928011a..fba27aa1 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 c7966a84..9c4679e9 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 1483692f..07712a83 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 a3c5b401..e1f1b3ef 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 00000000..7ddef637 --- /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 48fb77cf..6638cb8d 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 00000000..b82ef33f --- /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 00000000..15ea3584 --- /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 00000000..3948470f --- /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 00000000..86e2ba17 --- /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 c122f179..16995227 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 1da2f58e..6ced9f91 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 21d11fa4..0e39bd38 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 00000000..f4d2c90b --- /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 00000000..ed355d06 --- /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 00000000..5a1fb674 --- /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 00000000..0136a85f --- /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 00000000..7f1ededc --- /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 00000000..e63be465 --- /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 f81c7e47..874bebd3 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 394d996b..7e955b1a 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 6915a61a..cdf5aa25 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 4873fc33..40ac2cda 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 3b93b2dd..1cee664b 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 53332b97..7daa5bbb 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 1892eb46..19af42bd 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 ea47fb55..bb5cabd8 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 16ed1081..524a0466 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 f98cea55..67c63565 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 3220ae7b..5830cd3d 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 ab4caba3..0a1da2c5 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 705c6a37..15e5d4d8 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 b2089962..95b558a1 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 2eb9dee5..dccc193e 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 c7d622ea..cc060599 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 c200c077..91017e56 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 ca497688..77087eba 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 8e715451..c0d5347d 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 00000000..3a29a1a1 --- /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 00000000..affe3d3d --- /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 00000000..fc427dec --- /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 00000000..e241d180 --- /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 00000000..5b88d275 --- /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 00000000..185c4ac8 --- /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 1d1b0fbe..79ff94d8 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 7ba4d9dc..c58f3c8e 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 54e00ea5..ee29313b 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 c2286d12..22df1dc5 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 23331875..75439e36 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 489f1cd5..3f621eb6 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 9acf5b20..cc3184fb 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 12c73531..413fe084 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 fba27aa1..211f7139 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 07712a83..0f40af96 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 e1f1b3ef..89f2771e 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 15ea3584..68c666db 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 3948470f..bf1a69cc 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 5a1fb674..3d37ca6c 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 0136a85f..530822a5 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 7f1ededc..d67ee1fe 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 e63be465..f4615d99 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 b2ccecc4..c6e40399 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 3a29a1a1..7527eef9 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 fc427dec..f7dd3872 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 e241d180..fa77613f 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 5b88d275..d85bc550 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 79ff94d8..a5e960be 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 3f621eb6..7f8324ac 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 2e94644d..b00ec9ab 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 e83af9fe..2b5ce07d 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 211f7139..0b5e7f20 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 0f40af96..b2fcb00c 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 89f2771e..0d872160 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 7ddef637..00000000 --- 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 6638cb8d..48fb77cf 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 b82ef33f..6d5d6b55 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 bf1a69cc..799df164 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 16995227..c122f179 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 6ced9f91..1da2f58e 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 0e39bd38..21d11fa4 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 f4d2c90b..2d45e998 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 ed355d06..d9f4b87d 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 d67ee1fe..51b020ad 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 f4615d99..e8b7adcf 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 874bebd3..f81c7e47 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 7e955b1a..394d996b 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 cdf5aa25..6915a61a 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 40ac2cda..4873fc33 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 1cee664b..3b93b2dd 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 7daa5bbb..53332b97 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 19af42bd..1892eb46 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 bb5cabd8..ea47fb55 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 524a0466..16ed1081 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 67c63565..f98cea55 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 5830cd3d..3220ae7b 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 0a1da2c5..ab4caba3 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 15e5d4d8..705c6a37 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 95b558a1..b2089962 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 dccc193e..2eb9dee5 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 cc060599..c7d622ea 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 91017e56..c200c077 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 77087eba..ca497688 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 7527eef9..efb6e805 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 affe3d3d..363ade82 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 f7dd3872..a9483455 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 fa77613f..9517ad80 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 d85bc550..09ce5d6e 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 185c4ac8..84ad9821 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 6ce30772..ad7932f1 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 dac264ff..b8a44a08 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 97b86cec..19c77f92 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 0b5e7f20..a5f2e108 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 9c4679e9..bcbae469 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 b2fcb00c..046cb92d 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 68c666db..26a4e9c3 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 d9f4b87d..3e080948 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 530822a5..12613dd8 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 e8b7adcf..68a2b19f 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 84ad9821..991b5c6c 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 307fa21e..bd080623 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 579dd36b..15e1cfc5 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 19c77f92..b4642d8a 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 88de303a..6954d87c 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 */