diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index f18788e7..e2f1c3cf 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1743,6 +1743,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { // Current type being collected (when in jsClassBody state) private struct CurrentType { let name: String + let jsName: String? var constructor: ImportedConstructorSkeleton? var methods: [ImportedFunctionSkeleton] var getters: [ImportedGetterSkeleton] @@ -1758,10 +1759,22 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { hasAttribute(attributes, name: "JSFunction") } + static func firstJSFunctionAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { + attributes?.first { attribute in + attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSFunction" + }?.as(AttributeSyntax.self) + } + static func hasJSGetterAttribute(_ attributes: AttributeListSyntax?) -> Bool { hasAttribute(attributes, name: "JSGetter") } + static func firstJSGetterAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { + attributes?.first { attribute in + attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSGetter" + }?.as(AttributeSyntax.self) + } + static func hasJSSetterAttribute(_ attributes: AttributeListSyntax?) -> Bool { hasAttribute(attributes, name: "JSSetter") } @@ -1776,6 +1789,12 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { hasAttribute(attributes, name: "JSClass") } + static func firstJSClassAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { + attributes?.first { attribute in + attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSClass" + }?.as(AttributeSyntax.self) + } + static func hasAttribute(_ attributes: AttributeListSyntax?, name: String) -> Bool { guard let attributes else { return false } return attributes.contains { attribute in @@ -1784,7 +1803,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } } - /// Extracts the jsName argument value from a @JSSetter attribute, if present. + /// Extracts the `jsName` argument value from an attribute, if present. static func extractJSName(from attribute: AttributeSyntax) -> String? { guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else { return nil @@ -1883,22 +1902,15 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { // MARK: - Property Name Resolution - /// Helper for resolving property names from setter function names and jsName attributes + /// Helper for resolving property names from setter function names. private struct PropertyNameResolver { - /// Resolves property name and function base name from a setter function and optional jsName - /// - Returns: (propertyName, functionBaseName) where propertyName preserves case for getter matching, - /// and functionBaseName has lowercase first char for ABI generation + /// Resolves property name and function base name from a setter function. + /// - Returns: (propertyName, functionBaseName) where `propertyName` is derived from the setter name, + /// and `functionBaseName` has lowercase first char for ABI generation. static func resolve( functionName: String, - jsName: String?, normalizeIdentifier: (String) -> String ) -> (propertyName: String, functionBaseName: String)? { - if let jsName = jsName { - let propertyName = normalizeIdentifier(jsName) - let functionBaseName = propertyName.prefix(1).lowercased() + propertyName.dropFirst() - return (propertyName: propertyName, functionBaseName: functionBaseName) - } - let rawFunctionName = functionName.hasPrefix("`") && functionName.hasSuffix("`") && functionName.count > 2 ? String(functionName.dropFirst().dropLast()) @@ -1930,7 +1942,19 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { private func enterJSClass(_ typeName: String) { stateStack.append(.jsClassBody(name: typeName)) - currentType = CurrentType(name: typeName, constructor: nil, methods: [], getters: [], setters: []) + currentType = CurrentType(name: typeName, jsName: nil, constructor: nil, methods: [], getters: [], setters: []) + } + + private func enterJSClass(_ typeName: String, jsName: String?) { + stateStack.append(.jsClassBody(name: typeName)) + currentType = CurrentType( + name: typeName, + jsName: jsName, + constructor: nil, + methods: [], + getters: [], + setters: [] + ) } private func exitJSClass() { @@ -1938,6 +1962,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { importedTypes.append( ImportedTypeSkeleton( name: type.name, + jsName: type.jsName, constructor: type.constructor, methods: type.methods, getters: type.getters, @@ -1952,7 +1977,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { if AttributeChecker.hasJSClassAttribute(node.attributes) { - enterJSClass(node.name.text) + let jsName = AttributeChecker.firstJSClassAttribute(node.attributes).flatMap(AttributeChecker.extractJSName) + enterJSClass(node.name.text, jsName: jsName) } return .visitChildren } @@ -1965,7 +1991,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { if AttributeChecker.hasJSClassAttribute(node.attributes) { - enterJSClass(node.name.text) + let jsName = AttributeChecker.firstJSClassAttribute(node.attributes).flatMap(AttributeChecker.extractJSName) + enterJSClass(node.name.text, jsName: jsName) } return .visitChildren } @@ -2003,8 +2030,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } private func handleTopLevelFunction(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - if AttributeChecker.hasJSFunctionAttribute(node.attributes), - let function = parseFunction(node, enclosingTypeName: nil, isStaticMember: true) + if let jsFunction = AttributeChecker.firstJSFunctionAttribute(node.attributes), + let function = parseFunction(jsFunction, node, enclosingTypeName: nil, isStaticMember: true) { importedFunctions.append(function) return .skipChildren @@ -2028,13 +2055,13 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { isStaticMember: Bool, type: inout CurrentType ) -> Bool { - if AttributeChecker.hasJSFunctionAttribute(node.attributes) { + if let jsFunction = AttributeChecker.firstJSFunctionAttribute(node.attributes) { if isStaticMember { - parseFunction(node, enclosingTypeName: typeName, isStaticMember: true).map { + parseFunction(jsFunction, node, enclosingTypeName: typeName, isStaticMember: true).map { importedFunctions.append($0) } } else { - parseFunction(node, enclosingTypeName: typeName, isStaticMember: false).map { + parseFunction(jsFunction, node, enclosingTypeName: typeName, isStaticMember: false).map { type.methods.append($0) } } @@ -2065,10 +2092,13 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { guard AttributeChecker.hasJSGetterAttribute(node.attributes) else { return .visitChildren } + guard let jsGetter = AttributeChecker.firstJSGetterAttribute(node.attributes) else { + return .skipChildren + } switch state { case .topLevel: - if let getter = parseGetterSkeleton(node) { + if let getter = parseGetterSkeleton(jsGetter, node) { importedGlobalGetters.append(getter) } return .skipChildren @@ -2085,7 +2115,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { "@JSGetter is not supported for static members. Use it only for instance members in @JSClass types." ) ) - } else if let getter = parseGetterSkeleton(node) { + } else if let getter = parseGetterSkeleton(jsGetter, node) { type.getters.append(getter) currentType = type } @@ -2128,8 +2158,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { private func collectStaticMembers(in members: MemberBlockItemListSyntax, typeName: String) { for member in members { if let function = member.decl.as(FunctionDeclSyntax.self) { - if AttributeChecker.hasJSFunctionAttribute(function.attributes), - let parsed = parseFunction(function, enclosingTypeName: typeName, isStaticMember: true) + if let jsFunction = AttributeChecker.firstJSFunctionAttribute(function.attributes), + let parsed = parseFunction(jsFunction, function, enclosingTypeName: typeName, isStaticMember: true) { importedFunctions.append(parsed) } else if AttributeChecker.hasJSSetterAttribute(function.attributes) { @@ -2173,6 +2203,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } private func parseFunction( + _ jsFunction: AttributeSyntax, _ node: FunctionDeclSyntax, enclosingTypeName: String?, isStaticMember: Bool @@ -2183,6 +2214,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } let baseName = SwiftToSkeleton.normalizeIdentifier(node.name.text) + let jsName = AttributeChecker.extractJSName(from: jsFunction) let name: String if isStaticMember, let enclosingTypeName { name = "\(enclosingTypeName)_\(baseName)" @@ -2202,6 +2234,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } return ImportedFunctionSkeleton( name: name, + jsName: jsName, parameters: parameters, returnType: returnType, documentation: nil @@ -2223,7 +2256,10 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { return (identifier, typeAnnotation.type) } - private func parseGetterSkeleton(_ node: VariableDeclSyntax) -> ImportedGetterSkeleton? { + private func parseGetterSkeleton( + _ jsGetter: AttributeSyntax, + _ node: VariableDeclSyntax + ) -> ImportedGetterSkeleton? { guard let (identifier, type) = extractPropertyInfo(node) else { return nil } @@ -2231,8 +2267,10 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { return nil } let propertyName = SwiftToSkeleton.normalizeIdentifier(identifier.identifier.text) + let jsName = AttributeChecker.extractJSName(from: jsGetter) return ImportedGetterSkeleton( name: propertyName, + jsName: jsName, type: propertyType, documentation: nil, functionName: nil @@ -2252,7 +2290,6 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { guard let (propertyName, functionBaseName) = PropertyNameResolver.resolve( functionName: functionName, - jsName: validation.jsName, normalizeIdentifier: SwiftToSkeleton.normalizeIdentifier ) else { @@ -2261,6 +2298,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { return ImportedSetterSkeleton( name: propertyName, + jsName: validation.jsName, type: validation.valueType, documentation: nil, functionName: "\(functionBaseName)_set" diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 929e6e4c..ab4251c2 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1194,25 +1194,29 @@ public struct BridgeJSLink { // Add methods for method in type.methods { + let methodName = method.jsName ?? method.name let methodSignature = - "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));" + "\(renderTSPropertyName(methodName))\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));" printer.write(methodSignature) } // Add properties from getters var propertyNames = Set() for getter in type.getters { - propertyNames.insert(getter.name) - let hasSetter = type.setters.contains { $0.name == getter.name } + let propertyName = getter.jsName ?? getter.name + propertyNames.insert(propertyName) + let hasSetter = type.setters.contains { ($0.jsName ?? $0.name) == propertyName } let propertySignature = hasSetter - ? "\(getter.name): \(resolveTypeScriptType(getter.type));" - : "readonly \(getter.name): \(resolveTypeScriptType(getter.type));" + ? "\(renderTSPropertyName(propertyName)): \(resolveTypeScriptType(getter.type));" + : "readonly \(renderTSPropertyName(propertyName)): \(resolveTypeScriptType(getter.type));" printer.write(propertySignature) } // Add setters that don't have corresponding getters - for setter in type.setters where !propertyNames.contains(setter.name) { - printer.write("\(setter.name): \(resolveTypeScriptType(setter.type));") + for setter in type.setters { + let propertyName = setter.jsName ?? setter.name + guard !propertyNames.contains(propertyName) else { continue } + printer.write("\(renderTSPropertyName(propertyName)): \(resolveTypeScriptType(setter.type));") } printer.unindent() @@ -1387,6 +1391,20 @@ public struct BridgeJSLink { return "(\(parameterSignatures.joined(separator: ", "))): \(returnTypeWithEffect)" } + private func renderTSPropertyName(_ name: String) -> String { + // TypeScript allows quoted property names for keys that aren't valid identifiers. + if name.range(of: #"^[$A-Z_][0-9A-Z_$]*$"#, options: [.regularExpression, .caseInsensitive]) != nil { + return name + } + return "\"\(Self.escapeForJavaScriptStringLiteral(name))\"" + } + + fileprivate static func escapeForJavaScriptStringLiteral(_ string: String) -> String { + string + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + } + /// Helper method to append JSDoc comments for parameters with default values private func appendJSDocIfNeeded(for parameters: [Parameter], to lines: inout [String]) { let jsDocLines = DefaultValueUtils.formatJSDoc(for: parameters) @@ -2110,7 +2128,8 @@ extension BridgeJSLink { } func call(name: String, returnType: BridgeType) throws -> String? { - return try self.call(calleeExpr: "imports.\(name)", returnType: returnType) + let calleeExpr = Self.propertyAccessExpr(objectExpr: "imports", propertyName: name) + return try self.call(calleeExpr: calleeExpr, returnType: returnType) } private func call(calleeExpr: String, returnType: BridgeType) throws -> String? { @@ -2136,21 +2155,26 @@ extension BridgeJSLink { ) } - func callConstructor(name: String) throws -> String? { - let call = "new imports.\(name)(\(parameterForwardings.joined(separator: ", ")))" - let type: BridgeType = .jsObject(name) + func callConstructor(jsName: String, swiftTypeName: String) throws -> String? { + let ctorExpr = Self.propertyAccessExpr(objectExpr: "imports", propertyName: jsName) + let call = "new \(ctorExpr)(\(parameterForwardings.joined(separator: ", ")))" + let type: BridgeType = .jsObject(swiftTypeName) let loweringFragment = try IntrinsicJSFragment.lowerReturn(type: type, context: context) return try lowerReturnValue(returnType: type, returnExpr: call, loweringFragment: loweringFragment) } func callMethod(name: String, returnType: BridgeType) throws -> String? { + let objectExpr = "\(JSGlueVariableScope.reservedSwift).memory.getObject(self)" + let calleeExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name) return try call( - calleeExpr: "\(JSGlueVariableScope.reservedSwift).memory.getObject(self).\(name)", + calleeExpr: calleeExpr, returnType: returnType ) } func callPropertyGetter(name: String, returnType: BridgeType) throws -> String? { + let objectExpr = "\(JSGlueVariableScope.reservedSwift).memory.getObject(self)" + let accessExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name) if context == .exportSwift, returnType.usesSideChannelForOptionalReturn() { guard case .optional(let wrappedType) = returnType else { fatalError("usesSideChannelForOptionalReturn returned true for non-optional type") @@ -2158,7 +2182,7 @@ extension BridgeJSLink { let resultVar = scope.variable("ret") body.write( - "let \(resultVar) = \(JSGlueVariableScope.reservedSwift).memory.getObject(self).\(name);" + "let \(resultVar) = \(accessExpr);" ) let fragment = try IntrinsicJSFragment.protocolPropertyOptionalToSideChannel(wrappedType: wrappedType) @@ -2168,14 +2192,15 @@ extension BridgeJSLink { } return try call( - callExpr: "\(JSGlueVariableScope.reservedSwift).memory.getObject(self).\(name)", + callExpr: accessExpr, returnType: returnType ) } func callPropertySetter(name: String, returnType: BridgeType) { - let call = - "\(JSGlueVariableScope.reservedSwift).memory.getObject(self).\(name) = \(parameterForwardings.joined(separator: ", "))" + let objectExpr = "\(JSGlueVariableScope.reservedSwift).memory.getObject(self)" + let accessExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name) + let call = "\(accessExpr) = \(parameterForwardings.joined(separator: ", "))" body.write("\(call);") } @@ -2185,7 +2210,8 @@ extension BridgeJSLink { } let loweringFragment = try IntrinsicJSFragment.lowerReturn(type: returnType, context: context) - let expr = "imports[\"\(name)\"]" + let escapedName = BridgeJSLink.escapeForJavaScriptStringLiteral(name) + let expr = "imports[\"\(escapedName)\"]" let returnExpr: String? if loweringFragment.parameters.count == 0 { @@ -2214,6 +2240,15 @@ extension BridgeJSLink { assert(loweredValues.count <= 1, "Lowering fragment should produce at most one value") return loweredValues.first } + + private static func propertyAccessExpr(objectExpr: String, propertyName: String) -> String { + if propertyName.range(of: #"^[$A-Z_][0-9A-Z_$]*$"#, options: [.regularExpression, .caseInsensitive]) != nil + { + return "\(objectExpr).\(propertyName)" + } + let escapedName = BridgeJSLink.escapeForJavaScriptStringLiteral(propertyName) + return "\(objectExpr)[\"\(escapedName)\"]" + } } class ImportObjectBuilder { @@ -2928,7 +2963,8 @@ extension BridgeJSLink { for param in function.parameters { try thunkBuilder.liftParameter(param: param) } - let returnExpr = try thunkBuilder.call(name: function.name, returnType: function.returnType) + let jsName = function.jsName ?? function.name + let returnExpr = try thunkBuilder.call(name: jsName, returnType: function.returnType) let funcLines = thunkBuilder.renderFunction( name: function.abiName(context: nil), returnExpr: returnExpr, @@ -2937,7 +2973,7 @@ extension BridgeJSLink { let effects = Effects(isAsync: false, isThrows: false) importObjectBuilder.appendDts( [ - "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: effects));" + "\(renderTSPropertyName(jsName))\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: effects));" ] ) importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines) @@ -2948,14 +2984,15 @@ extension BridgeJSLink { getter: ImportedGetterSkeleton ) throws { let thunkBuilder = ImportedThunkBuilder() - let returnExpr = try thunkBuilder.getImportProperty(name: getter.name, returnType: getter.type) + let jsName = getter.jsName ?? getter.name + let returnExpr = try thunkBuilder.getImportProperty(name: jsName, returnType: getter.type) let abiName = getter.abiName(context: nil) let funcLines = thunkBuilder.renderFunction( name: abiName, returnExpr: returnExpr, returnType: getter.type ) - importObjectBuilder.appendDts(["readonly \(getter.name): \(getter.type.tsType);"]) + importObjectBuilder.appendDts(["readonly \(renderTSPropertyName(jsName)): \(getter.type.tsType);"]) importObjectBuilder.assignToImportObject(name: abiName, function: funcLines) } @@ -2976,7 +3013,10 @@ extension BridgeJSLink { getter: getter, abiName: getterAbiName, emitCall: { thunkBuilder in - return try thunkBuilder.callPropertyGetter(name: getter.name, returnType: getter.type) + return try thunkBuilder.callPropertyGetter( + name: getter.jsName ?? getter.name, + returnType: getter.type + ) } ) importObjectBuilder.assignToImportObject(name: getterAbiName, function: js) @@ -2992,7 +3032,7 @@ extension BridgeJSLink { try thunkBuilder.liftParameter( param: Parameter(label: nil, name: "newValue", type: setter.type) ) - thunkBuilder.callPropertySetter(name: setter.name, returnType: setter.type) + thunkBuilder.callPropertySetter(name: setter.jsName ?? setter.name, returnType: setter.type) return nil } ) @@ -3016,7 +3056,7 @@ extension BridgeJSLink { try thunkBuilder.liftParameter(param: param) } let returnType = BridgeType.jsObject(type.name) - let returnExpr = try thunkBuilder.callConstructor(name: type.name) + let returnExpr = try thunkBuilder.callConstructor(jsName: type.jsName ?? type.name, swiftTypeName: type.name) let abiName = constructor.abiName(context: type) let funcLines = thunkBuilder.renderFunction( name: abiName, @@ -3078,7 +3118,7 @@ extension BridgeJSLink { for param in method.parameters { try thunkBuilder.liftParameter(param: param) } - let returnExpr = try thunkBuilder.callMethod(name: method.name, returnType: method.returnType) + let returnExpr = try thunkBuilder.callMethod(name: method.jsName ?? method.name, returnType: method.returnType) let funcLines = thunkBuilder.renderFunction( name: method.abiName(context: context), returnExpr: returnExpr, diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 28e4b6dc..3baebe6d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -590,12 +590,21 @@ public struct ExportedSkeleton: Codable { public struct ImportedFunctionSkeleton: Codable { public let name: String + /// The JavaScript function/method name to call, if different from `name`. + public let jsName: String? public let parameters: [Parameter] public let returnType: BridgeType public let documentation: String? - public init(name: String, parameters: [Parameter], returnType: BridgeType, documentation: String? = nil) { + public init( + name: String, + jsName: String? = nil, + parameters: [Parameter], + returnType: BridgeType, + documentation: String? = nil + ) { self.name = name + self.jsName = jsName self.parameters = parameters self.returnType = returnType self.documentation = documentation @@ -626,6 +635,8 @@ public struct ImportedConstructorSkeleton: Codable { public struct ImportedGetterSkeleton: Codable { public let name: String + /// The JavaScript property name to read from, if different from `name`. + public let jsName: String? public let type: BridgeType public let documentation: String? /// Name of the getter function if it's a separate function (from @JSGetter) @@ -633,11 +644,13 @@ public struct ImportedGetterSkeleton: Codable { public init( name: String, + jsName: String? = nil, type: BridgeType, documentation: String? = nil, functionName: String? = nil ) { self.name = name + self.jsName = jsName self.type = type self.documentation = documentation self.functionName = functionName @@ -661,6 +674,8 @@ public struct ImportedGetterSkeleton: Codable { public struct ImportedSetterSkeleton: Codable { public let name: String + /// The JavaScript property name to write to, if different from `name`. + public let jsName: String? public let type: BridgeType public let documentation: String? /// Name of the setter function if it's a separate function (from @JSSetter) @@ -668,11 +683,13 @@ public struct ImportedSetterSkeleton: Codable { public init( name: String, + jsName: String? = nil, type: BridgeType, documentation: String? = nil, functionName: String? = nil ) { self.name = name + self.jsName = jsName self.type = type self.documentation = documentation self.functionName = functionName @@ -696,6 +713,8 @@ public struct ImportedSetterSkeleton: Codable { public struct ImportedTypeSkeleton: Codable { public let name: String + /// The JavaScript constructor name to use for `init(...)`, if different from `name`. + public let jsName: String? public let constructor: ImportedConstructorSkeleton? public let methods: [ImportedFunctionSkeleton] public let getters: [ImportedGetterSkeleton] @@ -704,6 +723,7 @@ public struct ImportedTypeSkeleton: Codable { public init( name: String, + jsName: String? = nil, constructor: ImportedConstructorSkeleton? = nil, methods: [ImportedFunctionSkeleton], getters: [ImportedGetterSkeleton] = [], @@ -711,6 +731,7 @@ public struct ImportedTypeSkeleton: Codable { documentation: String? = nil ) { self.name = name + self.jsName = jsName self.constructor = constructor self.methods = methods self.getters = getters diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js index 6a752ab8..1992b49f 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js @@ -60,6 +60,33 @@ export class TypeProcessor { /** @type {Set} */ this.visitedDeclarationKeys = new Set(); + + /** @type {Map} */ + this.swiftTypeNameByJSTypeName = new Map(); + } + + /** + * Convert a TypeScript type name to a valid Swift type identifier. + * @param {string} jsTypeName + * @returns {string} + * @private + */ + swiftTypeName(jsTypeName) { + const cached = this.swiftTypeNameByJSTypeName.get(jsTypeName); + if (cached) return cached; + const swiftName = isValidSwiftDeclName(jsTypeName) ? jsTypeName : makeValidSwiftIdentifier(jsTypeName, { emptyFallback: "_" }); + this.swiftTypeNameByJSTypeName.set(jsTypeName, swiftName); + return swiftName; + } + + /** + * Render a Swift type identifier from a TypeScript type name. + * @param {string} jsTypeName + * @returns {string} + * @private + */ + renderTypeIdentifier(jsTypeName) { + return this.renderIdentifier(this.swiftTypeName(jsTypeName)); } /** @@ -292,7 +319,7 @@ export class TypeProcessor { canBeStringEnum = false; canBeIntEnum = false; } - const swiftEnumName = this.renderIdentifier(enumName); + const swiftEnumName = this.renderTypeIdentifier(enumName); const dedupeNames = (items) => { const seen = new Map(); return items.map(item => { @@ -341,10 +368,10 @@ export class TypeProcessor { */ visitFunctionDeclaration(node) { if (!node.name) return; - const name = node.name.getText(); - if (!isValidSwiftDeclName(name)) { - return; - } + const jsName = node.name.text; + const swiftName = this.swiftTypeName(jsName); + const escapedJSName = jsName.replaceAll("\\", "\\\\").replaceAll("\"", "\\\\\""); + const annotation = jsName !== swiftName ? `@JSFunction(jsName: "${escapedJSName}")` : "@JSFunction"; const signature = this.checker.getSignatureFromDeclaration(node); if (!signature) return; @@ -352,9 +379,9 @@ export class TypeProcessor { const params = this.renderParameters(signature.getParameters(), node); const returnType = this.visitType(signature.getReturnType(), node); const effects = this.renderEffects({ isAsync: false }); - const swiftName = this.renderIdentifier(name); + const swiftFuncName = this.renderIdentifier(swiftName); - this.swiftLines.push(`@JSFunction func ${swiftName}(${params}) ${effects} -> ${returnType}`); + this.swiftLines.push(`${annotation} func ${swiftFuncName}(${params}) ${effects} -> ${returnType}`); this.swiftLines.push(""); } @@ -390,21 +417,28 @@ export class TypeProcessor { /** * @param {ts.PropertyDeclaration | ts.PropertySignature} node - * @returns {{ name: string, type: string, isReadonly: boolean, documentation: string | undefined } | null} + * @returns {{ jsName: string, swiftName: string, type: string, isReadonly: boolean, documentation: string | undefined } | null} */ visitPropertyDecl(node) { if (!node.name) return null; - - const propertyName = node.name.getText(); - if (!isValidSwiftDeclName(propertyName)) { + /** @type {string | null} */ + let jsName = null; + if (ts.isIdentifier(node.name)) { + jsName = node.name.text; + } else if (ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) { + jsName = node.name.text; + } else { + // Computed property names like `[Symbol.iterator]` are not supported yet. return null; } + const swiftName = isValidSwiftDeclName(jsName) ? jsName : makeValidSwiftIdentifier(jsName, { emptyFallback: "_" }); + const type = this.checker.getTypeAtLocation(node) const swiftType = this.visitType(type, node); const isReadonly = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false; const documentation = this.getFullJSDocText(node); - return { name: propertyName, type: swiftType, isReadonly, documentation }; + return { jsName, swiftName, type: swiftType, isReadonly, documentation }; } /** @@ -426,8 +460,15 @@ export class TypeProcessor { visitClassDecl(node) { if (!node.name) return; - const className = this.renderIdentifier(node.name.text); - this.swiftLines.push(`@JSClass struct ${className} {`); + const jsName = node.name.text; + if (this.emittedStructuredTypeNames.has(jsName)) return; + this.emittedStructuredTypeNames.add(jsName); + + const swiftName = this.swiftTypeName(jsName); + const escapedJSName = jsName.replaceAll("\\", "\\\\").replaceAll("\"", "\\\\\""); + const annotation = jsName !== swiftName ? `@JSClass(jsName: "${escapedJSName}")` : "@JSClass"; + const className = this.renderIdentifier(swiftName); + this.swiftLines.push(`${annotation} struct ${className} {`); // Process members in declaration order for (const member of node.members) { @@ -484,8 +525,11 @@ export class TypeProcessor { if (this.emittedStructuredTypeNames.has(name)) return; this.emittedStructuredTypeNames.add(name); - const typeName = this.renderIdentifier(name); - this.swiftLines.push(`@JSClass struct ${typeName} {`); + const swiftName = this.swiftTypeName(name); + const escapedJSName = name.replaceAll("\\", "\\\\").replaceAll("\"", "\\\\\""); + const annotation = name !== swiftName ? `@JSClass(jsName: "${escapedJSName}")` : "@JSClass"; + const typeName = this.renderIdentifier(swiftName); + this.swiftLines.push(`${annotation} struct ${typeName} {`); // Collect all declarations with their positions to preserve order /** @type {Array<{ decl: ts.Node, symbol: ts.Symbol, position: number }>} */ @@ -571,7 +615,7 @@ export class TypeProcessor { if (symbol && (symbol.flags & ts.SymbolFlags.Enum) !== 0) { const typeName = symbol.name; this.seenTypes.set(type, node); - return this.renderIdentifier(typeName); + return this.renderTypeIdentifier(typeName); } if (this.checker.isArrayType(type) || this.checker.isTupleType(type) || type.getCallSignatures().length > 0) { @@ -591,7 +635,7 @@ export class TypeProcessor { return "JSObject"; } this.seenTypes.set(type, node); - return this.renderIdentifier(typeName); + return this.renderTypeIdentifier(typeName); } const swiftType = convert(type); this.processedTypes.set(type, swiftType); @@ -626,17 +670,21 @@ export class TypeProcessor { if (!property) return; const type = property.type; - const name = this.renderIdentifier(property.name); + const swiftName = this.renderIdentifier(property.swiftName); + const needsJSGetterName = property.jsName !== property.swiftName; + const escapedJSName = property.jsName.replaceAll("\\", "\\\\").replaceAll("\"", "\\\\\""); + const getterAnnotation = needsJSGetterName ? `@JSGetter(jsName: "${escapedJSName}")` : "@JSGetter"; // Always render getter - this.swiftLines.push(` @JSGetter var ${name}: ${type}`); + this.swiftLines.push(` ${getterAnnotation} var ${swiftName}: ${type}`); // Render setter if not readonly if (!property.isReadonly) { - const capitalizedName = property.name.charAt(0).toUpperCase() + property.name.slice(1); - const needsJSNameField = property.name.charAt(0) != capitalizedName.charAt(0).toLowerCase(); - const setterName = `set${capitalizedName}`; - const annotation = needsJSNameField ? `@JSSetter(jsName: "${property.name}")` : "@JSSetter"; + const capitalizedSwiftName = property.swiftName.charAt(0).toUpperCase() + property.swiftName.slice(1); + const derivedPropertyName = property.swiftName.charAt(0).toLowerCase() + property.swiftName.slice(1); + const needsJSNameField = property.jsName !== derivedPropertyName; + const setterName = `set${capitalizedSwiftName}`; + const annotation = needsJSNameField ? `@JSSetter(jsName: "${escapedJSName}")` : "@JSSetter"; this.swiftLines.push(` ${annotation} func ${this.renderIdentifier(setterName)}(_ value: ${type}) ${this.renderEffects({ isAsync: false })}`); } } @@ -648,8 +696,21 @@ export class TypeProcessor { */ renderMethod(node) { if (!node.name) return; - const name = node.name.getText(); - if (!isValidSwiftDeclName(name)) return; + /** @type {string | null} */ + let jsName = null; + if (ts.isIdentifier(node.name)) { + jsName = node.name.text; + } else if (ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) { + jsName = node.name.text; + } else { + // Computed property names like `[Symbol.iterator]` are not supported yet. + return; + } + + const swiftName = this.swiftTypeName(jsName); + const needsJSNameField = jsName !== swiftName; + const escapedJSName = jsName.replaceAll("\\", "\\\\").replaceAll("\"", "\\\\\""); + const annotation = needsJSNameField ? `@JSFunction(jsName: "${escapedJSName}")` : "@JSFunction"; const signature = this.checker.getSignatureFromDeclaration(node); if (!signature) return; @@ -657,9 +718,9 @@ export class TypeProcessor { const params = this.renderParameters(signature.getParameters(), node); const returnType = this.visitType(signature.getReturnType(), node); const effects = this.renderEffects({ isAsync: false }); - const swiftName = this.renderIdentifier(name); + const swiftMethodName = this.renderIdentifier(swiftName); - this.swiftLines.push(` @JSFunction func ${swiftName}(${params}) ${effects} -> ${returnType}`); + this.swiftLines.push(` ${annotation} func ${swiftMethodName}(${params}) ${effects} -> ${returnType}`); } /** diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts index d21f3c20..b9d3722b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts @@ -21,3 +21,10 @@ interface WeirdNaming { export function createArrayBuffer(): ArrayBufferLike; export function createWeirdObject(): WeirdNaming; + +export class $Weird { + constructor(); + "method-with-dashes"(): void; +} + +export function createWeirdClass(): $Weird; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts index 2b0474bb..2efd2431 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts @@ -10,15 +10,28 @@ export interface ArrayBufferLike { } export interface WeirdNaming { as(): void; + try(): void; normalProperty: string; + "property-with-dashes": number; + "123invalidStart": boolean; + "property with spaces": string; + "@specialChar": number; + constructor: string; for: string; Any: string; } +export interface _Weird { + "method-with-dashes"(): void; +} export type Exports = { } export type Imports = { createArrayBuffer(): ArrayBufferLike; createWeirdObject(): WeirdNaming; + createWeirdClass(): _Weird; + _Weird: { + new(): _Weird; + } } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js index 60435e56..66cbe793 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js @@ -210,6 +210,15 @@ export async function createInstantiator(options, swift) { return 0 } } + TestModule["bjs_createWeirdClass"] = function bjs_createWeirdClass() { + try { + let ret = imports.createWeirdClass(); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } TestModule["bjs_ArrayBufferLike_byteLength_get"] = function bjs_ArrayBufferLike_byteLength_get(self) { try { let ret = swift.memory.getObject(self).byteLength; @@ -237,6 +246,51 @@ export async function createInstantiator(options, swift) { setException(error); } } + TestModule["bjs_WeirdNaming_property_with_dashes_get"] = function bjs_WeirdNaming_property_with_dashes_get(self) { + try { + let ret = swift.memory.getObject(self)["property-with-dashes"]; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_WeirdNaming__123invalidStart_get"] = function bjs_WeirdNaming__123invalidStart_get(self) { + try { + let ret = swift.memory.getObject(self)["123invalidStart"]; + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_WeirdNaming_property_with_spaces_get"] = function bjs_WeirdNaming_property_with_spaces_get(self) { + try { + let ret = swift.memory.getObject(self)["property with spaces"]; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming__specialChar_get"] = function bjs_WeirdNaming__specialChar_get(self) { + try { + let ret = swift.memory.getObject(self)["@specialChar"]; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_WeirdNaming_constructor_get"] = function bjs_WeirdNaming_constructor_get(self) { + try { + let ret = swift.memory.getObject(self).constructor; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } TestModule["bjs_WeirdNaming_for_get"] = function bjs_WeirdNaming_for_get(self) { try { let ret = swift.memory.getObject(self).for; @@ -264,6 +318,45 @@ export async function createInstantiator(options, swift) { setException(error); } } + TestModule["bjs_WeirdNaming_property_with_dashes_set"] = function bjs_WeirdNaming_property_with_dashes_set(self, newValue) { + try { + swift.memory.getObject(self)["property-with-dashes"] = newValue; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming__123invalidStart_set"] = function bjs_WeirdNaming__123invalidStart_set(self, newValue) { + try { + swift.memory.getObject(self)["123invalidStart"] = newValue !== 0; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_property_with_spaces_set"] = function bjs_WeirdNaming_property_with_spaces_set(self, newValue) { + try { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self)["property with spaces"] = newValueObject; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming__specialChar_set"] = function bjs_WeirdNaming__specialChar_set(self, newValue) { + try { + swift.memory.getObject(self)["@specialChar"] = newValue; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_constructor_set"] = function bjs_WeirdNaming_constructor_set(self, newValue) { + try { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self).constructor = newValueObject; + } catch (error) { + setException(error); + } + } TestModule["bjs_WeirdNaming_for_set"] = function bjs_WeirdNaming_for_set(self, newValue) { try { const newValueObject = swift.memory.getObject(newValue); @@ -289,6 +382,28 @@ export async function createInstantiator(options, swift) { setException(error); } } + TestModule["bjs_WeirdNaming_try"] = function bjs_WeirdNaming_try(self) { + try { + swift.memory.getObject(self).try(); + } catch (error) { + setException(error); + } + } + TestModule["bjs__Weird_init"] = function bjs__Weird_init() { + try { + return swift.memory.retain(new imports.$Weird()); + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs__Weird_method_with_dashes"] = function bjs__Weird_method_with_dashes(self) { + try { + swift.memory.getObject(self)["method-with-dashes"](); + } catch (error) { + setException(error); + } + } }, setInstance: (i) => { instance = i; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift index 6fa9b6d8..43d6a3eb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift @@ -18,9 +18,27 @@ @JSClass struct WeirdNaming { @JSGetter var normalProperty: String @JSSetter func setNormalProperty(_ value: String) throws (JSException) + @JSGetter(jsName: "property-with-dashes") var property_with_dashes: Double + @JSSetter(jsName: "property-with-dashes") func setProperty_with_dashes(_ value: Double) throws (JSException) + @JSGetter(jsName: "123invalidStart") var _123invalidStart: Bool + @JSSetter(jsName: "123invalidStart") func set_123invalidStart(_ value: Bool) throws (JSException) + @JSGetter(jsName: "property with spaces") var property_with_spaces: String + @JSSetter(jsName: "property with spaces") func setProperty_with_spaces(_ value: String) throws (JSException) + @JSGetter(jsName: "@specialChar") var _specialChar: Double + @JSSetter(jsName: "@specialChar") func set_specialChar(_ value: Double) throws (JSException) + @JSGetter var constructor: String + @JSSetter func setConstructor(_ value: String) throws (JSException) @JSGetter var `for`: String @JSSetter func setFor(_ value: String) throws (JSException) @JSGetter var `Any`: String @JSSetter(jsName: "Any") func setAny(_ value: String) throws (JSException) @JSFunction func `as`() throws (JSException) -> Void + @JSFunction func `try`() throws (JSException) -> Void } + +@JSClass(jsName: "$Weird") struct _Weird { + @JSFunction init() throws (JSException) + @JSFunction(jsName: "method-with-dashes") func method_with_dashes() throws (JSException) -> Void +} + +@JSFunction func createWeirdClass() throws (JSException) -> _Weird diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift index ba9e925e..0ef52d4d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift @@ -32,6 +32,23 @@ func _$createWeirdObject() throws(JSException) -> WeirdNaming { return WeirdNaming.bridgeJSLiftReturn(ret) } +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_createWeirdClass") +fileprivate func bjs_createWeirdClass() -> Int32 +#else +fileprivate func bjs_createWeirdClass() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$createWeirdClass() throws(JSException) -> _Weird { + let ret = bjs_createWeirdClass() + if let error = _swift_js_take_exception() { + throw error + } + return _Weird.bridgeJSLiftReturn(ret) +} + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_ArrayBufferLike_byteLength_get") fileprivate func bjs_ArrayBufferLike_byteLength_get(_ self: Int32) -> Float64 @@ -79,6 +96,51 @@ fileprivate func bjs_WeirdNaming_normalProperty_get(_ self: Int32) -> Int32 { } #endif +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_property_with_dashes_get") +fileprivate func bjs_WeirdNaming_property_with_dashes_get(_ self: Int32) -> Float64 +#else +fileprivate func bjs_WeirdNaming_property_with_dashes_get(_ self: Int32) -> Float64 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming__123invalidStart_get") +fileprivate func bjs_WeirdNaming__123invalidStart_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_WeirdNaming__123invalidStart_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_property_with_spaces_get") +fileprivate func bjs_WeirdNaming_property_with_spaces_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_WeirdNaming_property_with_spaces_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming__specialChar_get") +fileprivate func bjs_WeirdNaming__specialChar_get(_ self: Int32) -> Float64 +#else +fileprivate func bjs_WeirdNaming__specialChar_get(_ self: Int32) -> Float64 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_constructor_get") +fileprivate func bjs_WeirdNaming_constructor_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_WeirdNaming_constructor_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_for_get") fileprivate func bjs_WeirdNaming_for_get(_ self: Int32) -> Int32 @@ -106,6 +168,51 @@ fileprivate func bjs_WeirdNaming_normalProperty_set(_ self: Int32, _ newValue: I } #endif +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_property_with_dashes_set") +fileprivate func bjs_WeirdNaming_property_with_dashes_set(_ self: Int32, _ newValue: Float64) -> Void +#else +fileprivate func bjs_WeirdNaming_property_with_dashes_set(_ self: Int32, _ newValue: Float64) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming__123invalidStart_set") +fileprivate func bjs_WeirdNaming__123invalidStart_set(_ self: Int32, _ newValue: Int32) -> Void +#else +fileprivate func bjs_WeirdNaming__123invalidStart_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_property_with_spaces_set") +fileprivate func bjs_WeirdNaming_property_with_spaces_set(_ self: Int32, _ newValue: Int32) -> Void +#else +fileprivate func bjs_WeirdNaming_property_with_spaces_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming__specialChar_set") +fileprivate func bjs_WeirdNaming__specialChar_set(_ self: Int32, _ newValue: Float64) -> Void +#else +fileprivate func bjs_WeirdNaming__specialChar_set(_ self: Int32, _ newValue: Float64) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_constructor_set") +fileprivate func bjs_WeirdNaming_constructor_set(_ self: Int32, _ newValue: Int32) -> Void +#else +fileprivate func bjs_WeirdNaming_constructor_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_for_set") fileprivate func bjs_WeirdNaming_for_set(_ self: Int32, _ newValue: Int32) -> Void @@ -133,6 +240,15 @@ fileprivate func bjs_WeirdNaming_as(_ self: Int32) -> Void { } #endif +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_WeirdNaming_try") +fileprivate func bjs_WeirdNaming_try(_ self: Int32) -> Void +#else +fileprivate func bjs_WeirdNaming_try(_ self: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + func _$WeirdNaming_normalProperty_get(_ self: JSObject) throws(JSException) -> String { let selfValue = self.bridgeJSLowerParameter() let ret = bjs_WeirdNaming_normalProperty_get(selfValue) @@ -142,6 +258,51 @@ func _$WeirdNaming_normalProperty_get(_ self: JSObject) throws(JSException) -> S return String.bridgeJSLiftReturn(ret) } +func _$WeirdNaming_property_with_dashes_get(_ self: JSObject) throws(JSException) -> Double { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_WeirdNaming_property_with_dashes_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return Double.bridgeJSLiftReturn(ret) +} + +func _$WeirdNaming__123invalidStart_get(_ self: JSObject) throws(JSException) -> Bool { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_WeirdNaming__123invalidStart_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return Bool.bridgeJSLiftReturn(ret) +} + +func _$WeirdNaming_property_with_spaces_get(_ self: JSObject) throws(JSException) -> String { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_WeirdNaming_property_with_spaces_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} + +func _$WeirdNaming__specialChar_get(_ self: JSObject) throws(JSException) -> Double { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_WeirdNaming__specialChar_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return Double.bridgeJSLiftReturn(ret) +} + +func _$WeirdNaming_constructor_get(_ self: JSObject) throws(JSException) -> String { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_WeirdNaming_constructor_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} + func _$WeirdNaming_for_get(_ self: JSObject) throws(JSException) -> String { let selfValue = self.bridgeJSLowerParameter() let ret = bjs_WeirdNaming_for_get(selfValue) @@ -169,6 +330,51 @@ func _$WeirdNaming_normalProperty_set(_ self: JSObject, _ newValue: String) thro } } +func _$WeirdNaming_property_with_dashes_set(_ self: JSObject, _ newValue: Double) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValueValue = newValue.bridgeJSLowerParameter() + bjs_WeirdNaming_property_with_dashes_set(selfValue, newValueValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +func _$WeirdNaming__123invalidStart_set(_ self: JSObject, _ newValue: Bool) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValueValue = newValue.bridgeJSLowerParameter() + bjs_WeirdNaming__123invalidStart_set(selfValue, newValueValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +func _$WeirdNaming_property_with_spaces_set(_ self: JSObject, _ newValue: String) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValueValue = newValue.bridgeJSLowerParameter() + bjs_WeirdNaming_property_with_spaces_set(selfValue, newValueValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +func _$WeirdNaming__specialChar_set(_ self: JSObject, _ newValue: Double) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValueValue = newValue.bridgeJSLowerParameter() + bjs_WeirdNaming__specialChar_set(selfValue, newValueValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +func _$WeirdNaming_constructor_set(_ self: JSObject, _ newValue: String) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValueValue = newValue.bridgeJSLowerParameter() + bjs_WeirdNaming_constructor_set(selfValue, newValueValue) + if let error = _swift_js_take_exception() { + throw error + } +} + func _$WeirdNaming_for_set(_ self: JSObject, _ newValue: String) throws(JSException) -> Void { let selfValue = self.bridgeJSLowerParameter() let newValueValue = newValue.bridgeJSLowerParameter() @@ -193,4 +399,46 @@ func _$WeirdNaming_as(_ self: JSObject) throws(JSException) -> Void { if let error = _swift_js_take_exception() { throw error } +} + +func _$WeirdNaming_try(_ self: JSObject) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + bjs_WeirdNaming_try(selfValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs__Weird_init") +fileprivate func bjs__Weird_init() -> Int32 +#else +fileprivate func bjs__Weird_init() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs__Weird_method_with_dashes") +fileprivate func bjs__Weird_method_with_dashes(_ self: Int32) -> Void +#else +fileprivate func bjs__Weird_method_with_dashes(_ self: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$_Weird_init() throws(JSException) -> JSObject { + let ret = bjs__Weird_init() + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +func _$_Weird_method_with_dashes(_ self: JSObject) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + bjs__Weird_method_with_dashes(selfValue) + if let error = _swift_js_take_exception() { + throw error + } } \ No newline at end of file diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index 8cc3dbc6..61797a58 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -112,6 +112,9 @@ public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const) = Bui /// /// This macro is used by BridgeJS-generated Swift declarations. /// +/// - Parameter jsName: An optional string that specifies the name of the JavaScript property to read from. +/// If not provided, the Swift property name is used. +/// /// Example: /// /// ```swift @@ -125,7 +128,7 @@ public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const) = Bui /// ``` @attached(accessor) @_spi(Experimental) -public macro JSGetter() = +public macro JSGetter(jsName: String? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSGetterMacro") /// A macro that generates a Swift function body that writes a value to JavaScript. @@ -159,9 +162,12 @@ public macro JSSetter(jsName: String? = nil) = /// @JSFunction func greet() throws (JSException) -> String /// @JSFunction init(_ name: String) throws (JSException) /// ``` +/// +/// - Parameter jsName: An optional string that specifies the name of the JavaScript function or method to call. +/// If not provided, the Swift function name is used. @attached(body) @_spi(Experimental) -public macro JSFunction() = +public macro JSFunction(jsName: String? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSFunctionMacro") /// A macro that adds bridging members for a Swift type that represents a JavaScript class. @@ -184,5 +190,5 @@ public macro JSFunction() = @attached(member, names: arbitrary) @attached(extension, conformances: _JSBridgedClass) @_spi(Experimental) -public macro JSClass() = +public macro JSClass(jsName: String? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSClassMacro") diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift index 65a46a3f..ffcfca8d 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift @@ -40,3 +40,10 @@ extension FeatureFlag: _BridgedSwiftEnumNoPayload {} } @JSFunction func runAsyncWorks() throws (JSException) -> JSPromise + +@JSFunction(jsName: "$jsWeirdFunction") func _jsWeirdFunction() throws (JSException) -> Double + +@JSClass(jsName: "$WeirdClass") struct _WeirdClass { + @JSFunction init() throws (JSException) + @JSFunction(jsName: "method-with-dashes") func method_with_dashes() throws (JSException) -> String +} diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 5195bee1..ed2e5240 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -6449,6 +6449,23 @@ func _$runAsyncWorks() throws(JSException) -> JSPromise { return JSPromise.bridgeJSLiftReturn(ret) } +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs__jsWeirdFunction") +fileprivate func bjs__jsWeirdFunction() -> Float64 +#else +fileprivate func bjs__jsWeirdFunction() -> Float64 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$_jsWeirdFunction() throws(JSException) -> Double { + let ret = bjs__jsWeirdFunction() + if let error = _swift_js_take_exception() { + throw error + } + return Double.bridgeJSLiftReturn(ret) +} + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init") fileprivate func bjs_JsGreeter_init(_ name: Int32, _ prefix: Int32) -> Int32 @@ -6558,6 +6575,41 @@ func _$JsGreeter_changeName(_ self: JSObject, _ name: String) throws(JSException } } +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs__WeirdClass_init") +fileprivate func bjs__WeirdClass_init() -> Int32 +#else +fileprivate func bjs__WeirdClass_init() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs__WeirdClass_method_with_dashes") +fileprivate func bjs__WeirdClass_method_with_dashes(_ self: Int32) -> Int32 +#else +fileprivate func bjs__WeirdClass_method_with_dashes(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$_WeirdClass_init() throws(JSException) -> JSObject { + let ret = bjs__WeirdClass_init() + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +func _$_WeirdClass_method_with_dashes(_ self: JSObject) throws(JSException) -> String { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs__WeirdClass_method_with_dashes(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsApplyInt") fileprivate func bjs_jsApplyInt(_ value: Int32, _ transform: UnsafeMutableRawPointer) -> Int32 diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 66a87b06..fb2770ae 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -9155,6 +9155,18 @@ "_0" : "JSPromise" } } + }, + { + "jsName" : "$jsWeirdFunction", + "name" : "_jsWeirdFunction", + "parameters" : [ + + ], + "returnType" : { + "double" : { + + } + } } ], "types" : [ @@ -9240,6 +9252,35 @@ } } ] + }, + { + "constructor" : { + "parameters" : [ + + ] + }, + "getters" : [ + + ], + "jsName" : "$WeirdClass", + "methods" : [ + { + "jsName" : "method-with-dashes", + "name" : "method_with_dashes", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "_WeirdClass", + "setters" : [ + + ] } ] }, diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift index 5ceca3bf..ea9f8c68 100644 --- a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -120,4 +120,11 @@ class ImportAPITests: XCTestCase { XCTAssertEqual(ret, 5) XCTAssertEqual(total, 10) } + + func testJSNameFunctionAndClass() throws { + XCTAssertEqual(try _jsWeirdFunction(), 42) + + let obj = try _WeirdClass() + XCTAssertEqual(try obj.method_with_dashes(), "ok") + } } diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts index 87de440d..983d6052 100644 --- a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts +++ b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts @@ -23,3 +23,11 @@ export class JsGreeter { } export function runAsyncWorks(): Promise; + +// jsName tests +export function $jsWeirdFunction(): number; + +export class $WeirdClass { + constructor(); + "method-with-dashes"(): string; +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 66283b72..47f30a92 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -50,6 +50,9 @@ export async function setupOptions(options, context) { "jsRoundTripFeatureFlag": (flag) => { return flag; }, + "$jsWeirdFunction": () => { + return 42; + }, JsGreeter: class { /** * @param {string} name @@ -67,6 +70,13 @@ export async function setupOptions(options, context) { this.name = name; } }, + $WeirdClass: class { + constructor() { + } + ["method-with-dashes"]() { + return "ok"; + } + }, Foo: ImportedFoo, runAsyncWorks: async () => { const exports = importsContext.getExports();