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 e928011a..a5f2e108 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,11 +70,32 @@ 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] = [] + /// 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 { @@ -119,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 } } @@ -172,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 { @@ -231,10 +267,32 @@ 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 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 } @@ -264,34 +322,226 @@ 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 { + 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) { + // 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 { + return .skipChildren + } + + guard let jsAttribute = node.attributes.firstJSAttribute else { + return .skipChildren + } + + 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 + + let attributeNamespace = extractNamespace(from: jsAttribute) + let computedNamespace = computeNamespace(for: node) + + 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)) + + return .visitChildren + } + + override func visitPost(_ node: EnumDeclSyntax) { + 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 + } + + let attributeNamespace = extractNamespace(from: jsAttribute) + let computedNamespace = computeNamespace(for: node) + + let effectiveNamespace: [String]? + if computedNamespace == nil && attributeNamespace != nil { + effectiveNamespace = attributeNamespace + } else { + 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, + emitStyle: emitStyle + ) + 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)) + } + } + let enumCase = EnumCase( + name: caseName, + rawValue: rawValue, + associatedValues: associatedValues + ) + + currentEnum.cases.append(enumCase) + } + + 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 + } } func parseSingleFile(_ sourceFile: SourceFileSyntax) throws -> [DiagnosticError] { @@ -303,9 +553,36 @@ public class ExportSwift { collector.exportedClassByName[$0]! } ) + exportedEnums.append( + contentsOf: collector.exportedEnumNames.map { + collector.exportedEnumByName[$0]! + } + ) 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 @@ -313,9 +590,40 @@ public class ExportSwift { guard let identifier = type.as(IdentifierTypeSyntax.self) else { return nil } + guard let typeDecl = typeDeclResolver.lookupType(for: identifier) else { return nil } + if let enumDecl = typeDecl.as(EnumDeclSyntax.self) { + 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 { return nil } @@ -334,10 +642,15 @@ 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) + + for enumDef in exportedEnums where enumDef.enumType == .simple { + decls.append(renderCaseEnumHelpers(enumDef)) + } + for function in exportedFunctions { decls.append(renderSingleExportedFunction(function: function)) } @@ -348,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] = [] @@ -420,6 +759,63 @@ 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)(bridgeJSRawValue: \(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 { + let conversionExpr: String + switch rawType { + case .bool: + conversionExpr = "\(enumName)(rawValue: \(param.name) != 0)!" + case .uint, .uint32, .uint64: + if rawType == .uint64 { + conversionExpr = + "\(enumName)(rawValue: \(rawType.rawValue)(bitPattern: Int64(\(param.name))))!" + } else { + conversionExpr = "\(enumName)(rawValue: \(rawType.rawValue)(bitPattern: \(param.name)))!" + } + default: + conversionExpr = "\(enumName)(rawValue: \(rawType.rawValue)(\(param.name)))!" + } + + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax(stringLiteral: conversionExpr) + ) + ) + if let wasmType = rawType.wasmCoreType { + abiParameterSignatures.append((param.name, wasmType)) + } + } + case .associatedValueEnum(_): + break + case .namespaceEnum: + break case .jsObject(nil): abiParameterForwardings.append( LabeledExprSyntax( @@ -437,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( @@ -470,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 { @@ -520,6 +920,14 @@ public class ExportSwift { case .swiftHeapObject: // UnsafeMutableRawPointer is returned as an i32 pointer abiReturnType = .pointer + case .caseEnum: + abiReturnType = .i32 + case .rawValueEnum(_, let rawType): + abiReturnType = rawType == .string ? nil : rawType.wasmCoreType + case .associatedValueEnum: + abiReturnType = nil + case .namespaceEnum: + abiReturnType = nil } if effects.isAsync { @@ -542,6 +950,37 @@ public class ExportSwift { } """ ) + case .caseEnum: + abiReturnType = .i32 + append("return ret.bridgeJSRawValue") + case .rawValueEnum(_, let rawType): + if rawType == .string { + append( + """ + var rawValue = ret.rawValue + return 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)") + } + } + case .associatedValueEnum: break; + case .namespaceEnum: break; case .jsObject(nil): append( """ @@ -691,25 +1130,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 ) @@ -723,7 +1163,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() } """ ) @@ -755,7 +1195,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 @@ -766,6 +1206,10 @@ public class ExportSwift { } } +fileprivate enum Constants { + static let supportedRawTypes = SwiftEnumRawType.allCases.map { $0.rawValue } +} + extension AttributeListSyntax { fileprivate func hasJSAttribute() -> Bool { firstJSAttribute != nil @@ -825,6 +1269,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..bcbae469 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 { @@ -501,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 1483692f..046cb92d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -69,6 +69,11 @@ struct BridgeJSLink { var dtsClassLines: [String] = [] var namespacedFunctions: [ExportedFunction] = [] var namespacedClasses: [ExportedClass] = [] + 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( @@ -96,6 +101,35 @@ struct BridgeJSLink { } } + if !skeleton.enums.isEmpty { + for enumDefinition in skeleton.enums { + let (jsEnum, dtsEnum) = try renderExportedEnum(enumDefinition) + + 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) + } + } + } + for function in skeleton.functions { var (js, dts) = renderExportedFunction(function: function) @@ -124,36 +158,63 @@ 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 namespacedEnumsForExports = namespacedEnums.filter { $0.enumType == .associatedValue } + let namespaceSetupCode = namespaceBuilder.renderGlobalNamespace( namespacedFunctions: namespacedFunctions, - namespacedClasses: namespacedClasses + namespacedClasses: namespacedClasses, + namespacedEnums: namespacedEnumsForExports ) .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")) - 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")) - return { + \(enumSection)\("return {".indent(count: 12)) \(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n")) - }; + \("};".indent(count: 12)) }, """ } + 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. @@ -161,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; @@ -225,8 +286,9 @@ 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()) dtsLines.append("export type Exports = {") dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) }) @@ -234,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. @@ -241,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<{ @@ -318,102 +383,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]) { + var jsLines: [String] = [] + var dtsLines: [String] = [] + let style: EnumEmitStyle = enumDefinition.emitStyle + + switch enumDefinition.enumType { + case .simple: + jsLines.append("const \(enumDefinition.name) = {") + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + jsLines.append("\(caseName): \(index),".indent(count: 4)) + } + jsLines.append("};") + jsLines.append("") + + if enumDefinition.namespace == nil { + 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),".indent(count: 4)) + } + 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);".indent(count: 4)) + } + 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 { + throw BridgeJSLinkError(message: "Raw value enum \(enumDefinition.name) is missing rawType") + } + + jsLines.append("const \(enumDefinition.name) = {") + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue: String + + 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 + } + + jsLines.append("\(caseName): \(formattedValue),".indent(count: 4)) + } + jsLines.append("};") + jsLines.append("") + + if enumDefinition.namespace == nil { + 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),".indent(count: 4)) + } + 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);".indent(count: 4)) + } + dtsLines.append("};") + dtsLines.append( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + ) + dtsLines.append("") + } + } + + 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) + } + func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) { let thunkBuilder = ExportedThunkBuilder(effects: function.effects) for param in function.parameters { @@ -633,8 +771,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() @@ -647,10 +788,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 @@ -663,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("}") } @@ -672,6 +818,13 @@ struct BridgeJSLink { lines.append("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);") } + namespacedEnums.forEach { enumDefinition in + if enumDefinition.enumType == .associatedValue { + 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);") @@ -698,6 +851,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 +940,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 nil + case .namespaceEnum: + return nil case .int, .float, .double: return "ret" case .bool: @@ -804,6 +991,351 @@ 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) = {};".indent(count: 4)) + 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 + } + + 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] = [] + 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 { + let style: EnumEmitStyle = enumDefinition.emitStyle + switch enumDefinition.enumType { + case .simple: + switch style { + case .tsEnum: + dtsLines.append( + "enum \(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 } + 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( + "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 @@ -949,6 +1481,13 @@ extension String { } } +fileprivate extension String { + var capitalizedFirstLetter: String { + guard !isEmpty else { return self } + return prefix(1).uppercased() + dropFirst() + } +} + extension BridgeType { var tsType: String { switch self { @@ -968,6 +1507,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..0d872160 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -4,12 +4,48 @@ public enum BridgeType: Codable, Equatable { case int, float, double, string, bool, jsObject(String?), swiftHeapObject(String), void + case caseEnum(String) + case rawValueEnum(String, SwiftEnumRawType) + case associatedValueEnum(String) + case namespaceEnum(String) } public enum WasmCoreType: String, Codable { case i32, i64, f32, f64, pointer } +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 + 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 @@ -32,6 +68,80 @@ 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 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 + } else if cases.allSatisfy(\.isSimple) { + return rawType != nil ? .rawValue : .simple + } else { + return .associatedValue + } + } + + public init( + name: String, + swiftCallName: String, + cases: [EnumCase], + rawType: String?, + namespace: [String]?, + emitStyle: EnumEmitStyle + ) { + self.name = name + self.swiftCallName = swiftCallName + self.cases = cases + self.rawType = rawType + self.namespace = namespace + self.emitStyle = emitStyle + } +} + +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 { @@ -61,17 +171,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 @@ -96,11 +209,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 +278,8 @@ public struct ImportedModuleSkeleton: Codable { } } +// MARK: - BridgeType extension + extension BridgeType { public var abiReturnType: WasmCoreType? { switch self { @@ -176,6 +293,14 @@ extension BridgeType { case .swiftHeapObject: // UnsafeMutableRawPointer is returned as an i32 pointer return .pointer + case .caseEnum: + return .i32 + case .rawValueEnum(_, let rawType): + return rawType.wasmCoreType + case .associatedValueEnum: + return nil + case .namespaceEnum: + return nil } } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift new file mode 100644 index 00000000..6d5d6b55 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumCase.swift @@ -0,0 +1,26 @@ +@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 + +@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/EnumNamespace.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift new file mode 100644 index 00000000..26a4e9c3 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumNamespace.swift @@ -0,0 +1,56 @@ +// Empty enums to act as namespace wrappers for nested elements + +@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 + } + // Invalid to declare @JS(namespace) here as it would be conflicting with nesting + @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) {} + } +} + +// 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 new file mode 100644 index 00000000..799df164 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/EnumRawType.swift @@ -0,0 +1,117 @@ +@JS enum Theme: String { + case light = "light" + case dark = "dark" + 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 +} + +@JS enum HttpStatus: Int { + case ok = 200 + case notFound = 404 + 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 + 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 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 + +@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/EnumCase.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts new file mode 100644 index 00000000..2d45e998 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.d.ts @@ -0,0 +1,44 @@ +// 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 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 = { +} +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..3e080948 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumCase.Export.js @@ -0,0 +1,109 @@ +// 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 = { + 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; + 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; + + return { + 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; + }, + setTSDirection: function bjs_setTSDirection(direction) { + instance.exports.bjs_setTSDirection(direction | 0); + }, + getTSDirection: function bjs_getTSDirection() { + const ret = instance.exports.bjs_getTSDirection(); + 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..3d37ca6c --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.d.ts @@ -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 {}; + +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 = { +} +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..12613dd8 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Export.js @@ -0,0 +1,219 @@ +// 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 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; + 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); + } + // 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) => { + instance = i; + memory = instance.exports.memory; + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype) { + const obj = Object.create(prototype); + obj.pointer = pointer; + obj.hasReleased = false; + obj.deinit = deinit; + obj.registry = new FinalizationRegistry((pointer) => { + deinit(pointer); + }); + obj.registry.register(this, obj.pointer); + return obj; + } + + release() { + this.registry.unregister(this); + this.deinit(this.pointer); + } + } + class 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 exports = { + Converter, + HTTPServer, + TestServer, + }; + + 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; + + 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 new file mode 100644 index 00000000..51b020ad --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.d.ts @@ -0,0 +1,131 @@ +// 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 enum TSTheme { + Light = "light", + Dark = "dark", + Auto = "auto", +} + +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 enum TSHttpStatus { + Ok = 200, + NotFound = 404, + ServerError = 500, +} + +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 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; + 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..68a2b19f --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumRawType.Export.js @@ -0,0 +1,264 @@ +// 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 = { + 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; + 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; + + return { + 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; + }, + 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); + }, + 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; + }, + 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); + }, + 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/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/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..efb6e805 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json @@ -0,0 +1,201 @@ +{ + "classes" : [ + + ], + "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" : "north" + }, + { + "associatedValues" : [ + + ], + "name" : "south" + }, + { + "associatedValues" : [ + + ], + "name" : "east" + }, + { + "associatedValues" : [ + + ], + "name" : "west" + } + ], + "emitStyle" : "tsEnum", + "name" : "TSDirection", + "swiftCallName" : "TSDirection" + } + ], + "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" + } + } + }, + { + "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" +} \ 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..363ade82 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.swift @@ -0,0 +1,146 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(BridgeJS) import JavaScriptKit + +extension 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(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 +} + +@_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_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 +} \ 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..a9483455 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json @@ -0,0 +1,280 @@ +{ + "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" : [ + { + "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" : [ + + ], + "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..9517ad80 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.swift @@ -0,0 +1,167 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(BridgeJS) import JavaScriptKit + +extension 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 { + #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(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: "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(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: "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 new file mode 100644 index 00000000..09ce5d6e --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json @@ -0,0 +1,1003 @@ +{ + "classes" : [ + + ], + "enums" : [ + { + "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" : "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" + }, + { + "associatedValues" : [ + + ], + "name" : "disabled", + "rawValue" : "false" + } + ], + "emitStyle" : "const", + "name" : "FeatureFlag", + "rawType" : "Bool", + "swiftCallName" : "FeatureFlag" + }, + { + "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" : "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" + }, + { + "associatedValues" : [ + + ], + "name" : "low", + "rawValue" : "2" + }, + { + "associatedValues" : [ + + ], + "name" : "medium", + "rawValue" : "3" + }, + { + "associatedValues" : [ + + ], + "name" : "high", + "rawValue" : "4" + }, + { + "associatedValues" : [ + + ], + "name" : "highest", + "rawValue" : "5" + } + ], + "emitStyle" : "const", + "name" : "Priority", + "rawType" : "Int32", + "swiftCallName" : "Priority" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "tiny", + "rawValue" : "1024" + }, + { + "associatedValues" : [ + + ], + "name" : "small", + "rawValue" : "10240" + }, + { + "associatedValues" : [ + + ], + "name" : "medium", + "rawValue" : "102400" + }, + { + "associatedValues" : [ + + ], + "name" : "large", + "rawValue" : "1048576" + } + ], + "emitStyle" : "const", + "name" : "FileSize", + "rawType" : "Int64", + "swiftCallName" : "FileSize" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "guest", + "rawValue" : "0" + }, + { + "associatedValues" : [ + + ], + "name" : "user", + "rawValue" : "1000" + }, + { + "associatedValues" : [ + + ], + "name" : "admin", + "rawValue" : "9999" + } + ], + "emitStyle" : "const", + "name" : "UserId", + "rawType" : "UInt", + "swiftCallName" : "UserId" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "invalid", + "rawValue" : "0" + }, + { + "associatedValues" : [ + + ], + "name" : "session", + "rawValue" : "12345" + }, + { + "associatedValues" : [ + + ], + "name" : "refresh", + "rawValue" : "67890" + } + ], + "emitStyle" : "const", + "name" : "TokenId", + "rawType" : "UInt32", + "swiftCallName" : "TokenId" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "none", + "rawValue" : "0" + }, + { + "associatedValues" : [ + + ], + "name" : "active", + "rawValue" : "9876543210" + }, + { + "associatedValues" : [ + + ], + "name" : "expired", + "rawValue" : "1234567890" + } + ], + "emitStyle" : "const", + "name" : "SessionId", + "rawType" : "UInt64", + "swiftCallName" : "SessionId" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "rough", + "rawValue" : "0.1" + }, + { + "associatedValues" : [ + + ], + "name" : "normal", + "rawValue" : "0.01" + }, + { + "associatedValues" : [ + + ], + "name" : "fine", + "rawValue" : "0.001" + } + ], + "emitStyle" : "const", + "name" : "Precision", + "rawType" : "Float", + "swiftCallName" : "Precision" + }, + { + "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" + } + ], + "emitStyle" : "const", + "name" : "Ratio", + "rawType" : "Double", + "swiftCallName" : "Ratio" + } + ], + "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_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" : { + "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_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" : { + "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..991b5c6c --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.swift @@ -0,0 +1,334 @@ +// 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() + 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_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() + 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_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_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 { + #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)))!) + 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 +} \ 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..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,8 +111,12 @@ "namespace" : [ "__Swift", "Foundation" - ] + ], + "swiftCallName" : "UUID" } + ], + "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..7f8324ac 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json @@ -61,8 +61,12 @@ } } ], - "name" : "Greeter" + "name" : "Greeter", + "swiftCallName" : "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" : [ { 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/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 97b86cec..b4642d8a 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,7 +124,454 @@ } } ], - "name" : "Calculator" + "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" : [ @@ -777,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 */