Skip to content

Commit 511e46a

Browse files
committed
BridgeJS: Support custom exported type names
1 parent ebc501f commit 511e46a

11 files changed

Lines changed: 409 additions & 180 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

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,48 @@ export type Exports = {
7777
}
7878
```
7979
80+
## Choosing an Exported Name
81+
82+
Pass a name to `@JS` to expose a class under a different JavaScript and TypeScript name while keeping its Swift identifier:
83+
84+
```swift
85+
@JS("Counter") class InternalCounter {
86+
@JS var value: Int
87+
88+
@JS init(value: Int) {
89+
self.value = value
90+
}
91+
92+
@JS static func make(_ value: Int) -> InternalCounter {
93+
InternalCounter(value: value)
94+
}
95+
96+
@JS func roundTrip(_ other: InternalCounter?) -> InternalCounter? {
97+
other
98+
}
99+
}
100+
```
101+
102+
After initializing the package, use the constructor and static factory on that WASM instance's `exports` object:
103+
104+
```javascript
105+
const counter = new exports.Counter(1);
106+
const other = exports.Counter.make(2);
107+
const returned = counter.roundTrip(other);
108+
console.log(returned.value); // 2
109+
returned.release();
110+
other.release();
111+
counter.release();
112+
```
113+
114+
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.
115+
116+
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.
117+
118+
This naming option also applies to structs, enums, and protocols. For namespace composition and nested types, see <doc:Using-Namespace>.
119+
120+
> 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.
121+
80122
## Adding Members via Extensions
81123

82124
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:

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,35 @@ BridgeJS generates separate objects with descriptive naming for `.const` enums:
1919
- **`EnumNameTag`**: Represents the union type for enums
2020
- **`EnumNameObject`**: Object type for all const-style enums, contains static members for enums with methods/properties or references the values type for simple enums
2121

22+
### Choosing an Exported Name
23+
24+
A name passed to `@JS` changes the generated naming stem, not the Swift enum or its representation:
25+
26+
```swift
27+
@JS("TaskState") enum InternalTaskState: String {
28+
case pending
29+
case complete
30+
}
31+
32+
@JS func roundTripState(_ state: InternalTaskState) -> InternalTaskState {
33+
state
34+
}
35+
```
36+
37+
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`:
38+
39+
```typescript
40+
import { TaskStateValues } from "./bridge-js.js";
41+
import type { TaskStateTag } from "./bridge-js.js";
42+
43+
const state: TaskStateTag = exports.roundTripState(TaskStateValues.Pending);
44+
console.log(state === exports.TaskState.Pending); // true
45+
```
46+
47+
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.
48+
49+
For empty enums used as namespaces, the override names the namespace object. Nested declarations use that renamed parent in their export paths; see <doc:Using-Namespace>.
50+
2251
### Case Enums
2352

2453
```swift

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ When you mark a protocol with `@JS`, BridgeJS generates:
1313
- A TypeScript interface with the protocol's method signatures
1414
- A Swift wrapper struct (`Any{ProtocolName}`) that conforms to the protocol and bridges calls to JavaScript objects
1515

16+
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.
17+
1618
## Example: Counter Protocol
1719

1820
Mark a Swift protocol with `@JS` to expose it:

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ Learn how to export Swift structs to JavaScript.
66

77
> 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/).
88
9-
To export a Swift struct, mark it with `@JS`:
9+
To export a Swift struct, mark it with `@JS`.
10+
11+
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 <doc:Exporting-Swift-Class> for an example of naming an exported type.
1012

1113
```swift
1214
import JavaScriptKit

0 commit comments

Comments
 (0)