@@ -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