diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index 2fc6f8db3..63ce68fe5 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1068,6 +1068,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { struct NamespaceResolution { let namespace: [String]? + var jsNamespace: [String]? = nil let isValid: Bool } @@ -1079,7 +1080,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { declarationType: String ) -> NamespaceResolution { let attributeNamespace = extractNamespace(from: jsAttribute) - let computedNamespace = computeNamespace(for: node) + let computedNamespace = computeNamespace(for: node, includeParentTypes: true) if computedNamespace != nil && attributeNamespace != nil { diagnose( @@ -1091,7 +1092,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return NamespaceResolution(namespace: nil, isValid: false) } - return NamespaceResolution(namespace: computedNamespace ?? attributeNamespace, isValid: true) + let jsNamespace = computeNamespace(for: node, includeParentTypes: true, useJSNames: true) + return NamespaceResolution( + namespace: computedNamespace ?? attributeNamespace, + jsNamespace: jsNamespace != computedNamespace ? jsNamespace : nil, + isValid: true + ) } enum State { @@ -1288,10 +1294,10 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { switch type { case .swiftStruct(let name), .nullable(.swiftStruct(let name), _): isStructType = true - expectedTypeName = name.split(separator: ".").last.map(String.init) + expectedTypeName = name case .swiftHeapObject(let name), .nullable(.swiftHeapObject(let name), _): isStructType = false - expectedTypeName = name.split(separator: ".").last.map(String.init) + expectedTypeName = name default: diagnose( node: funcCall, @@ -1301,7 +1307,9 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return nil } - guard let expectedTypeName = expectedTypeName, typeName == expectedTypeName else { + guard let expectedTypeName = expectedTypeName, + typeName == expectedTypeName.split(separator: ".").last.map(String.init) + else { diagnose( node: funcCall, message: "Constructor type name '\(typeName)' doesn't match parameter type", @@ -1335,7 +1343,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return .structLiteral(typeName, fields) } else { if funcCall.arguments.isEmpty { - return .object(typeName) + return .object(expectedTypeName) } var constructorArgs: [DefaultValue] = [] @@ -1350,7 +1358,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { } constructorArgs.append(argValue) } - return .objectWithArguments(typeName, constructorArgs) + return .objectWithArguments(expectedTypeName, constructorArgs) } } @@ -2017,7 +2025,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return .skipChildren } - diagnoseUnsupportedJSName(from: jsAttribute) + let jsName = extractValidatedJSName(from: jsAttribute) if let aliasTarget = parent.extractAliasTarget(from: jsAttribute) { recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget) @@ -2028,10 +2036,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { guard namespaceResult.isValid else { return .skipChildren } - let effectiveNamespace = effectiveNamespace( - resolvedNamespace: namespaceResult.namespace, - parentTypeNamespace: computeParentTypeNamespace(for: node) - ) + let effectiveNamespace = namespaceResult.namespace let swiftCallName = parent.computeSwiftCallName(for: node, itemName: name) let explicitAccessControl = computeExplicitAtLeastInternalAccessControl( for: node, @@ -2041,12 +2046,14 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { let isFinal = node.modifiers.contains { $0.name.tokenKind == .keyword(.final) } ? true : nil let exportedClass = ExportedClass( name: name, + jsName: jsName, swiftCallName: swiftCallName, explicitAccessControl: explicitAccessControl, constructor: nil, methods: [], properties: [], namespace: effectiveNamespace, + jsNamespace: namespaceResult.jsNamespace, identityMode: classIdentityMode, documentation: extractDocumentation(from: node), isFinal: isFinal @@ -2126,6 +2133,14 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { aliasTarget: TypeSyntax ) { let swiftCallName = parent.computeSwiftCallName(for: node, itemName: node.name.text) + if extractJSName(from: jsAttribute) != nil { + diagnose( + node: jsAttribute, + message: "A separate name for JavaScript is not supported on `@JS(as:)` types", + hint: "Remove the name argument; an alias adopts its target's representation" + ) + return + } if extractNamespace(from: jsAttribute) != nil { errors.append( DiagnosticError( @@ -2167,7 +2182,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return .skipChildren } - diagnoseUnsupportedJSName(from: jsAttribute) + let jsName = extractValidatedJSName(from: jsAttribute) if let aliasTarget = parent.extractAliasTarget(from: jsAttribute) { recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget) @@ -2185,10 +2200,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { guard namespaceResult.isValid else { return .skipChildren } - let effectiveNamespace = effectiveNamespace( - resolvedNamespace: namespaceResult.namespace, - parentTypeNamespace: computeParentTypeNamespace(for: node) - ) + let effectiveNamespace = namespaceResult.namespace let emitStyle = extractEnumStyle(from: jsAttribute) ?? .const let swiftCallName = parent.computeSwiftCallName(for: node, itemName: name) let explicitAccessControl = computeExplicitAtLeastInternalAccessControl( @@ -2196,22 +2208,16 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { message: "Enum visibility must be at least internal" ) - let tsFullPath: String - if let namespace = effectiveNamespace, !namespace.isEmpty { - tsFullPath = namespace.joined(separator: ".") + "." + name - } else { - tsFullPath = name - } - // Create enum directly in dictionary let exportedEnum = ExportedEnum( name: name, + jsName: jsName, swiftCallName: swiftCallName, - tsFullPath: tsFullPath, explicitAccessControl: explicitAccessControl, cases: [], // Will be populated in visit(EnumCaseDeclSyntax) rawType: SwiftEnumRawType(rawType), namespace: effectiveNamespace, + jsNamespace: namespaceResult.jsNamespace, emitStyle: emitStyle, staticMethods: [], staticProperties: [], @@ -2294,7 +2300,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return .skipChildren } - diagnoseUnsupportedJSName(from: jsAttribute) + let jsName = extractValidatedJSName(from: jsAttribute) + + if let aliasTarget = parent.extractAliasTarget(from: jsAttribute), jsName != nil { + recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget) + return .skipChildren + } let name = node.name.text @@ -2302,10 +2313,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { guard namespaceResult.isValid else { return .skipChildren } - let effectiveNamespace = effectiveNamespace( - resolvedNamespace: namespaceResult.namespace, - parentTypeNamespace: computeParentTypeNamespace(for: node) - ) + let effectiveNamespace = namespaceResult.namespace _ = computeExplicitAtLeastInternalAccessControl( for: node, message: "Protocol visibility must be at least internal" @@ -2315,9 +2323,11 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { exportedProtocolByName[protocolUniqueKey] = ExportedProtocol( name: name, + jsName: jsName, methods: [], properties: [], namespace: effectiveNamespace, + jsNamespace: namespaceResult.jsNamespace, documentation: extractDocumentation(from: node) ) @@ -2340,9 +2350,11 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { let exportedProtocol = ExportedProtocol( name: name, + jsName: jsName, methods: methods, properties: exportedProtocolByName[protocolUniqueKey]?.properties ?? [], namespace: effectiveNamespace, + jsNamespace: namespaceResult.jsNamespace, documentation: extractDocumentation(from: node) ) @@ -2359,7 +2371,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return .skipChildren } - diagnoseUnsupportedJSName(from: jsAttribute) + let jsName = extractValidatedJSName(from: jsAttribute) if let aliasTarget = parent.extractAliasTarget(from: jsAttribute) { recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget) @@ -2372,10 +2384,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { guard namespaceResult.isValid else { return .skipChildren } - let effectiveNamespace = effectiveNamespace( - resolvedNamespace: namespaceResult.namespace, - parentTypeNamespace: computeParentTypeNamespace(for: node) - ) + let effectiveNamespace = namespaceResult.namespace let swiftCallName = parent.computeSwiftCallName(for: node, itemName: name) let explicitAccessControl = computeExplicitAtLeastInternalAccessControl( for: node, @@ -2437,11 +2446,13 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { let structUniqueKey = makeKey(name: name, namespace: effectiveNamespace) let exportedStruct = ExportedStruct( name: name, + jsName: jsName, swiftCallName: swiftCallName, explicitAccessControl: explicitAccessControl, properties: properties, methods: [], namespace: effectiveNamespace, + jsNamespace: namespaceResult.jsNamespace, documentation: extractDocumentation(from: node) ) @@ -2595,9 +2606,11 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { currentProtocol = ExportedProtocol( name: currentProtocol.name, + jsName: currentProtocol.jsName, methods: currentProtocol.methods, properties: properties, namespace: currentProtocol.namespace, + jsNamespace: currentProtocol.jsNamespace, documentation: currentProtocol.documentation ) exportedProtocolByName[protocolKey] = currentProtocol @@ -2684,61 +2697,44 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { 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]? { + /// Computes inherited namespaces using Swift names for ABI generation or public names for exports. + private func computeNamespace( + for node: some SyntaxProtocol, + includeParentTypes: Bool = false, + useJSNames: Bool = false + ) -> [String]? { var namespace: [String] = [] for declaration in parent.enclosingDeclarations(of: node) { + let name: String + let jsAttribute: AttributeSyntax if let enumDecl = declaration.as(EnumDeclSyntax.self), - enumDecl.attributes.hasJSAttribute() + let attribute = enumDecl.attributes.firstJSAttribute, + !enumDecl.memberBlock.members.contains(where: { $0.decl.is(EnumCaseDeclSyntax.self) }) { - 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 - } - } - } - } - - return namespace.isEmpty ? nil : namespace - } - - private func computeParentTypeNamespace(for node: some SyntaxProtocol) -> [String]? { - var path: [String] = [] - - for declaration in parent.enclosingDeclarations(of: node) { - if let structDecl = declaration.as(StructDeclSyntax.self), - structDecl.attributes.hasJSAttribute() + name = enumDecl.name.text + jsAttribute = attribute + } else if includeParentTypes, let structDecl = declaration.as(StructDeclSyntax.self), + let attribute = structDecl.attributes.firstJSAttribute { - path.insert(structDecl.name.text, at: 0) - } else if let classDecl = declaration.as(ClassDeclSyntax.self), - classDecl.attributes.hasJSAttribute() + name = structDecl.name.text + jsAttribute = attribute + } else if includeParentTypes, let classDecl = declaration.as(ClassDeclSyntax.self), + let attribute = classDecl.attributes.firstJSAttribute { - path.insert(classDecl.name.text, at: 0) + name = classDecl.name.text + jsAttribute = attribute + } else { + continue + } + namespace.insert(useJSNames ? extractJSName(from: jsAttribute) ?? name : name, at: 0) + if let explicitNamespace = extractNamespace(from: jsAttribute) { + namespace = explicitNamespace + namespace + break } } - return path.isEmpty ? nil : path - } - - private func effectiveNamespace( - resolvedNamespace: [String]?, - parentTypeNamespace: [String]? - ) -> [String]? { - let combined = (parentTypeNamespace ?? []) + (resolvedNamespace ?? []) - return combined.isEmpty ? nil : combined + return namespace.isEmpty ? nil : namespace } /// Requires the node to have at least internal access control. diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index cf6eab60c..5d41869f8 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -226,7 +226,7 @@ public struct BridgeJSLink { } } - for structDefinition in skeleton.structs where structDefinition.namespace == nil { + for structDefinition in skeleton.structs where structDefinition.resolvedJSNamespace == nil { data.topLevelDtsTypeLines.append( contentsOf: renderExportedStructInterface(structDefinition) ) @@ -405,7 +405,6 @@ public struct BridgeJSLink { scope: JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry), printer: printer, hasDirectAccessToSwiftClass: false, - classNamespaces: intrinsicRegistry.classNamespaces ) } @@ -1022,7 +1021,7 @@ public struct BridgeJSLink { for skeleton in exportedSkeletons { for proto in skeleton.protocols { printer.write(lines: renderJSDoc(documentation: proto.documentation, parameters: [])) - printer.write("export interface \(proto.name) {") + printer.write("export interface \(proto.resolvedJSName) {") printer.indent { for method in proto.methods { printer.write( @@ -1057,7 +1056,7 @@ public struct BridgeJSLink { // Use fully-qualified path for namespaced enums let fullEnumValuesPath: String - if let namespace = enumDefinition.namespace, !namespace.isEmpty { + if let namespace = enumDefinition.resolvedJSNamespace, !namespace.isEmpty { fullEnumValuesPath = namespace.joined(separator: ".") + "." + enumValuesName } else { fullEnumValuesPath = enumValuesName @@ -1134,7 +1133,7 @@ public struct BridgeJSLink { renderPropertyEntry: { property in let readonly = property.isReadonly ? "readonly " : "" return self.renderJSDoc(documentation: property.documentation, parameters: []) - + ["\(readonly)\(property.resolvedJSName): \(property.type.tsType);"] + + ["\(readonly)\(property.resolvedJSName): \(self.resolveTypeScriptType(property.type));"] } ) printer.write("export type Exports = {") @@ -1226,7 +1225,6 @@ public struct BridgeJSLink { scope: structScope, printer: structPrinter, hasDirectAccessToSwiftClass: false, - classNamespaces: intrinsicRegistry.classNamespaces ) ) bodyPrinter.write(lines: structPrinter.lines) @@ -1248,7 +1246,6 @@ public struct BridgeJSLink { scope: enumScope, printer: enumPrinter, hasDirectAccessToSwiftClass: false, - classNamespaces: intrinsicRegistry.classNamespaces ) ) bodyPrinter.write(lines: enumPrinter.lines) @@ -1348,12 +1345,14 @@ public struct BridgeJSLink { public func link(sharedMemory: Bool = false) throws -> (outputJs: String, outputDts: String) { intrinsicRegistry.reset() importedModuleRegistry.configure(skeletons: skeletons) - intrinsicRegistry.classNamespaces = skeletons.reduce(into: [:]) { result, unified in - guard let skeleton = unified.exported else { return } - for klass in skeleton.classes { - if let namespace = klass.namespace { - result[klass.name] = namespace - } + intrinsicRegistry.classPaths = skeletons.reduce(into: [:]) { result, unified in + for klass in unified.exported?.classes ?? [] { + result[klass.swiftCallName] = klass.tsPathComponents + } + } + intrinsicRegistry.renamedEnumNames = skeletons.reduce(into: [:]) { result, unified in + for enumDef in unified.exported?.enums ?? [] where enumDef.resolvedJSName != enumDef.name { + result[enumDef.swiftCallName] = (value: enumDef.valuesName, type: enumDef.resolvedJSName) } } intrinsicRegistry.typeOwnerModules = collectTypeOwnerModules() @@ -1454,9 +1453,10 @@ public struct BridgeJSLink { for klass in classes.sorted(by: { $0.name < $1.name }) { let wrapperFunctionName = "bjs_\(klass.abiName)_wrap" - let namespacePath = (klass.namespace ?? []).map { ".\($0)" }.joined() + let namespacePath = (klass.resolvedJSNamespace ?? []).map { ".\($0)" }.joined() let exportsPath = - namespacePath.isEmpty ? "_exports['\(klass.name)']" : "_exports\(namespacePath).\(klass.name)" + namespacePath.isEmpty + ? "_exports['\(klass.resolvedJSName)']" : "_exports\(namespacePath).\(klass.resolvedJSName)" wrapperLines.append("importObject[\"\(moduleName)\"][\"\(wrapperFunctionName)\"] = function(pointer) {") wrapperLines.append(" const obj = \(exportsPath).__construct(pointer);") wrapperLines.append(" return \(JSGlueVariableScope.reservedSwift).memory.retain(obj);") @@ -1570,7 +1570,6 @@ public struct BridgeJSLink { scope: scope, printer: body, hasDirectAccessToSwiftClass: hasDirectAccessToSwiftClass, - classNamespaces: intrinsicRegistry.classNamespaces ) } @@ -1665,7 +1664,10 @@ public struct BridgeJSLink { ) -> [String] { let printer = CodeFragmentPrinter() - let parameterList = DefaultValueUtils.formatParameterList(parameters) + let parameterList = DefaultValueUtils.formatParameterList( + parameters, + resolveTypeName: context.defaultValueTypeName + ) printer.write( "\(declarationPrefixKeyword.map { "\($0) "} ?? "")\(name)(\(parameterList)) {" @@ -1679,15 +1681,12 @@ public struct BridgeJSLink { } } - /// Returns TypeScript type string for a BridgeType, using full paths for enums - /// If the type is an enum, looks up the ExportedEnum and uses its tsFullPath - /// Otherwise, uses the default tsType property + /// Resolves public TypeScript names without changing the skeleton's Swift type identities. private func resolveTypeScriptType(_ type: BridgeType) -> String { return Self.resolveTypeScriptType(type, exportedSkeletons: skeletons.compactMap(\.exported)) } - /// Static helper for resolving TypeScript types with full enum paths - /// Can be used by both BridgeJSLink and NamespaceBuilder + /// Recursively resolves nominal types in signatures shared by the linker and namespace builder. fileprivate static func resolveTypeScriptType(_ type: BridgeType, exportedSkeletons: [ExportedSkeleton]) -> String { switch type { case .caseEnum(let name), .rawValueEnum(let name, _), @@ -1723,11 +1722,18 @@ public struct BridgeJSLink { for skeleton in exportedSkeletons { for klass in skeleton.classes { if klass.swiftCallName == name { - return klass.name + return klass.resolvedJSName } } } return type.tsType + case .swiftProtocol(let name): + for skeleton in exportedSkeletons { + if let proto = skeleton.protocols.first(where: { $0.name == name }) { + return proto.resolvedJSName + } + } + return type.tsType case .closure(let signature, _): return signature.renderTSFunctionType { resolveTypeScriptType($0, exportedSkeletons: exportedSkeletons) } case .alias(_, let underlying): @@ -1790,7 +1796,7 @@ public struct BridgeJSLink { for line in renderJSDoc(documentation: structDefinition.documentation, parameters: []) { dtsTypePrinter.write(line) } - dtsTypePrinter.write("export interface \(structDefinition.name) {") + dtsTypePrinter.write("export interface \(structDefinition.resolvedJSName) {") dtsTypePrinter.indent { for property in structDefinition.properties where !property.isStatic { let tsType = resolveTypeScriptType(property.type) @@ -1865,7 +1871,10 @@ public struct BridgeJSLink { ) let constructorPrinter = CodeFragmentPrinter() - let paramList = DefaultValueUtils.formatParameterList(constructor.parameters) + let paramList = DefaultValueUtils.formatParameterList( + constructor.parameters, + resolveTypeName: thunkBuilder.context.defaultValueTypeName + ) constructorPrinter.write("init: function(\(paramList)) {") constructorPrinter.indent { thunkBuilder.renderFunctionBody(into: constructorPrinter, returnExpr: returnExpr) @@ -1924,7 +1933,7 @@ public struct BridgeJSLink { break } - if enumDefinition.namespace == nil { + if enumDefinition.resolvedJSNamespace == nil { dtsTypeLines.append(contentsOf: generateDeclarations(enumDefinition: enumDefinition)) } @@ -1933,7 +1942,7 @@ public struct BridgeJSLink { if enumDefinition.enumType != .namespace && enumDefinition.emitStyle != .tsEnum - && enumDefinition.namespace == nil + && enumDefinition.resolvedJSNamespace == nil { var enumMethodLines: [String] = [] for function in enumDefinition.staticMethods { @@ -1953,7 +1962,7 @@ public struct BridgeJSLink { let exportsPrinter = CodeFragmentPrinter() if !enumMethodLines.isEmpty || !enumPropertyLines.isEmpty { - exportsPrinter.write("\(enumDefinition.name): {") + exportsPrinter.write("\(enumDefinition.resolvedJSName): {") exportsPrinter.indent { exportsPrinter.write("...\(enumValuesName),") var allLines = enumMethodLines + enumPropertyLines @@ -1964,11 +1973,11 @@ public struct BridgeJSLink { } exportsPrinter.write("},") } else { - exportsPrinter.write("\(enumDefinition.name): \(enumValuesName),") + exportsPrinter.write("\(enumDefinition.resolvedJSName): \(enumValuesName),") } jsExportEntryLines = exportsPrinter.lines - dtsExportEntryLines = ["\(enumDefinition.name): \(enumDefinition.objectTypeName)"] + dtsExportEntryLines = ["\(enumDefinition.resolvedJSName): \(enumDefinition.objectTypeName)"] } return (jsTopLevelLines, jsExportEntryLines, dtsTypeLines, dtsExportEntryLines) @@ -1986,7 +1995,7 @@ public struct BridgeJSLink { case .tsEnum: switch enumDefinition.enumType { case .simple, .rawValue: - printer.write("export enum \(enumDefinition.name) {") + printer.write("export enum \(enumDefinition.resolvedJSName) {") printer.indent { for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter @@ -2020,7 +2029,7 @@ public struct BridgeJSLink { } printer.write("};") printer.write( - "export type \(enumDefinition.name)Tag = typeof \(enumValuesName)[keyof typeof \(enumValuesName)];" + "export type \(enumDefinition.resolvedJSName)Tag = typeof \(enumValuesName)[keyof typeof \(enumValuesName)];" ) printer.nextLine() case .associatedValue: @@ -2057,7 +2066,8 @@ public struct BridgeJSLink { } let unionTypeName = - enumDefinition.emitStyle == .tsEnum ? enumDefinition.name : "\(enumDefinition.name)Tag" + enumDefinition.emitStyle == .tsEnum + ? enumDefinition.resolvedJSName : "\(enumDefinition.resolvedJSName)Tag" printer.write("export type \(unionTypeName) =") printer.write(" " + unionParts.joined(separator: " | ")) printer.nextLine() @@ -2170,7 +2180,11 @@ extension BridgeJSLink { let returnExpr = try thunkBuilder.call(abiName: function.abiName, returnType: function.returnType) let printer = CodeFragmentPrinter() - printer.write("\(function.resolvedJSName)(\(DefaultValueUtils.formatParameterList(function.parameters))) {") + let parameterList = DefaultValueUtils.formatParameterList( + function.parameters, + resolveTypeName: thunkBuilder.context.defaultValueTypeName + ) + printer.write("\(function.resolvedJSName)(\(parameterList)) {") printer.indent { thunkBuilder.renderFunctionBody(into: printer, returnExpr: returnExpr) } @@ -2226,8 +2240,12 @@ extension BridgeJSLink { let returnExpr = try thunkBuilder.call(abiName: method.abiName, returnType: method.returnType) let methodPrinter = CodeFragmentPrinter() + let parameterList = DefaultValueUtils.formatParameterList( + method.parameters, + resolveTypeName: thunkBuilder.context.defaultValueTypeName + ) methodPrinter.write( - "\(method.resolvedJSName): function(\(DefaultValueUtils.formatParameterList(method.parameters))) {" + "\(method.resolvedJSName): function(\(parameterList)) {" ) methodPrinter.indent { thunkBuilder.renderFunctionBody(into: methodPrinter, returnExpr: returnExpr) @@ -2296,8 +2314,8 @@ extension BridgeJSLink { for line in renderJSDoc(documentation: klass.documentation, parameters: []) { dtsTypePrinter.write(line) } - dtsTypePrinter.write("export interface \(klass.name) extends SwiftHeapObject {") - jsPrinter.write("class \(klass.name) extends SwiftHeapObject {") + dtsTypePrinter.write("export interface \(klass.resolvedJSName) extends SwiftHeapObject {") + jsPrinter.write("class \(klass.resolvedJSName) extends SwiftHeapObject {") // Per-class identity mode: determine at codegen time whether this class uses identity caching let useIdentity = shouldUseIdentityCache(for: klass) @@ -2310,11 +2328,11 @@ extension BridgeJSLink { jsPrinter.indent { if useIdentity { jsPrinter.write( - "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.name).prototype, \(klass.name).__identityCache);" + "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.resolvedJSName).prototype, \(klass.resolvedJSName).__identityCache);" ) } else { jsPrinter.write( - "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.name).prototype, null);" + "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.resolvedJSName).prototype, null);" ) } } @@ -2331,12 +2349,15 @@ extension BridgeJSLink { try thunkBuilder.lowerParameter(param: param) } - let constructorParamList = DefaultValueUtils.formatParameterList(constructor.parameters) + let constructorParamList = DefaultValueUtils.formatParameterList( + constructor.parameters, + resolveTypeName: thunkBuilder.context.defaultValueTypeName + ) jsPrinter.indent { jsPrinter.write("constructor(\(constructorParamList)) {") let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) - let constructCall = "\(klass.name).__construct(\(returnExpr))" + let constructCall = "\(klass.resolvedJSName).__construct(\(returnExpr))" jsPrinter.indent { thunkBuilder.renderFunctionBody( into: jsPrinter, @@ -2430,7 +2451,7 @@ extension BridgeJSLink { lines: renderJSDoc(documentation: constructor.documentation, parameters: constructor.parameters) ) printer.write( - "new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name), effects: constructor.effects));" + "new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.swiftCallName), effects: constructor.effects));" ) } for method in klass.methods where method.effects.isStatic { @@ -2452,12 +2473,12 @@ extension BridgeJSLink { ) -> [String] { let printer = CodeFragmentPrinter() printer.write(lines: renderJSDoc(documentation: klass.documentation, parameters: [])) - printer.write("class \(klass.name) {") + printer.write("class \(klass.resolvedJSName) {") printer.indent { if let constructor = klass.constructor { let paramSignatures = constructor.parameters.map { param in let optional = param.hasDefault ? "?" : "" - return "\(param.name)\(optional): \(param.type.tsType)" + return "\(param.name)\(optional): \(resolveTypeScriptType(param.type))" } printer.write( lines: renderJSDoc(documentation: constructor.documentation, parameters: constructor.parameters) @@ -2584,7 +2605,6 @@ extension BridgeJSLink { scope: scope, printer: body, hasDirectAccessToSwiftClass: false, - classNamespaces: intrinsicRegistry.classNamespaces ) } @@ -2922,9 +2942,9 @@ extension BridgeJSLink { return Set( exportedSkeletons.flatMap { skeleton in let itemNamespaces = - (skeleton.functions.compactMap(\.namespace) + skeleton.classes.compactMap(\.namespace) - + skeleton.enums.filter { $0.namespace != nil && $0.enumType != .namespace } - .compactMap(\.namespace)) + (skeleton.functions.compactMap(\.namespace) + skeleton.classes.compactMap(\.resolvedJSNamespace) + + skeleton.enums.filter { $0.resolvedJSNamespace != nil && $0.enumType != .namespace } + .compactMap(\.resolvedJSNamespace)) let namespaceEnumPaths = skeleton.enums .filter { $0.enumType == .namespace } @@ -2951,8 +2971,9 @@ extension BridgeJSLink { var namespacedEnumPaths: Set<[String]> = [] for skeleton in globalSkeletons { - for enumDef in skeleton.enums where enumDef.namespace != nil && enumDef.enumType != .namespace { - namespacedEnumPaths.insert(enumDef.namespace!) + for enumDef in skeleton.enums where enumDef.resolvedJSNamespace != nil && enumDef.enumType != .namespace + { + namespacedEnumPaths.insert(enumDef.resolvedJSNamespace!) } } @@ -2962,8 +2983,9 @@ extension BridgeJSLink { printer.write(lines: initCode) for skeleton in globalSkeletons { - for enumDef in skeleton.enums where enumDef.namespace != nil && enumDef.enumType != .namespace { - let namespacePath = enumDef.namespace!.joined(separator: ".") + for enumDef in skeleton.enums where enumDef.resolvedJSNamespace != nil && enumDef.enumType != .namespace + { + let namespacePath = enumDef.resolvedJSNamespace!.joined(separator: ".") printer.write("globalThis.\(namespacePath).\(enumDef.valuesName) = \(enumDef.valuesName);") } } @@ -2975,9 +2997,11 @@ extension BridgeJSLink { let printer = CodeFragmentPrinter() for skeleton in exportedSkeletons where skeleton.exposeToGlobal { - for klass in skeleton.classes where klass.namespace != nil { - let namespacePath = klass.namespace!.joined(separator: ".") - printer.write("globalThis.\(namespacePath).\(klass.name) = exports.\(namespacePath).\(klass.name);") + for klass in skeleton.classes where klass.resolvedJSNamespace != nil { + let namespacePath = klass.resolvedJSNamespace!.joined(separator: ".") + printer.write( + "globalThis.\(namespacePath).\(klass.resolvedJSName) = exports.\(namespacePath).\(klass.resolvedJSName);" + ) } for function in skeleton.functions where function.namespace != nil { let namespacePath = function.namespace!.joined(separator: ".") @@ -3120,9 +3144,10 @@ extension BridgeJSLink { currentNode.content.declaration = .structType(structDef) } - for enumDef in skeleton.enums where enumDef.namespace != nil && enumDef.enumType != .namespace { + for enumDef in skeleton.enums where enumDef.resolvedJSNamespace != nil && enumDef.enumType != .namespace + { var currentNode = rootNode - for part in enumDef.namespace! { + for part in enumDef.resolvedJSNamespace! { currentNode = currentNode.addChild(part) } currentNode.content.enums.append(enumDef) @@ -3199,7 +3224,9 @@ extension BridgeJSLink { } for enumDef in node.content.enums { - node.content.enumDtsLines.append((enumDef.name, "\(enumDef.name): \(enumDef.objectTypeName)")) + node.content.enumDtsLines.append( + (enumDef.resolvedJSName, "\(enumDef.resolvedJSName): \(enumDef.objectTypeName)") + ) } for (_, childNode) in node.children { @@ -3410,7 +3437,7 @@ extension BridgeJSLink { printer.write(lines: node.content.structJsLines) for enumDef in node.content.enums.sorted(by: { $0.name < $1.name }) { - printer.write("\(enumDef.name): \(enumDef.valuesName),") + printer.write("\(enumDef.resolvedJSName): \(enumDef.valuesName),") } // Print function and property implementations @@ -3573,7 +3600,7 @@ extension BridgeJSLink { case .simple: switch style { case .tsEnum: - printer.write("enum \(enumDefinition.name) {") + printer.write("enum \(enumDefinition.resolvedJSName) {") printer.indent { for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter @@ -3591,14 +3618,14 @@ extension BridgeJSLink { } printer.write("};") printer.write( - "type \(enumDefinition.name)Tag = typeof \(enumValuesName)[keyof typeof \(enumValuesName)];" + "type \(enumDefinition.resolvedJSName)Tag = typeof \(enumValuesName)[keyof typeof \(enumValuesName)];" ) } case .rawValue: guard let rawType = enumDefinition.rawType else { continue } switch style { case .tsEnum: - printer.write("enum \(enumDefinition.name) {") + printer.write("enum \(enumDefinition.resolvedJSName) {") printer.indent { for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter @@ -3624,7 +3651,7 @@ extension BridgeJSLink { } printer.write("};") printer.write( - "type \(enumDefinition.name)Tag = typeof \(enumValuesName)[keyof typeof \(enumValuesName)];" + "type \(enumDefinition.resolvedJSName)Tag = typeof \(enumValuesName)[keyof typeof \(enumValuesName)];" ) } case .associatedValue: @@ -3663,7 +3690,8 @@ extension BridgeJSLink { } } let unionTypeName = - enumDefinition.emitStyle == .tsEnum ? enumDefinition.name : "\(enumDefinition.name)Tag" + enumDefinition.emitStyle == .tsEnum + ? enumDefinition.resolvedJSName : "\(enumDefinition.resolvedJSName)Tag" printer.write("type \(unionTypeName) =") printer.write(" " + unionParts.joined(separator: " | ")) case .namespace: @@ -3688,7 +3716,9 @@ extension BridgeJSLink { for property in sortedProperties { let readonly = property.isReadonly ? "var " : "let " printer.write(lines: renderDocCallback(property.documentation, [])) - printer.write("\(readonly)\(property.resolvedJSName): \(property.type.tsType);") + printer.write( + "\(readonly)\(property.resolvedJSName): \(BridgeJSLink.resolveTypeScriptType(property.type, exportedSkeletons: exportedSkeletons));" + ) } } @@ -3769,7 +3799,8 @@ extension BridgeJSLink { let abiName = getter.abiName(context: nil) let funcLines = thunkBuilder.renderFunction(name: abiName) if getter.from == nil { - importObjectBuilder.appendDts(["readonly \(renderTSPropertyName(jsName)): \(getter.type.tsType);"] + importObjectBuilder.appendDts( + ["readonly \(renderTSPropertyName(jsName)): \(resolveTypeScriptType(getter.type));"] ) } importObjectBuilder.assignToImportObject(name: abiName, function: funcLines) @@ -4036,7 +4067,11 @@ enum DefaultValueUtils { } /// Generates default value representation for JavaScript or TypeScript - static func format(_ defaultValue: DefaultValue, as format: OutputFormat) -> String { + static func format( + _ defaultValue: DefaultValue, + as format: OutputFormat, + resolveTypeName: (BridgeType, OutputFormat) -> String? = { _, _ in nil } + ) -> String { switch defaultValue { case .string(let value): let escapedValue = @@ -4056,23 +4091,26 @@ enum DefaultValueUtils { return "null" case .enumCase(let enumName, let caseName): let simpleName = enumName.components(separatedBy: ".").last ?? enumName - let jsEnumName = format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName + let jsEnumName = + resolveTypeName(.caseEnum(enumName), format) + ?? (format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName) return "\(jsEnumName).\(caseName.capitalizedFirstLetter)" case .object(let className): - return "new \(className)()" + return "new \(resolveTypeName(.swiftHeapObject(className), format) ?? className)()" case .objectWithArguments(let className, let args): let argStrings = args.map { arg in - Self.format(arg, as: format) + Self.format(arg, as: format, resolveTypeName: resolveTypeName) } - return "new \(className)(\(argStrings.joined(separator: ", ")))" + let name = resolveTypeName(.swiftHeapObject(className), format) ?? className + return "new \(name)(\(argStrings.joined(separator: ", ")))" case .structLiteral(_, let fields): let fieldStrings = fields.map { field in - "\(field.name): \(Self.format(field.value, as: format))" + "\(field.name): \(Self.format(field.value, as: format, resolveTypeName: resolveTypeName))" } return "{ \(fieldStrings.joined(separator: ", ")) }" case .array(let elements): let elementStrings = elements.map { element in - DefaultValueUtils.format(element, as: format) + DefaultValueUtils.format(element, as: format, resolveTypeName: resolveTypeName) } return "[\(elementStrings.joined(separator: ", "))]" } @@ -4086,10 +4124,13 @@ enum DefaultValueUtils { } /// Generates a JavaScript parameter list with default values - static func formatParameterList(_ parameters: [Parameter]) -> String { + static func formatParameterList( + _ parameters: [Parameter], + resolveTypeName: (BridgeType, OutputFormat) -> String? = { _, _ in nil } + ) -> String { return parameters.map { param in if let defaultValue = param.defaultValue { - let defaultJs = format(defaultValue, as: .javascript) + let defaultJs = format(defaultValue, as: .javascript, resolveTypeName: resolveTypeName) return "\(param.name) = \(defaultJs)" } return param.name @@ -4104,10 +4145,17 @@ extension BridgeJSLink { fileprivate func renderJSDoc(documentation: String?, parameters: [Parameter]) -> [String] { let parsed = documentation.map(DocCComment.init(parsing:)) ?? DocCComment() + let resolveTypeName = makeCodecPrintContext(printer: CodeFragmentPrinter()).defaultValueTypeName var tagLines: [String] = [] for parameter in parameters { let docText = parsed.parameter(named: parameter.name) - let defaultValue = parameter.defaultValue.map { DefaultValueUtils.format($0, as: .typescript) } + let defaultValue = parameter.defaultValue.map { + DefaultValueUtils.format( + $0, + as: .typescript, + resolveTypeName: resolveTypeName + ) + } switch (docText, defaultValue) { case let (.some(text), .some(value)): tagLines.append("@param \(parameter.name) \(text) (default: \(value))") diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index cb79b5625..747c9f532 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -110,6 +110,14 @@ final class JSGlueVariableScope { intrinsicRegistry.typeOwnerModules[typeName] } + func exportedClassPath(forSwiftName name: String) -> [String]? { + intrinsicRegistry.classPaths[name] + } + + func renamedEnumNames(forSwiftName name: String) -> (value: String, type: String)? { + intrinsicRegistry.renamedEnumNames[name] + } + func makeChildScope() -> JSGlueVariableScope { JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry) } @@ -532,8 +540,6 @@ struct IntrinsicJSFragment: Sendable { /// Whether the fragment has direct access to the SwiftHeapObject classes. /// If false, the fragment needs to use `_exports` to access the class. var hasDirectAccessToSwiftClass: Bool = true - /// Maps class names to their namespace path components for resolving `_exports` access. - var classNamespaces: [String: [String]] = [:] func with(_ keyPath: WritableKeyPath, _ value: T) -> PrintCodeContext { var new = self @@ -545,20 +551,30 @@ struct IntrinsicJSFragment: Sendable { qualifiedName.split(separator: ".").last.map(String.init) ?? qualifiedName } - private func exportsAccess(forClass name: String) -> String { - if let namespace = classNamespaces[name], !namespace.isEmpty { - let path = namespace.map { ".\($0)" }.joined() - return "_exports\(path).\(name)" - } - return "_exports['\(name)']" - } - func classReference(forQualifiedName qualifiedName: String) -> String { + if let path = scope.exportedClassPath(forSwiftName: qualifiedName), let name = path.last { + if hasDirectAccessToSwiftClass { + return name + } + return path.count == 1 ? "_exports['\(name)']" : "_exports.\(path.joined(separator: "."))" + } if hasDirectAccessToSwiftClass { return unqualifiedClassName(for: qualifiedName) } - let unqualified = unqualifiedClassName(for: qualifiedName) - return exportsAccess(forClass: unqualified) + return "_exports['\(unqualifiedClassName(for: qualifiedName))']" + } + + func defaultValueTypeName(_ type: BridgeType, _ format: DefaultValueUtils.OutputFormat) -> String? { + switch type { + case .swiftHeapObject(let name): + guard let jsName = scope.exportedClassPath(forSwiftName: name)?.last else { return nil } + return format == .javascript ? classReference(forQualifiedName: name) : jsName + case .caseEnum(let name): + guard let renamed = scope.renamedEnumNames(forSwiftName: name) else { return nil } + return format == .javascript ? renamed.value : renamed.type + default: + return nil + } } } @@ -2565,7 +2581,10 @@ struct IntrinsicJSFragment: Sendable { // Attach instance methods to the struct instance for method in structDef.methods where !method.effects.isStatic { - let paramList = DefaultValueUtils.formatParameterList(method.parameters) + let paramList = DefaultValueUtils.formatParameterList( + method.parameters, + resolveTypeName: context.defaultValueTypeName + ) printer.write( "\(instanceVar).\(method.resolvedJSName) = function(\(paramList)) {" ) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSIntrinsicRegistry.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSIntrinsicRegistry.swift index d0bf2781f..8dfca16b6 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSIntrinsicRegistry.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSIntrinsicRegistry.swift @@ -5,7 +5,8 @@ import BridgeJSUtilities /// Registry for JS helper intrinsics used during code generation. final class JSIntrinsicRegistry { private var entries: [String: [String]] = [:] - var classNamespaces: [String: [String]] = [:] + var classPaths: [String: [String]] = [:] + var renamedEnumNames: [String: (value: String, type: String)] = [:] var typeOwnerModules: [String: String] = [:] @@ -42,7 +43,8 @@ final class JSIntrinsicRegistry { func reset() { entries.removeAll() - classNamespaces.removeAll() + classPaths.removeAll() + renamedEnumNames.removeAll() typeOwnerModules.removeAll() codecNameOrder.removeAll() codecBodies.removeAll() diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 8aaa6d5cd..ce85176cc 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -4,10 +4,17 @@ public protocol NamespacedExportedType { var name: String { get } + var jsName: String? { get } + /// The namespace using Swift declaration names, retained for ABI generation. var namespace: [String]? { get } + /// The public namespace when an enclosing declaration has an exported name override. + var jsNamespace: [String]? { get } } extension NamespacedExportedType { + public var resolvedJSName: String { jsName ?? name } + public var resolvedJSNamespace: [String]? { jsNamespace ?? namespace } + public var abiName: String { if let namespace = namespace, !namespace.isEmpty { return (namespace + [name]).joined(separator: "_") @@ -16,7 +23,7 @@ extension NamespacedExportedType { } public var tsPathComponents: [String] { - (namespace ?? []) + [name] + (resolvedJSNamespace ?? []) + [resolvedJSName] } public var tsFullPath: String { @@ -778,31 +785,37 @@ public struct StructField: Codable, Equatable, Sendable { public struct ExportedStruct: Codable, Equatable, Sendable, NamespacedExportedType { public let name: String + public let jsName: String? public let swiftCallName: String public let explicitAccessControl: String? public var properties: [ExportedProperty] public var constructor: ExportedConstructor? public var methods: [ExportedFunction] public let namespace: [String]? + public let jsNamespace: [String]? public var documentation: String? public init( name: String, + jsName: String? = nil, swiftCallName: String, explicitAccessControl: String?, properties: [ExportedProperty] = [], constructor: ExportedConstructor? = nil, methods: [ExportedFunction] = [], namespace: [String]?, + jsNamespace: [String]? = nil, documentation: String? = nil ) { self.name = name + self.jsName = jsName self.swiftCallName = swiftCallName self.explicitAccessControl = explicitAccessControl self.properties = properties self.constructor = constructor self.methods = methods self.namespace = namespace + self.jsNamespace = jsNamespace self.documentation = documentation } } @@ -864,12 +877,14 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType public static let objectSuffix = "Object" public let name: String + public let jsName: String? public let swiftCallName: String public let tsFullPath: String public let explicitAccessControl: String? public var cases: [EnumCase] public let rawType: SwiftEnumRawType? public let namespace: [String]? + public let jsNamespace: [String]? public let emitStyle: EnumEmitStyle public var staticMethods: [ExportedFunction] public var staticProperties: [ExportedProperty] = [] @@ -885,33 +900,36 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType } public var valuesName: String { - emitStyle == .tsEnum ? name : "\(name)\(Self.valuesSuffix)" + emitStyle == .tsEnum ? resolvedJSName : "\(resolvedJSName)\(Self.valuesSuffix)" } public var objectTypeName: String { - "\(name)\(Self.objectSuffix)" + "\(resolvedJSName)\(Self.objectSuffix)" } public init( name: String, + jsName: String? = nil, swiftCallName: String, - tsFullPath: String, explicitAccessControl: String?, cases: [EnumCase], rawType: SwiftEnumRawType?, namespace: [String]?, + jsNamespace: [String]? = nil, emitStyle: EnumEmitStyle, staticMethods: [ExportedFunction] = [], staticProperties: [ExportedProperty] = [], documentation: String? = nil ) { self.name = name + self.jsName = jsName self.swiftCallName = swiftCallName - self.tsFullPath = tsFullPath + self.tsFullPath = ((jsNamespace ?? namespace ?? []) + [jsName ?? name]).joined(separator: ".") self.explicitAccessControl = explicitAccessControl self.cases = cases self.rawType = rawType self.namespace = namespace + self.jsNamespace = jsNamespace self.emitStyle = emitStyle self.staticMethods = staticMethods self.staticProperties = staticProperties @@ -947,24 +965,30 @@ public struct ExportedProtocolProperty: Codable, Equatable, Sendable { } } -public struct ExportedProtocol: Codable, Equatable { +public struct ExportedProtocol: Codable, Equatable, NamespacedExportedType { public let name: String + public let jsName: String? public let methods: [ExportedFunction] public let properties: [ExportedProtocolProperty] public let namespace: [String]? + public let jsNamespace: [String]? public var documentation: String? public init( name: String, + jsName: String? = nil, methods: [ExportedFunction], properties: [ExportedProtocolProperty] = [], namespace: [String]? = nil, + jsNamespace: [String]? = nil, documentation: String? = nil ) { self.name = name + self.jsName = jsName self.methods = methods self.properties = properties self.namespace = namespace + self.jsNamespace = jsNamespace self.documentation = documentation } } @@ -1007,35 +1031,41 @@ public struct ExportedFunction: Codable, Equatable, Sendable { public struct ExportedClass: Codable, NamespacedExportedType { public var name: String + public var jsName: String? public var swiftCallName: String public var explicitAccessControl: String? public var constructor: ExportedConstructor? public var methods: [ExportedFunction] public var properties: [ExportedProperty] public var namespace: [String]? + public var jsNamespace: [String]? public var identityMode: Bool? // nil = use config default, true/false = override public var documentation: String? public var isFinal: Bool? public init( name: String, + jsName: String? = nil, swiftCallName: String, explicitAccessControl: String?, constructor: ExportedConstructor? = nil, methods: [ExportedFunction], properties: [ExportedProperty] = [], namespace: [String]? = nil, + jsNamespace: [String]? = nil, identityMode: Bool? = nil, documentation: String? = nil, isFinal: Bool? = nil ) { self.name = name + self.jsName = jsName self.swiftCallName = swiftCallName self.explicitAccessControl = explicitAccessControl self.constructor = constructor self.methods = methods self.properties = properties self.namespace = namespace + self.jsNamespace = jsNamespace self.identityMode = identityMode self.documentation = documentation self.isFinal = isFinal diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift index ea8ae9402..580360bb7 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift @@ -696,42 +696,6 @@ import Testing } } - @Test - func jsNameOnClassDiagnostic() throws { - let source = """ - @JS("Renamed") class Box { @JS init() {} } - """ - let diagnostics = try #require(moduleDiagnostics(source: source)) - #expect(diagnostics.description.contains("A separate name for JavaScript is not supported here")) - } - - @Test - func jsNameOnStructDiagnostic() throws { - let source = """ - @JS("Renamed") struct Box { var x: Int } - """ - let diagnostics = try #require(moduleDiagnostics(source: source)) - #expect(diagnostics.description.contains("A separate name for JavaScript is not supported here")) - } - - @Test - func jsNameOnEnumDiagnostic() throws { - let source = """ - @JS("Renamed") enum Box { case a } - """ - let diagnostics = try #require(moduleDiagnostics(source: source)) - #expect(diagnostics.description.contains("A separate name for JavaScript is not supported here")) - } - - @Test - func jsNameOnProtocolDiagnostic() throws { - let source = """ - @JS("Renamed") protocol Box { func run() } - """ - let diagnostics = try #require(moduleDiagnostics(source: source)) - #expect(diagnostics.description.contains("A separate name for JavaScript is not supported here")) - } - @Test func jsNameOnInitializerDiagnostic() throws { let source = """ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportedTypeNameTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportedTypeNameTests.swift new file mode 100644 index 000000000..9d7c9a689 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportedTypeNameTests.swift @@ -0,0 +1,60 @@ +import Foundation +import SwiftParser +import Testing + +@testable import BridgeJSCore +@testable import BridgeJSSkeleton + +@Suite struct ExportedTypeNameTests { + private func parse(_ source: String) throws -> ExportedSkeleton { + let generator = SwiftToSkeleton( + progress: .silent, + moduleName: "TestModule", + exposeToGlobal: false, + externalModuleIndex: .empty + ) + generator.addSourceFile(Parser.parse(source: source), inputFilePath: "Types.swift") + return try #require(generator.finalize().exported) + } + + @Test + func renamedTypesKeepSwiftIdentities() throws { + let exported = try parse( + """ + @JS func box() -> SwiftBox + @JS func record() -> SwiftRecord + @JS func choice() -> SwiftChoice + @JS func delegate(_ value: SwiftDelegate) + @JS("PublicBox") class SwiftBox { @JS init() {} } + @JS("PublicRecord") struct SwiftRecord { var value: Int } + @JS("PublicChoice") enum SwiftChoice { case first } + @JS("PublicDelegate") protocol SwiftDelegate { func run() } + """ + ) + #expect(exported.classes.first?.resolvedJSName == "PublicBox") + #expect(exported.structs.first?.resolvedJSName == "PublicRecord") + #expect(exported.enums.first?.resolvedJSName == "PublicChoice") + #expect(exported.protocols.first?.resolvedJSName == "PublicDelegate") + #expect(exported.functions[0].returnType == .swiftHeapObject("SwiftBox")) + #expect(exported.functions[1].returnType == .swiftStruct("SwiftRecord")) + #expect(exported.functions[2].returnType == .caseEnum("SwiftChoice")) + #expect(exported.functions[3].parameters.first?.type == .swiftProtocol("SwiftDelegate")) + } + + @Test + func renamedParentsKeepSwiftThunkABI() throws { + let source = """ + @JS(namespace: "API") enum InternalAPI { + @JS class Item { @JS init() {} } + } + """ + let original = try parse(source) + let renamed = try parse( + source.replacingOccurrences(of: "@JS(namespace:", with: "@JS(\"PublicAPI\", namespace:") + ) + let originalThunks = try ExportSwift(progress: .silent, moduleName: "TestModule", skeleton: original).finalize() + let renamedThunks = try ExportSwift(progress: .silent, moduleName: "TestModule", skeleton: renamed).finalize() + #expect(renamedThunks == originalThunks) + #expect(renamed.classes.first?.tsFullPath == "API.PublicAPI.Item") + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/JSNameTypes.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/JSNameTypes.swift new file mode 100644 index 000000000..40a41bf78 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/JSNameTypes.swift @@ -0,0 +1,11 @@ +@JS("PublicBox") final class SwiftBox { + @JS init() + @JS func copy() -> SwiftBox +} + +@JS("PublicChoice") enum SwiftChoice { + case first + case second +} + +@JS func renamedChoice(_ value: SwiftChoice) -> SwiftChoice diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSNameTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSNameTypes.json new file mode 100644 index 000000000..9e3bc099c --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSNameTypes.json @@ -0,0 +1,115 @@ +{ + "exported" : { + "aliases" : [ + + ], + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_SwiftBox_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "isFinal" : true, + "jsName" : "PublicBox", + "methods" : [ + { + "abiName" : "bjs_SwiftBox_copy", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "copy", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "SwiftBox" + } + } + } + ], + "name" : "SwiftBox", + "properties" : [ + + ], + "swiftCallName" : "SwiftBox" + } + ], + "enums" : [ + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "first" + }, + { + "associatedValues" : [ + + ], + "name" : "second" + } + ], + "emitStyle" : "const", + "jsName" : "PublicChoice", + "name" : "SwiftChoice", + "staticMethods" : [ + + ], + "staticProperties" : [ + + ], + "swiftCallName" : "SwiftChoice", + "tsFullPath" : "PublicChoice" + } + ], + "exposeToGlobal" : false, + "functions" : [ + { + "abiName" : "bjs_renamedChoice", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "renamedChoice", + "parameters" : [ + { + "label" : "_", + "name" : "value", + "type" : { + "caseEnum" : { + "_0" : "SwiftChoice" + } + } + } + ], + "returnType" : { + "caseEnum" : { + "_0" : "SwiftChoice" + } + } + } + ], + "protocols" : [ + + ], + "structs" : [ + + ] + }, + "moduleName" : "TestModule", + "usedExternalModules" : [ + + ] +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSNameTypes.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSNameTypes.swift new file mode 100644 index 000000000..ddcf5b98b --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/JSNameTypes.swift @@ -0,0 +1,124 @@ +extension SwiftChoice: _BridgedSwiftCaseEnum { + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { + return bridgeJSRawValue + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftReturn(_ value: Int32) -> SwiftChoice { + return bridgeJSLiftParameter(value) + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter(_ value: Int32) -> SwiftChoice { + return SwiftChoice(bridgeJSRawValue: value)! + } + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() -> Int32 { + return bridgeJSLowerParameter() + } + + @_spi(BridgeJS) @usableFromInline init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .first + case 1: + self = .second + default: + return nil + } + } + + @_spi(BridgeJS) @usableFromInline var bridgeJSRawValue: Int32 { + switch self { + case .first: + return 0 + case .second: + return 1 + } + } +} + +@_expose(wasm, "bjs_renamedChoice") +@_cdecl("bjs_renamedChoice") +public func _bjs_renamedChoice(_ value: Int32) -> Int32 { + #if arch(wasm32) + let value = SwiftChoice.bridgeJSLiftParameter(value) + let ret = renamedChoice(_: value) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftBox_init") +@_cdecl("bjs_SwiftBox_init") +public func _bjs_SwiftBox_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = SwiftBox() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftBox_copy") +@_cdecl("bjs_SwiftBox_copy") +public func _bjs_SwiftBox_copy(_ _self: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let _self = SwiftBox.bridgeJSLiftParameter(_self) + let ret = _self.copy() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftBox_deinit") +@_cdecl("bjs_SwiftBox_deinit") +public func _bjs_SwiftBox_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension SwiftBox: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_SwiftBox_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_SwiftBox_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_SwiftBox_wrap") +fileprivate func _bjs_SwiftBox_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_SwiftBox_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_SwiftBox_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_SwiftBox_wrap_extern(pointer) +} + +extension SwiftBox: BridgedSwiftGenericBridgeable { + @_spi(BridgeJS) public static let bridgeJSTypeHandle = SwiftBox.bridgeJSMakeTypeHandle() +} + +extension SwiftChoice: BridgedSwiftGenericBridgeable { + @_spi(BridgeJS) public static let bridgeJSTypeHandle = SwiftChoice.bridgeJSMakeTypeHandle() +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "bjs_TestModule_register_type_handles") +fileprivate func _bjs_TestModule_register_type_handles_extern(_ base: UnsafePointer?, _ count: Int32) + +@_expose(wasm, "bjs_TestModule_register_type_handles") +public func _bjs_TestModule_register_type_handles() { + let typeIds: [Int32] = [ + SwiftBox.bridgeJSTypeID, + SwiftChoice.bridgeJSTypeID, + ] + typeIds.withUnsafeBufferPointer { buffer in + _bjs_TestModule_register_type_handles_extern(buffer.baseAddress, Int32(buffer.count)) + } +} +#endif \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSNameTypes.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSNameTypes.d.ts new file mode 100644 index 000000000..e7a00d877 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSNameTypes.d.ts @@ -0,0 +1,40 @@ +// 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 PublicChoiceValues: { + readonly First: 0; + readonly Second: 1; +}; +export type PublicChoiceTag = typeof PublicChoiceValues[keyof typeof PublicChoiceValues]; + +export type PublicChoiceObject = typeof PublicChoiceValues; + +/// 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 PublicBox extends SwiftHeapObject { + copy(): PublicBox; +} +export type Exports = { + renamedChoice(value: PublicChoiceTag): PublicChoiceTag; + PublicChoice: PublicChoiceObject + PublicBox: { + new(): PublicBox; + }, +} +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/JSNameTypes.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSNameTypes.js new file mode 100644 index 000000000..fb061d342 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSNameTypes.js @@ -0,0 +1,313 @@ +// 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 PublicChoiceValues = { + First: 0, + Second: 1, +}; + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + let taStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(ptr, len) { + tmpRetString = decodeString(ptr, len); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr >>> 0); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + return swift.memory.retain(decodeString(ptr, len)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const value = decodeString(ptr, len); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + const taCtors = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]; + bjs["swift_js_push_typed_array"] = function(kind, ptr, count) { + const Ctor = taCtors[kind]; + const byteLen = count * Ctor.BYTES_PER_ELEMENT; + const copy = memory.buffer.slice(ptr, ptr + byteLen); + taStack.push(Array.from(new Ctor(copy))); + } + bjs["bjs_core_register_type_handles"] = function() {}; + bjs["bjs_TestModule_register_type_handles"] = function() {}; + const __bjs_promiseSettlers = Symbol("JavaScriptKit.promiseSettlers"); + bjs["swift_js_make_promise"] = function() { + let resolve, reject; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + promise[__bjs_promiseSettlers] = { resolve, reject }; + return swift.memory.retain(promise); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(ptr, len); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_SwiftBox_wrap"] = function(pointer) { + const obj = _exports['PublicBox'].__construct(pointer); + return swift.memory.retain(obj); + }; + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + decodeString = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.hasReleased) { + return; + } + state.hasReleased = true; + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + }); + + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype, identityCache) { + pointer = pointer >>> 0; + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); + } + + release() { + const state = this.__swiftHeapObjectState; + if (state.hasReleased) { + return; + } + state.hasReleased = true; + swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + } + } + class PublicBox extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_SwiftBox_deinit, PublicBox.prototype, null); + } + + constructor() { + const ret = instance.exports.bjs_SwiftBox_init(); + return PublicBox.__construct(ret); + } + copy() { + const ret = instance.exports.bjs_SwiftBox_copy(this.pointer); + return PublicBox.__construct(ret); + } + } + const exports = { + renamedChoice: function bjs_renamedChoice(value) { + const ret = instance.exports.bjs_renamedChoice(value); + return ret; + }, + PublicChoice: PublicChoiceValues, + PublicBox, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md index 8a1b7dff5..09bc51d79 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md @@ -77,6 +77,48 @@ export type Exports = { } ``` +## Choosing an Exported Name + +Pass a name to `@JS` to expose a class under a different JavaScript and TypeScript name while keeping its Swift identifier: + +```swift +@JS("Counter") class InternalCounter { + @JS var value: Int + + @JS init(value: Int) { + self.value = value + } + + @JS static func make(_ value: Int) -> InternalCounter { + InternalCounter(value: value) + } + + @JS func roundTrip(_ other: InternalCounter?) -> InternalCounter? { + other + } +} +``` + +After initializing the package, use the constructor and static factory on that WASM instance's `exports` object: + +```javascript +const counter = new exports.Counter(1); +const other = exports.Counter.make(2); +const returned = counter.roundTrip(other); +console.log(returned.value); // 2 +returned.release(); +other.release(); +counter.release(); +``` + +The generated TypeScript interface is named `Counter`, and signatures refer to `Counter`, including in optionals, arrays, and callbacks. For example, `roundTrip` accepts and returns `Counter | null`. The old name `InternalCounter` is not also exported. Swift code continues to use `InternalCounter`, including in extensions and return types. + +The constructor remains instance-owned: use `exports.Counter`, not a named JavaScript module import such as `import { Counter } from "./bridge-js.js"`. A TypeScript type-only import of `Counter` refers to the generated interface, not a constructor value. Renaming does not change reference semantics, `release()`, or identity mode. + +This naming option also applies to structs, enums, and protocols. For namespace composition and nested types, see . + +> Important: `@JS("Counter")` only changes the exported symbol. `@JS(as: Other.self)` instead changes the JavaScript representation through `bridgeToJS()` and `bridgeFromJS(_:)`. Combining a name override with `as:` on the same declaration produces a diagnostic. + ## Adding Members via Extensions You can add exported methods, computed properties, and static members to a `@JS` class using extensions. The extension block itself does not need `@JS` - only the individual members do: diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md index 68996b27b..6d7af6799 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md @@ -19,6 +19,35 @@ BridgeJS generates separate objects with descriptive naming for `.const` enums: - **`EnumNameTag`**: Represents the union type for enums - **`EnumNameObject`**: Object type for all const-style enums, contains static members for enums with methods/properties or references the values type for simple enums +### Choosing an Exported Name + +A name passed to `@JS` changes the generated naming stem, not the Swift enum or its representation: + +```swift +@JS("TaskState") enum InternalTaskState: String { + case pending + case complete +} + +@JS func roundTripState(_ state: InternalTaskState) -> InternalTaskState { + state +} +``` + +With the default `.const` style, this generates `TaskStateValues`, `TaskStateTag`, and `TaskStateObject` instead of names beginning with `InternalTaskState`. The instance export is `exports.TaskState`, and function signatures use `TaskStateTag`: + +```typescript +import { TaskStateValues } from "./bridge-js.js"; +import type { TaskStateTag } from "./bridge-js.js"; + +const state: TaskStateTag = exports.roundTripState(TaskStateValues.Pending); +console.log(state === exports.TaskState.Pending); // true +``` + +The same naming rule applies to associated-value enums, including their payload type references. With `@JS("TaskState", enumStyle: .tsEnum)`, the TypeScript enum itself is named `TaskState`. Case names, raw values, and associated-value layouts are unchanged. + +For empty enums used as namespaces, the override names the namespace object. Nested declarations use that renamed parent in their export paths; see . + ### Case Enums ```swift diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md index 3b2ec1526..d62d71051 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md @@ -13,6 +13,8 @@ When you mark a protocol with `@JS`, BridgeJS generates: - A TypeScript interface with the protocol's method signatures - A Swift wrapper struct (`Any{ProtocolName}`) that conforms to the protocol and bridges calls to JavaScript objects +Use `@JS("PublicName")` on a protocol to rename its TypeScript interface and generated type references. Swift still uses the original protocol name for conformances and signatures. JavaScript objects still satisfy the interface structurally; renaming does not create a protocol constructor or change how callbacks and protocol values cross the boundary. Unlike `@JS(as: Other.self)`, it does not change the representation, and combining the two options is not supported. + ## Example: Counter Protocol Mark a Swift protocol with `@JS` to expose it: diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md index c4a9524d9..208c80a25 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md @@ -6,7 +6,9 @@ Learn how to export Swift structs to JavaScript. > Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). -To export a Swift struct, mark it with `@JS`: +To export a Swift struct, mark it with `@JS`. + +Use `@JS("PublicName")` to choose a different JavaScript and TypeScript name while keeping the Swift struct name. Generated interfaces, type references, and the instance export containing any exported initializer or static members use the chosen name. This does not change copy semantics or field names. Unlike `@JS(as: Other.self)`, it does not change the representation; the two options cannot be combined. See for an example of naming an exported type. ```swift import JavaScriptKit diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md index 1aff82f65..e24f45a24 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md @@ -121,3 +121,45 @@ export interface Greeter extends SwiftHeapObject { ``` Using namespaces can be preferable for projects with many global functions, as they help prevent naming collisions. Namespaces also provide intuitive hierarchies for organizing your exported Swift code, and they do not affect the code generated by `@JS` declarations without namespaces. + +## Renamed Types in Namespaces + +The first argument to `@JS` controls a declaration's exported name; `namespace:` controls where it appears. Use both to separate Swift implementation names from your JavaScript API. + +Type names must be valid JavaScript/TypeScript identifiers. + +```swift +@JS("Store", namespace: "MyApp") enum InternalStore { + @JS("Item") class InternalItem { + @JS var title: String + + @JS init(title: String) { + self.title = title + } + + @JS enum State: String { + case available + case reserved + } + } + + @JS static func makeItem(_ title: String) -> InternalStore.InternalItem { + InternalStore.InternalItem(title: title) + } +} +``` + +Nested declarations use their parents' exported names, even when a nested declaration has no name override of its own: + +```javascript +const item = new exports.MyApp.Store.Item("Notebook"); +const another = exports.MyApp.Store.makeItem("Pencil"); +console.log(item.title); // "Notebook" +console.log(exports.MyApp.Store.Item.State.Available); // "available" +item.release(); +another.release(); +``` + +The export paths contain neither `InternalStore` nor `InternalItem`. Generated TypeScript type references also use the renamed types. Swift paths remain `InternalStore.InternalItem` and `InternalStore.InternalItem.State`. + +The same exported paths are available under `globalThis` only when `exposeToGlobal: true` is enabled. Renaming does not enable global exports or turn instance-owned constructors into named JavaScript module exports. See for the distinction between a type name and a constructor value. diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index a1cdc0c44..1e1b72dfb 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -50,7 +50,7 @@ public enum JSName: ExpressibleByStringLiteral { } } -/// A macro that exposes Swift functions, classes, and methods to JavaScript. +/// A macro that exposes Swift declarations to JavaScript and TypeScript. /// /// Apply this macro to Swift declarations that you want to make callable from JavaScript: /// @@ -138,9 +138,26 @@ public enum JSName: ExpressibleByStringLiteral { /// accessible from JavaScript, and TypeScript declaration files (`.d.ts`) will be /// automatically generated to provide type safety. /// +/// Use the first argument to choose a JavaScript and TypeScript name without changing +/// the Swift identifier: `@JS("Counter") class InternalCounter`. This also works on +/// structs, enums, and protocols. Generated type references use the chosen name, +/// including references in optional, array, and callback signatures. Nested exported +/// declarations inherit their enclosing types' exported names in their namespace paths. +/// For const-style enums, the chosen name is the stem of the generated `Values`, `Tag`, +/// and `Object` names. +/// +/// A renamed class constructor is still accessed through the WASM instance's exports +/// object, such as `exports.Counter`, not as a named JavaScript module export. +/// Renaming does not change bridging or memory-management semantics. In contrast, +/// `@JS(as: Other.self)` changes the JavaScript representation using conversion methods. +/// Combining a custom name with `as:` on the same declaration is not supported. +/// /// For detailed usage information, see the article . /// -/// - Parameter name: A different name to use in the exported JavaScript. +/// - Parameter name: A different JavaScript and TypeScript name for the exported declaration. +/// Does not rename the Swift declaration or change its representation. +/// - Parameter aliasOf: A different JavaScript representation, supplied as `as: Other.self`. +/// Requires `bridgeToJS()` and `bridgeFromJS(_:)` conversions and cannot be combined with `name`. /// - 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: diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 6ad1711a3..4496316e7 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -4858,6 +4858,33 @@ fileprivate func bjs_DataProcessor_optionalHelper_set_extern(_ jsObject: Int32, return bjs_DataProcessor_optionalHelper_set_extern(jsObject, newValueIsSome, newValuePointer) } +struct AnyJSNameTransformer: JSNameTransformer, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + func apply(_ value: Int) -> Int { + let valueValue = value.bridgeJSLowerParameter() + let jsObjectValue = jsObject.bridgeJSLowerParameter() + let ret = _extern_apply(jsObjectValue, valueValue) + return Int.bridgeJSLiftReturn(ret) + } + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyJSNameTransformer(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JSNameTransformer_apply") +fileprivate func _extern_apply_extern(_ jsObject: Int32, _ value: Int32) -> Int32 +#else +fileprivate func _extern_apply_extern(_ jsObject: Int32, _ value: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _extern_apply(_ jsObject: Int32, _ value: Int32) -> Int32 { + return _extern_apply_extern(jsObject, value) +} + extension Severity: _BridgedSwiftCaseEnum { @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { return bridgeJSRawValue @@ -6899,6 +6926,19 @@ public func _bjs_IntegerTypesSupportExports_static_roundTripUInt64(_ v: Int64) - #endif } +@_expose(wasm, "bjs_Renaming_JSNameTools_static_apply") +@_cdecl("bjs_Renaming_JSNameTools_static_apply") +public func _bjs_Renaming_JSNameTools_static_apply(_ transformer: Int32, _ value: Int32) -> Int32 { + #if arch(wasm32) + let value = Int.bridgeJSLiftParameter(value) + let transformer = AnyJSNameTransformer.bridgeJSLiftParameter(transformer) + let ret = JSNameTools.apply(_: transformer, _: value) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_JSTypedArrayExports_static_roundTripUint8Array") @_cdecl("bjs_JSTypedArrayExports_static_roundTripUint8Array") public func _bjs_JSTypedArrayExports_static_roundTripUint8Array(_ v: Int32) -> Int32 { @@ -7897,6 +7937,64 @@ fileprivate func _bjs_struct_lift_Point_extern() -> Int32 { return _bjs_struct_lift_Point_extern() } +extension JSNameSnapshot: _BridgedSwiftStruct { + @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> JSNameSnapshot { + let value = Int.bridgeJSStackPop() + return JSNameSnapshot(value: value) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSStackPush() { + self.value.bridgeJSStackPush() + } + + init(unsafelyCopying jsObject: JSObject) { + _bjs_struct_lower_JSNameSnapshot(jsObject.bridgeJSLowerParameter()) + self = Self.bridgeJSStackPop() + } + + func toJSObject() -> JSObject { + let __bjs_self = self + __bjs_self.bridgeJSStackPush() + return JSObject(id: UInt32(bitPattern: _bjs_struct_lift_JSNameSnapshot())) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_JSNameSnapshot") +fileprivate func _bjs_struct_lower_JSNameSnapshot_extern(_ objectId: Int32) -> Void +#else +fileprivate func _bjs_struct_lower_JSNameSnapshot_extern(_ objectId: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lower_JSNameSnapshot(_ objectId: Int32) -> Void { + return _bjs_struct_lower_JSNameSnapshot_extern(objectId) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lift_JSNameSnapshot") +fileprivate func _bjs_struct_lift_JSNameSnapshot_extern() -> Int32 +#else +fileprivate func _bjs_struct_lift_JSNameSnapshot_extern() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lift_JSNameSnapshot() -> Int32 { + return _bjs_struct_lift_JSNameSnapshot_extern() +} + +@_expose(wasm, "bjs_JSNameSnapshot_init") +@_cdecl("bjs_JSNameSnapshot_init") +public func _bjs_JSNameSnapshot_init(_ value: Int32) -> Void { + #if arch(wasm32) + let value = Int.bridgeJSLiftParameter(value) + let ret = JSNameSnapshot(value: value) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + extension PointerFields: _BridgedSwiftStruct { @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> PointerFields { let mutPtr = UnsafeMutablePointer.bridgeJSStackPop() @@ -15552,6 +15650,10 @@ extension Point: BridgedSwiftGenericBridgeable { @_spi(BridgeJS) public static let bridgeJSTypeHandle = Point.bridgeJSMakeTypeHandle() } +extension JSNameSnapshot: BridgedSwiftGenericBridgeable { + @_spi(BridgeJS) public static let bridgeJSTypeHandle = JSNameSnapshot.bridgeJSMakeTypeHandle() +} + extension PointerFields: BridgedSwiftGenericBridgeable { @_spi(BridgeJS) public static let bridgeJSTypeHandle = PointerFields.bridgeJSMakeTypeHandle() } @@ -20859,6 +20961,7 @@ public func _bjs_BridgeJSRuntimeTests_register_type_handles() { GenericRTPoint.bridgeJSTypeID, GenericRTNamespace.Metadata.bridgeJSTypeID, Point.bridgeJSTypeID, + JSNameSnapshot.bridgeJSTypeID, PointerFields.bridgeJSTypeID, DataPoint.bridgeJSTypeID, PublicPoint.bridgeJSTypeID, diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index e0c5a65b8..0084d9f6a 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -5143,6 +5143,7 @@ } ] }, + "jsName" : "NamedCounter", "methods" : [ { "abiName" : "bjs_JSNameRenamedClass_doubled", @@ -11291,6 +11292,73 @@ { "cases" : [ + ], + "emitStyle" : "const", + "jsName" : "CounterTools", + "name" : "JSNameTools", + "namespace" : [ + "Renaming" + ], + "staticMethods" : [ + { + "abiName" : "bjs_Renaming_JSNameTools_static_apply", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "apply", + "namespace" : [ + "Renaming", + "JSNameTools" + ], + "parameters" : [ + { + "label" : "_", + "name" : "transformer", + "type" : { + "swiftProtocol" : { + "_0" : "JSNameTransformer" + } + } + }, + { + "label" : "_", + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + }, + "staticContext" : { + "namespaceEnum" : { + "_0" : "JSNameTools" + } + } + } + ], + "staticProperties" : [ + + ], + "swiftCallName" : "JSNameTools", + "tsFullPath" : "Renaming.CounterTools" + }, + { + "cases" : [ + ], "emitStyle" : "const", "name" : "JSTypedArrayExports", @@ -18915,6 +18983,46 @@ } } ] + }, + { + "jsName" : "CounterTransform", + "methods" : [ + { + "abiName" : "bjs_JSNameTransformer_apply", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "apply", + "parameters" : [ + { + "label" : "_", + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "name" : "JSNameTransformer", + "properties" : [ + + ] } ], "structs" : [ @@ -19378,6 +19486,51 @@ ], "swiftCallName" : "Point" }, + { + "constructor" : { + "abiName" : "bjs_JSNameSnapshot_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "jsName" : "CounterSnapshot", + "methods" : [ + + ], + "name" : "JSNameSnapshot", + "properties" : [ + { + "isReadonly" : true, + "isStatic" : false, + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "swiftCallName" : "JSNameSnapshot" + }, { "constructor" : { "abiName" : "bjs_PointerFields_init", diff --git a/Tests/BridgeJSRuntimeTests/JSNameAPIs.swift b/Tests/BridgeJSRuntimeTests/JSNameAPIs.swift index 48efcff59..43d5b1fc5 100644 --- a/Tests/BridgeJSRuntimeTests/JSNameAPIs.swift +++ b/Tests/BridgeJSRuntimeTests/JSNameAPIs.swift @@ -12,7 +12,7 @@ import JavaScriptKit return "Hello, \(count) people!" } -@JS class JSNameRenamedClass { +@JS("NamedCounter") class JSNameRenamedClass { private var storage: Int @JS init(value: Int) { @@ -32,3 +32,21 @@ import JavaScriptKit return JSNameRenamedClass(value: value) } } + +@JS("CounterSnapshot") struct JSNameSnapshot { + var value: Int + + @JS init(value: Int) { + self.value = value + } +} + +@JS("CounterTransform") protocol JSNameTransformer { + func apply(_ value: Int) -> Int +} + +@JS("CounterTools", namespace: "Renaming") enum JSNameTools { + @JS static func apply(_ transformer: JSNameTransformer, _ value: Int) -> Int { + transformer.apply(value) + } +} diff --git a/Tests/BridgeJSRuntimeTests/JavaScript/JSNameTests.mjs b/Tests/BridgeJSRuntimeTests/JavaScript/JSNameTests.mjs new file mode 100644 index 000000000..5b7ddd1fa --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/JavaScript/JSNameTests.mjs @@ -0,0 +1,37 @@ +// @ts-check + +import assert from "node:assert"; + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +export function runJSNameTests(exports) { + assert.equal(exports.renamedEcho("hi"), "echo: hi"); + assert.ok(!("jsNameEcho" in exports)); + assert.equal(exports.greetName("John"), "Hello, John!"); + assert.equal(exports.greetCount(3), "Hello, 3 people!"); + assert.ok(!("JSNameRenamedClass" in exports)); + + /** @type {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').NamedCounter} */ + const counter = new exports.NamedCounter(21); + assert.equal(counter.doubled(), 42); + assert.equal(counter.current, 21); + counter.current = 5; + assert.equal(counter.doubled(), 10); + const made = exports.NamedCounter.makeWithValue(7); + assert.equal(made.current, 7); + assert.ok(made instanceof exports.NamedCounter); + + /** @type {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').CounterSnapshot} */ + const snapshot = exports.CounterSnapshot.init(9); + assert.equal(snapshot.value, 9); + + /** @type {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').CounterTransform} */ + const transformer = { + apply(value) { return value * 2; }, + }; + assert.equal(exports.Renaming.CounterTools.apply(transformer, 6), 12); + + made.release(); + counter.release(); +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index d07f7b3e0..977a1f386 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -19,6 +19,7 @@ import { getImports as getIntegerTypesSupportImports } from './BridgeJSRuntimeTe import { getImports as getAsyncImportImports, runAsyncWorksTests } from './BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs'; import { getImports as getJSTypedArrayImports } from './BridgeJSRuntimeTests/JavaScript/JSTypedArrayTests.mjs'; import { getImports as getIdentityModeTestImports } from './BridgeJSIdentityTests/JavaScript/IdentityModeTests.mjs'; +import { runJSNameTests } from './BridgeJSRuntimeTests/JavaScript/JSNameTests.mjs'; /** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ export async function setupOptions(options, context) { @@ -317,17 +318,7 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.roundTripUnsafeMutablePointer(p), p); } - assert.equal(exports.renamedEcho("hi"), "echo: hi"); - assert.equal(exports.jsNameEcho, undefined); - assert.equal(exports.greetName("John"), "Hello, John!"); - assert.equal(exports.greetCount(3), "Hello, 3 people!"); - const renamed = new exports.JSNameRenamedClass(21); - assert.equal(renamed.doubled(), 42); - assert.equal(renamed.current, 21); - renamed.current = 5; - assert.equal(renamed.doubled(), 10); - const madeRenamed = exports.JSNameRenamedClass.makeWithValue(7); - assert.equal(madeRenamed.current, 7); + runJSNameTests(exports); const g = new exports.Greeter("John"); assert.equal(g.greet(), "Hello, John!");