Skip to content

Commit 53e8cc1

Browse files
authored
Merge pull request #814 from PassiveLogic/kr/export-type-names
BridgeJS: Support custom exported type names
2 parents ebc501f + 1711b35 commit 53e8cc1

23 files changed

Lines changed: 1386 additions & 228 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 76 additions & 80 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 124 additions & 76 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ final class JSGlueVariableScope {
110110
intrinsicRegistry.typeOwnerModules[typeName]
111111
}
112112

113+
func exportedClassPath(forSwiftName name: String) -> [String]? {
114+
intrinsicRegistry.classPaths[name]
115+
}
116+
117+
func renamedEnumNames(forSwiftName name: String) -> (value: String, type: String)? {
118+
intrinsicRegistry.renamedEnumNames[name]
119+
}
120+
113121
func makeChildScope() -> JSGlueVariableScope {
114122
JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry)
115123
}
@@ -532,8 +540,6 @@ struct IntrinsicJSFragment: Sendable {
532540
/// Whether the fragment has direct access to the SwiftHeapObject classes.
533541
/// If false, the fragment needs to use `_exports` to access the class.
534542
var hasDirectAccessToSwiftClass: Bool = true
535-
/// Maps class names to their namespace path components for resolving `_exports` access.
536-
var classNamespaces: [String: [String]] = [:]
537543

538544
func with<T>(_ keyPath: WritableKeyPath<PrintCodeContext, T>, _ value: T) -> PrintCodeContext {
539545
var new = self
@@ -545,20 +551,30 @@ struct IntrinsicJSFragment: Sendable {
545551
qualifiedName.split(separator: ".").last.map(String.init) ?? qualifiedName
546552
}
547553

548-
private func exportsAccess(forClass name: String) -> String {
549-
if let namespace = classNamespaces[name], !namespace.isEmpty {
550-
let path = namespace.map { ".\($0)" }.joined()
551-
return "_exports\(path).\(name)"
552-
}
553-
return "_exports['\(name)']"
554-
}
555-
556554
func classReference(forQualifiedName qualifiedName: String) -> String {
555+
if let path = scope.exportedClassPath(forSwiftName: qualifiedName), let name = path.last {
556+
if hasDirectAccessToSwiftClass {
557+
return name
558+
}
559+
return path.count == 1 ? "_exports['\(name)']" : "_exports.\(path.joined(separator: "."))"
560+
}
557561
if hasDirectAccessToSwiftClass {
558562
return unqualifiedClassName(for: qualifiedName)
559563
}
560-
let unqualified = unqualifiedClassName(for: qualifiedName)
561-
return exportsAccess(forClass: unqualified)
564+
return "_exports['\(unqualifiedClassName(for: qualifiedName))']"
565+
}
566+
567+
func defaultValueTypeName(_ type: BridgeType, _ format: DefaultValueUtils.OutputFormat) -> String? {
568+
switch type {
569+
case .swiftHeapObject(let name):
570+
guard let jsName = scope.exportedClassPath(forSwiftName: name)?.last else { return nil }
571+
return format == .javascript ? classReference(forQualifiedName: name) : jsName
572+
case .caseEnum(let name):
573+
guard let renamed = scope.renamedEnumNames(forSwiftName: name) else { return nil }
574+
return format == .javascript ? renamed.value : renamed.type
575+
default:
576+
return nil
577+
}
562578
}
563579
}
564580

@@ -2565,7 +2581,10 @@ struct IntrinsicJSFragment: Sendable {
25652581

25662582
// Attach instance methods to the struct instance
25672583
for method in structDef.methods where !method.effects.isStatic {
2568-
let paramList = DefaultValueUtils.formatParameterList(method.parameters)
2584+
let paramList = DefaultValueUtils.formatParameterList(
2585+
method.parameters,
2586+
resolveTypeName: context.defaultValueTypeName
2587+
)
25692588
printer.write(
25702589
"\(instanceVar).\(method.resolvedJSName) = function(\(paramList)) {"
25712590
)

Plugins/BridgeJS/Sources/BridgeJSLink/JSIntrinsicRegistry.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import BridgeJSUtilities
55
/// Registry for JS helper intrinsics used during code generation.
66
final class JSIntrinsicRegistry {
77
private var entries: [String: [String]] = [:]
8-
var classNamespaces: [String: [String]] = [:]
8+
var classPaths: [String: [String]] = [:]
9+
var renamedEnumNames: [String: (value: String, type: String)] = [:]
910

1011
var typeOwnerModules: [String: String] = [:]
1112

@@ -42,7 +43,8 @@ final class JSIntrinsicRegistry {
4243

4344
func reset() {
4445
entries.removeAll()
45-
classNamespaces.removeAll()
46+
classPaths.removeAll()
47+
renamedEnumNames.removeAll()
4648
typeOwnerModules.removeAll()
4749
codecNameOrder.removeAll()
4850
codecBodies.removeAll()

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44

55
public protocol NamespacedExportedType {
66
var name: String { get }
7+
var jsName: String? { get }
8+
/// The namespace using Swift declaration names, retained for ABI generation.
79
var namespace: [String]? { get }
10+
/// The public namespace when an enclosing declaration has an exported name override.
11+
var jsNamespace: [String]? { get }
812
}
913

1014
extension NamespacedExportedType {
15+
public var resolvedJSName: String { jsName ?? name }
16+
public var resolvedJSNamespace: [String]? { jsNamespace ?? namespace }
17+
1118
public var abiName: String {
1219
if let namespace = namespace, !namespace.isEmpty {
1320
return (namespace + [name]).joined(separator: "_")
@@ -16,7 +23,7 @@ extension NamespacedExportedType {
1623
}
1724

1825
public var tsPathComponents: [String] {
19-
(namespace ?? []) + [name]
26+
(resolvedJSNamespace ?? []) + [resolvedJSName]
2027
}
2128

2229
public var tsFullPath: String {
@@ -778,31 +785,37 @@ public struct StructField: Codable, Equatable, Sendable {
778785

779786
public struct ExportedStruct: Codable, Equatable, Sendable, NamespacedExportedType {
780787
public let name: String
788+
public let jsName: String?
781789
public let swiftCallName: String
782790
public let explicitAccessControl: String?
783791
public var properties: [ExportedProperty]
784792
public var constructor: ExportedConstructor?
785793
public var methods: [ExportedFunction]
786794
public let namespace: [String]?
795+
public let jsNamespace: [String]?
787796
public var documentation: String?
788797

789798
public init(
790799
name: String,
800+
jsName: String? = nil,
791801
swiftCallName: String,
792802
explicitAccessControl: String?,
793803
properties: [ExportedProperty] = [],
794804
constructor: ExportedConstructor? = nil,
795805
methods: [ExportedFunction] = [],
796806
namespace: [String]?,
807+
jsNamespace: [String]? = nil,
797808
documentation: String? = nil
798809
) {
799810
self.name = name
811+
self.jsName = jsName
800812
self.swiftCallName = swiftCallName
801813
self.explicitAccessControl = explicitAccessControl
802814
self.properties = properties
803815
self.constructor = constructor
804816
self.methods = methods
805817
self.namespace = namespace
818+
self.jsNamespace = jsNamespace
806819
self.documentation = documentation
807820
}
808821
}
@@ -864,12 +877,14 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType
864877
public static let objectSuffix = "Object"
865878

866879
public let name: String
880+
public let jsName: String?
867881
public let swiftCallName: String
868882
public let tsFullPath: String
869883
public let explicitAccessControl: String?
870884
public var cases: [EnumCase]
871885
public let rawType: SwiftEnumRawType?
872886
public let namespace: [String]?
887+
public let jsNamespace: [String]?
873888
public let emitStyle: EnumEmitStyle
874889
public var staticMethods: [ExportedFunction]
875890
public var staticProperties: [ExportedProperty] = []
@@ -885,33 +900,36 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType
885900
}
886901

887902
public var valuesName: String {
888-
emitStyle == .tsEnum ? name : "\(name)\(Self.valuesSuffix)"
903+
emitStyle == .tsEnum ? resolvedJSName : "\(resolvedJSName)\(Self.valuesSuffix)"
889904
}
890905

891906
public var objectTypeName: String {
892-
"\(name)\(Self.objectSuffix)"
907+
"\(resolvedJSName)\(Self.objectSuffix)"
893908
}
894909

895910
public init(
896911
name: String,
912+
jsName: String? = nil,
897913
swiftCallName: String,
898-
tsFullPath: String,
899914
explicitAccessControl: String?,
900915
cases: [EnumCase],
901916
rawType: SwiftEnumRawType?,
902917
namespace: [String]?,
918+
jsNamespace: [String]? = nil,
903919
emitStyle: EnumEmitStyle,
904920
staticMethods: [ExportedFunction] = [],
905921
staticProperties: [ExportedProperty] = [],
906922
documentation: String? = nil
907923
) {
908924
self.name = name
925+
self.jsName = jsName
909926
self.swiftCallName = swiftCallName
910-
self.tsFullPath = tsFullPath
927+
self.tsFullPath = ((jsNamespace ?? namespace ?? []) + [jsName ?? name]).joined(separator: ".")
911928
self.explicitAccessControl = explicitAccessControl
912929
self.cases = cases
913930
self.rawType = rawType
914931
self.namespace = namespace
932+
self.jsNamespace = jsNamespace
915933
self.emitStyle = emitStyle
916934
self.staticMethods = staticMethods
917935
self.staticProperties = staticProperties
@@ -947,24 +965,30 @@ public struct ExportedProtocolProperty: Codable, Equatable, Sendable {
947965
}
948966
}
949967

950-
public struct ExportedProtocol: Codable, Equatable {
968+
public struct ExportedProtocol: Codable, Equatable, NamespacedExportedType {
951969
public let name: String
970+
public let jsName: String?
952971
public let methods: [ExportedFunction]
953972
public let properties: [ExportedProtocolProperty]
954973
public let namespace: [String]?
974+
public let jsNamespace: [String]?
955975
public var documentation: String?
956976

957977
public init(
958978
name: String,
979+
jsName: String? = nil,
959980
methods: [ExportedFunction],
960981
properties: [ExportedProtocolProperty] = [],
961982
namespace: [String]? = nil,
983+
jsNamespace: [String]? = nil,
962984
documentation: String? = nil
963985
) {
964986
self.name = name
987+
self.jsName = jsName
965988
self.methods = methods
966989
self.properties = properties
967990
self.namespace = namespace
991+
self.jsNamespace = jsNamespace
968992
self.documentation = documentation
969993
}
970994
}
@@ -1007,35 +1031,41 @@ public struct ExportedFunction: Codable, Equatable, Sendable {
10071031

10081032
public struct ExportedClass: Codable, NamespacedExportedType {
10091033
public var name: String
1034+
public var jsName: String?
10101035
public var swiftCallName: String
10111036
public var explicitAccessControl: String?
10121037
public var constructor: ExportedConstructor?
10131038
public var methods: [ExportedFunction]
10141039
public var properties: [ExportedProperty]
10151040
public var namespace: [String]?
1041+
public var jsNamespace: [String]?
10161042
public var identityMode: Bool? // nil = use config default, true/false = override
10171043
public var documentation: String?
10181044
public var isFinal: Bool?
10191045

10201046
public init(
10211047
name: String,
1048+
jsName: String? = nil,
10221049
swiftCallName: String,
10231050
explicitAccessControl: String?,
10241051
constructor: ExportedConstructor? = nil,
10251052
methods: [ExportedFunction],
10261053
properties: [ExportedProperty] = [],
10271054
namespace: [String]? = nil,
1055+
jsNamespace: [String]? = nil,
10281056
identityMode: Bool? = nil,
10291057
documentation: String? = nil,
10301058
isFinal: Bool? = nil
10311059
) {
10321060
self.name = name
1061+
self.jsName = jsName
10331062
self.swiftCallName = swiftCallName
10341063
self.explicitAccessControl = explicitAccessControl
10351064
self.constructor = constructor
10361065
self.methods = methods
10371066
self.properties = properties
10381067
self.namespace = namespace
1068+
self.jsNamespace = jsNamespace
10391069
self.identityMode = identityMode
10401070
self.documentation = documentation
10411071
self.isFinal = isFinal

Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -696,42 +696,6 @@ import Testing
696696
}
697697
}
698698

699-
@Test
700-
func jsNameOnClassDiagnostic() throws {
701-
let source = """
702-
@JS("Renamed") class Box { @JS init() {} }
703-
"""
704-
let diagnostics = try #require(moduleDiagnostics(source: source))
705-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
706-
}
707-
708-
@Test
709-
func jsNameOnStructDiagnostic() throws {
710-
let source = """
711-
@JS("Renamed") struct Box { var x: Int }
712-
"""
713-
let diagnostics = try #require(moduleDiagnostics(source: source))
714-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
715-
}
716-
717-
@Test
718-
func jsNameOnEnumDiagnostic() throws {
719-
let source = """
720-
@JS("Renamed") enum Box { case a }
721-
"""
722-
let diagnostics = try #require(moduleDiagnostics(source: source))
723-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
724-
}
725-
726-
@Test
727-
func jsNameOnProtocolDiagnostic() throws {
728-
let source = """
729-
@JS("Renamed") protocol Box { func run() }
730-
"""
731-
let diagnostics = try #require(moduleDiagnostics(source: source))
732-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
733-
}
734-
735699
@Test
736700
func jsNameOnInitializerDiagnostic() throws {
737701
let source = """
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
import SwiftParser
3+
import Testing
4+
5+
@testable import BridgeJSCore
6+
@testable import BridgeJSSkeleton
7+
8+
@Suite struct ExportedTypeNameTests {
9+
private func parse(_ source: String) throws -> ExportedSkeleton {
10+
let generator = SwiftToSkeleton(
11+
progress: .silent,
12+
moduleName: "TestModule",
13+
exposeToGlobal: false,
14+
externalModuleIndex: .empty
15+
)
16+
generator.addSourceFile(Parser.parse(source: source), inputFilePath: "Types.swift")
17+
return try #require(generator.finalize().exported)
18+
}
19+
20+
@Test
21+
func renamedTypesKeepSwiftIdentities() throws {
22+
let exported = try parse(
23+
"""
24+
@JS func box() -> SwiftBox
25+
@JS func record() -> SwiftRecord
26+
@JS func choice() -> SwiftChoice
27+
@JS func delegate(_ value: SwiftDelegate)
28+
@JS("PublicBox") class SwiftBox { @JS init() {} }
29+
@JS("PublicRecord") struct SwiftRecord { var value: Int }
30+
@JS("PublicChoice") enum SwiftChoice { case first }
31+
@JS("PublicDelegate") protocol SwiftDelegate { func run() }
32+
"""
33+
)
34+
#expect(exported.classes.first?.resolvedJSName == "PublicBox")
35+
#expect(exported.structs.first?.resolvedJSName == "PublicRecord")
36+
#expect(exported.enums.first?.resolvedJSName == "PublicChoice")
37+
#expect(exported.protocols.first?.resolvedJSName == "PublicDelegate")
38+
#expect(exported.functions[0].returnType == .swiftHeapObject("SwiftBox"))
39+
#expect(exported.functions[1].returnType == .swiftStruct("SwiftRecord"))
40+
#expect(exported.functions[2].returnType == .caseEnum("SwiftChoice"))
41+
#expect(exported.functions[3].parameters.first?.type == .swiftProtocol("SwiftDelegate"))
42+
}
43+
44+
@Test
45+
func renamedParentsKeepSwiftThunkABI() throws {
46+
let source = """
47+
@JS(namespace: "API") enum InternalAPI {
48+
@JS class Item { @JS init() {} }
49+
}
50+
"""
51+
let original = try parse(source)
52+
let renamed = try parse(
53+
source.replacingOccurrences(of: "@JS(namespace:", with: "@JS(\"PublicAPI\", namespace:")
54+
)
55+
let originalThunks = try ExportSwift(progress: .silent, moduleName: "TestModule", skeleton: original).finalize()
56+
let renamedThunks = try ExportSwift(progress: .silent, moduleName: "TestModule", skeleton: renamed).finalize()
57+
#expect(renamedThunks == originalThunks)
58+
#expect(renamed.classes.first?.tsFullPath == "API.PublicAPI.Item")
59+
}
60+
}

0 commit comments

Comments
 (0)