@@ -366,6 +366,11 @@ public final class SwiftToSkeleton {
366366 if structDecl . attributes . hasAttribute ( name : " JSClass " ) {
367367 return . jsObject ( swiftCallName )
368368 }
369+ if let jsAttribute = structDecl . attributes . firstJSAttribute ,
370+ SwiftToSkeleton . extractStructStyle ( from : jsAttribute ) == . reference
371+ {
372+ return . swiftBoxedStruct ( swiftCallName )
373+ }
369374 return . swiftStruct ( swiftCallName )
370375 }
371376
@@ -523,6 +528,22 @@ public final class SwiftToSkeleton {
523528 }
524529 }
525530
531+ static func extractStructStyle ( from jsAttribute : AttributeSyntax ) -> JSStructStyle ? {
532+ guard let arguments = jsAttribute . arguments ? . as ( LabeledExprListSyntax . self ) ,
533+ let styleArg = arguments . first ( where : { $0 . label ? . text == " structStyle " } )
534+ else {
535+ return nil
536+ }
537+ let text = styleArg . expression . trimmedDescription
538+ if text . contains ( " reference " ) {
539+ return . reference
540+ }
541+ if text . contains ( " fields " ) {
542+ return . fields
543+ }
544+ return nil
545+ }
546+
526547 /// Strips surrounding backticks from an identifier (e.g. "`Foo`" -> "Foo").
527548 static func normalizeIdentifier ( _ name : String ) -> String {
528549 guard name . hasPrefix ( " ` " ) , name . hasSuffix ( " ` " ) , name . count >= 2 else {
@@ -1397,8 +1418,17 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
13971418 let structAbiName = exportedStructByName [ structKey ] ? . abiName ?? " unknown "
13981419 staticContext = . structName ( structAbiName )
13991420 } else {
1400- diagnose ( node : node , message : " @JS var must be static in structs (instance fields don't need @JS) " )
1401- return . skipChildren
1421+ switch exportedStructByName [ structKey ] ? . structStyle ?? . default {
1422+ case . reference :
1423+ staticContext = nil
1424+ case . fields :
1425+ diagnose (
1426+ node : node ,
1427+ message : " @JS var must be static in structs (instance fields don't need @JS) " ,
1428+ hint : " Export the struct using @JS(structStyle: .reference) instead "
1429+ )
1430+ return . skipChildren
1431+ }
14021432 }
14031433 }
14041434
@@ -1661,12 +1691,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16611691 for associatedValue in enumCase . associatedValues {
16621692 switch associatedValue . type {
16631693 case . string , . integer , . float , . double , . bool , . caseEnum , . rawValueEnum ,
1664- . swiftStruct , . swiftHeapObject , . jsObject , . associatedValueEnum , . array :
1694+ . swiftStruct , . swiftHeapObject , . swiftBoxedStruct , . jsObject , . associatedValueEnum , . array :
16651695 break
16661696 case . nullable ( let wrappedType , _) :
16671697 switch wrappedType {
16681698 case . string , . integer , . float , . double , . bool , . caseEnum , . rawValueEnum ,
1669- . swiftStruct , . swiftHeapObject , . jsObject , . associatedValueEnum , . array :
1699+ . swiftStruct , . swiftHeapObject , . swiftBoxedStruct , . jsObject , . associatedValueEnum , . array :
16701700 break
16711701 default :
16721702 diagnose (
@@ -1772,54 +1802,61 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
17721802 for : node ,
17731803 message : " Struct visibility must be at least internal "
17741804 )
1805+ let structStyle = SwiftToSkeleton . extractStructStyle ( from : jsAttribute )
17751806
17761807 var properties : [ ExportedProperty ] = [ ]
17771808
1778- // Process all variables in struct as readonly (value semantics) and don't require @JS
1779- for member in node . memberBlock . members {
1780- if let varDecl = member . decl . as ( VariableDeclSyntax . self ) {
1781- let isStatic = varDecl . modifiers . contains { modifier in
1782- modifier . name . tokenKind == . keyword ( . static ) || modifier . name . tokenKind == . keyword ( . class )
1783- }
1784-
1785- // Handled with error in visitVariable
1786- if varDecl . attributes . hasJSAttribute ( ) {
1787- continue
1788- }
1789- // Skips static non-@JS properties
1790- if isStatic {
1791- continue
1792- }
1793-
1794- for binding in varDecl . bindings {
1795- guard let pattern = binding . pattern . as ( IdentifierPatternSyntax . self ) else {
1796- continue
1809+ switch structStyle ?? . default {
1810+ case . reference :
1811+ // Reference structs require @JS on variables to export
1812+ break
1813+ case . fields :
1814+ // Process all variables in struct as readonly (value semantics) and don't require @JS
1815+ for member in node . memberBlock . members {
1816+ if let varDecl = member . decl . as ( VariableDeclSyntax . self ) {
1817+ let isStatic = varDecl . modifiers . contains { modifier in
1818+ modifier . name . tokenKind == . keyword ( . static ) || modifier . name . tokenKind == . keyword ( . class )
17971819 }
17981820
1799- let fieldName = pattern . identifier . text
1800-
1801- guard let typeAnnotation = binding . typeAnnotation else {
1802- diagnose ( node : binding , message : " Struct field must have explicit type annotation " )
1821+ // Handled with error in visitVariable
1822+ if varDecl . attributes . hasJSAttribute ( ) {
18031823 continue
18041824 }
1805-
1806- guard
1807- let fieldType = withLookupErrors ( {
1808- self . parent . lookupType ( for : typeAnnotation . type , errors : & $0 )
1809- } )
1810- else {
1825+ // Skips static non-@JS properties
1826+ if isStatic {
18111827 continue
18121828 }
18131829
1814- let property = ExportedProperty (
1815- name : fieldName ,
1816- type : fieldType ,
1817- isReadonly : true ,
1818- isStatic : false ,
1819- namespace : effectiveNamespace ,
1820- staticContext : nil
1821- )
1822- properties . append ( property )
1830+ for binding in varDecl . bindings {
1831+ guard let pattern = binding . pattern . as ( IdentifierPatternSyntax . self ) else {
1832+ continue
1833+ }
1834+
1835+ let fieldName = pattern . identifier . text
1836+
1837+ guard let typeAnnotation = binding . typeAnnotation else {
1838+ diagnose ( node : binding , message : " Struct field must have explicit type annotation " )
1839+ continue
1840+ }
1841+
1842+ guard
1843+ let fieldType = withLookupErrors ( {
1844+ self . parent . lookupType ( for : typeAnnotation . type , errors : & $0 )
1845+ } )
1846+ else {
1847+ continue
1848+ }
1849+
1850+ let property = ExportedProperty (
1851+ name : fieldName ,
1852+ type : fieldType ,
1853+ isReadonly : true ,
1854+ isStatic : false ,
1855+ namespace : effectiveNamespace ,
1856+ staticContext : nil
1857+ )
1858+ properties . append ( property )
1859+ }
18231860 }
18241861 }
18251862 }
@@ -1831,7 +1868,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18311868 explicitAccessControl : explicitAccessControl ,
18321869 properties : properties ,
18331870 methods : [ ] ,
1834- namespace : effectiveNamespace
1871+ namespace : effectiveNamespace ,
1872+ structStyle : structStyle
18351873 )
18361874
18371875 exportedStructByName [ structUniqueKey ] = exportedStruct
@@ -1858,6 +1896,11 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18581896 return
18591897 }
18601898
1899+ guard exportedStruct . structStyle ?? . default == . fields else {
1900+ // This validation is only required for fields structs.
1901+ return
1902+ }
1903+
18611904 let instanceProps = exportedStruct . properties . filter { ! $0 . isStatic }
18621905 let expectedLabels = instanceProps . map ( \. name )
18631906 let actualLabels = constructor . parameters . compactMap ( \. label )
0 commit comments