diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift index 053db447..22130271 100644 --- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift +++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift @@ -8,6 +8,6 @@ @JSFunction func createTS2Swift() throws (JSException) -> TS2Swift -@JSClass struct TS2Swift: _JSBridgedClass { +@JSClass struct TS2Swift { @JSFunction func convert(_ ts: String) throws (JSException) -> String } diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift index fc7af6cd..060d2269 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift @@ -9,6 +9,7 @@ extension JSClassMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { var members: [DeclSyntax] = [] @@ -49,3 +50,20 @@ extension JSClassMacro: MemberMacro { return members } } + +extension JSClassMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + guard !protocols.isEmpty else { return [] } + + let conformanceList = protocols.map { $0.trimmed.description }.joined(separator: ", ") + return [ + try ExtensionDeclSyntax("extension \(type.trimmed): \(raw: conformanceList) {}") + ] + } +} diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js index 830c0d0a..47a4a1ac 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js @@ -213,7 +213,7 @@ export class TypeProcessor { if (!node.name) return; const className = this.renderIdentifier(node.name.text); - this.swiftLines.push(`@JSClass struct ${className}: _JSBridgedClass {`); + this.swiftLines.push(`@JSClass struct ${className} {`); // Process members in declaration order for (const member of node.members) { @@ -268,7 +268,7 @@ export class TypeProcessor { */ visitStructuredType(name, members) { const typeName = this.renderIdentifier(name); - this.swiftLines.push(`@JSClass struct ${typeName}: _JSBridgedClass {`); + this.swiftLines.push(`@JSClass struct ${typeName} {`); // Collect all declarations with their positions to preserve order /** @type {Array<{ decl: ts.Node, symbol: ts.Symbol, position: number }>} */ diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift index 5997d305..95e3893e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift @@ -1,5 +1,6 @@ import SwiftDiagnostics import SwiftSyntax +import SwiftSyntaxMacroExpansion import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import Testing @@ -7,6 +8,9 @@ import BridgeJSMacros @Suite struct JSClassMacroTests { private let indentationWidth: Trivia = .spaces(4) + private let macroSpecs: [String: MacroSpec] = [ + "JSClass": MacroSpec(type: JSClassMacro.self, conformances: ["_JSBridgedClass"]) + ] @Test func emptyStruct() { assertMacroExpansion( @@ -23,8 +27,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -45,8 +52,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -69,8 +79,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -95,8 +108,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -119,8 +135,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -140,8 +159,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -161,8 +183,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyEnum: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -182,8 +207,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyActor: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -206,8 +234,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -232,8 +263,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -258,8 +292,11 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } @@ -281,8 +318,32 @@ import BridgeJSMacros self.jsObject = jsObject } } + + extension MyClass: _JSBridgedClass { + } + """, + macroSpecs: macroSpecs, + indentationWidth: indentationWidth + ) + } + + @Test func structAlreadyConforms() { + assertMacroExpansion( + """ + @JSClass + struct MyClass: _JSBridgedClass { + } + """, + expandedSource: """ + struct MyClass: _JSBridgedClass { + let jsObject: JSObject + + init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + } """, - macros: ["JSClass": JSClassMacro.self], + macroSpecs: macroSpecs, indentationWidth: indentationWidth ) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.Macros.swift index 1017ded7..5d13234c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.Macros.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.Macros.swift @@ -8,7 +8,7 @@ @JSFunction func returnAnimatable() throws (JSException) -> Animatable -@JSClass struct Animatable: _JSBridgedClass { +@JSClass struct Animatable { @JSFunction func animate(_ keyframes: JSObject, _ options: JSObject) throws (JSException) -> JSObject @JSFunction func getAnimations(_ options: JSObject) throws (JSException) -> JSObject } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift index ee636020..6fa9b6d8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.Macros.swift @@ -8,14 +8,14 @@ @JSFunction func createArrayBuffer() throws (JSException) -> ArrayBufferLike -@JSClass struct ArrayBufferLike: _JSBridgedClass { +@JSClass struct ArrayBufferLike { @JSGetter var byteLength: Double @JSFunction func slice(_ begin: Double, _ end: Double) throws (JSException) -> ArrayBufferLike } @JSFunction func createWeirdObject() throws (JSException) -> WeirdNaming -@JSClass struct WeirdNaming: _JSBridgedClass { +@JSClass struct WeirdNaming { @JSGetter var normalProperty: String @JSSetter func setNormalProperty(_ value: String) throws (JSException) @JSGetter var `for`: String diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/MultipleImportedTypes.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/MultipleImportedTypes.Macros.swift index 64317b79..47efcbc8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/MultipleImportedTypes.Macros.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/MultipleImportedTypes.Macros.swift @@ -8,7 +8,7 @@ @JSFunction func createDatabaseConnection(_ config: JSObject) throws (JSException) -> DatabaseConnection -@JSClass struct DatabaseConnection: _JSBridgedClass { +@JSClass struct DatabaseConnection { @JSFunction func connect(_ url: String) throws (JSException) -> Void @JSFunction func execute(_ query: String) throws (JSException) -> JSObject @JSGetter var isConnected: Bool @@ -18,7 +18,7 @@ @JSFunction func createLogger(_ level: String) throws (JSException) -> Logger -@JSClass struct Logger: _JSBridgedClass { +@JSClass struct Logger { @JSFunction func log(_ message: String) throws (JSException) -> Void @JSFunction func error(_ message: String, _ error: JSObject) throws (JSException) -> Void @JSGetter var level: String @@ -26,7 +26,7 @@ @JSFunction func getConfigManager() throws (JSException) -> ConfigManager -@JSClass struct ConfigManager: _JSBridgedClass { +@JSClass struct ConfigManager { @JSFunction func get(_ key: String) throws (JSException) -> JSObject @JSFunction func set(_ key: String, _ value: JSObject) throws (JSException) -> Void @JSGetter var configPath: String diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TS2SkeletonLike.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TS2SkeletonLike.Macros.swift index 39998492..e0d6c9d2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TS2SkeletonLike.Macros.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TS2SkeletonLike.Macros.swift @@ -8,7 +8,7 @@ @JSFunction func createTS2Skeleton() throws (JSException) -> TypeScriptProcessor -@JSClass struct TypeScriptProcessor: _JSBridgedClass { +@JSClass struct TypeScriptProcessor { @JSFunction func convert(_ ts: String) throws (JSException) -> String @JSFunction func validate(_ ts: String) throws (JSException) -> Bool @JSGetter var version: String @@ -16,7 +16,7 @@ @JSFunction func createCodeGenerator(_ format: String) throws (JSException) -> CodeGenerator -@JSClass struct CodeGenerator: _JSBridgedClass { +@JSClass struct CodeGenerator { @JSFunction func generate(_ input: JSObject) throws (JSException) -> String @JSGetter var outputFormat: String } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.Macros.swift index 4026e007..1fbf3376 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.Macros.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.Macros.swift @@ -6,7 +6,7 @@ @_spi(Experimental) import JavaScriptKit -@JSClass struct Greeter: _JSBridgedClass { +@JSClass struct Greeter { @JSGetter var name: String @JSSetter func setName(_ value: String) throws (JSException) @JSGetter var age: Double diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift index 8f5cf55a..bb23cc00 100644 --- a/Sources/JavaScriptKit/JSBridgedType.swift +++ b/Sources/JavaScriptKit/JSBridgedType.swift @@ -15,7 +15,7 @@ extension JSBridgedType { /// A protocol that Swift classes that are exposed to JavaScript via `@JS class` conform to. /// -/// The conformance is automatically synthesized by the BridgeJS code generator. +/// The conformance is automatically synthesized by `@JSClass` for BridgeJS-generated declarations. public protocol _JSBridgedClass { /// The JavaScript object wrapped by this instance. /// You may assume that `jsObject instanceof Self.constructor == true` diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index 9e11db3b..8cc3dbc6 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -174,7 +174,7 @@ public macro JSFunction() = /// @_spi(Experimental) import JavaScriptKit /// /// @JSClass -/// struct JsGreeter: _JSBridgedClass { +/// struct JsGreeter { /// @JSGetter var name: String /// @JSSetter func setName(_ value: String) throws (JSException) /// @JSFunction init(_ name: String) throws (JSException) @@ -182,6 +182,7 @@ public macro JSFunction() = /// } /// ``` @attached(member, names: arbitrary) +@attached(extension, conformances: _JSBridgedClass) @_spi(Experimental) public macro JSClass() = #externalMacro(module: "BridgeJSMacros", type: "JSClassMacro") diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift index f65f5d23..2ad11fd9 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift @@ -22,7 +22,7 @@ @JSFunction func jsThrowOrString(_ shouldThrow: Bool) throws (JSException) -> String -@JSClass struct JsGreeter: _JSBridgedClass { +@JSClass struct JsGreeter { @JSGetter var name: String @JSSetter func setName(_ value: String) throws (JSException) @JSGetter var `prefix`: String