22import BridgeJSSkeleton
33#endif
44
5+ import Foundation
6+
57final class ImportedJSModuleRegistry {
6- struct Reference : Hashable {
8+ /// A JavaScript module that imported declarations are read from.
9+ ///
10+ /// A `snippet` reference is a file inside a Swift target, which packaging copies
11+ /// into the generated output. A `module` reference is a bare specifier resolved by
12+ /// the JavaScript host (a bundler, an import map, or Node's `node_modules` lookup),
13+ /// so it has no file and nothing to copy. Because a bare specifier names the same
14+ /// module no matter which Swift module mentions it — and ECMAScript caches module
15+ /// instances — it is keyed by specifier alone and shared across targets.
16+ enum Reference : Hashable {
17+ case snippet ( swiftModuleName : String , path : String )
18+ case module ( specifier : String )
19+ }
20+
21+ /// A JavaScript file shipped in a Swift target that packaging must copy into the output.
22+ struct SnippetFile : Hashable {
723 let swiftModuleName : String
824 let path : String
925
@@ -12,56 +28,211 @@ final class ImportedJSModuleRegistry {
1228 }
1329 }
1430
15- private var aliases : [ Reference : String ] = [ : ]
31+ private struct Binding {
32+ let index : Int
33+ /// Member names looked up on this module, sorted for stable output.
34+ let members : [ String ]
35+ /// Whether every member name is a valid JavaScript identifier, and so can be
36+ /// reached with a named import instead of a namespace property lookup.
37+ let usesNamedImports : Bool
38+ }
39+
40+ private var bindings : [ Reference : Binding ] = [ : ]
1641 private( set) var references : [ Reference ] = [ ]
1742
43+ /// The snippet files packaging must copy, in deterministic order.
44+ var snippetFiles : [ SnippetFile ] {
45+ references . compactMap { reference in
46+ guard case . snippet ( let swiftModuleName , let path ) = reference else { return nil }
47+ return SnippetFile ( swiftModuleName : swiftModuleName , path : path )
48+ }
49+ }
50+
1851 func configure ( skeletons : [ BridgeJSSkeleton ] ) {
19- aliases . removeAll ( keepingCapacity : true )
52+ bindings . removeAll ( keepingCapacity : true )
2053 references = Self . collectReferences ( skeletons : skeletons )
54+
55+ var membersByReference : [ Reference : Set < String > ] = [ : ]
56+ for skeleton in skeletons {
57+ Self . forEachMemberLookup ( skeleton : skeleton ) { reference , memberName in
58+ membersByReference [ reference , default : [ ] ] . insert ( memberName )
59+ }
60+ }
61+
2162 for ( index , reference ) in references . enumerated ( ) {
22- aliases [ reference ] = " __bjs_imported_module_ \( index ) "
63+ let members = ( membersByReference [ reference ] ?? [ ] ) . sorted ( )
64+ // A reference with no member lookups keeps the namespace form: a named import
65+ // is a hard link-time requirement, so importing a name nothing references
66+ // would fail the whole module load if the module does not export it.
67+ bindings [ reference ] = Binding (
68+ index : index ,
69+ members : members ,
70+ usesNamedImports : ! members . isEmpty && members . allSatisfy ( Self . isValidJSIdentifier )
71+ )
2372 }
2473 }
2574
2675 static func collectReferences ( skeletons : [ BridgeJSSkeleton ] ) -> [ Reference ] {
2776 var references = Set < Reference > ( )
2877 for skeleton in skeletons {
29- for file in skeleton . imported ? . children ?? [ ] {
30- let origins =
31- file . functions . compactMap ( \. from )
32- + file . globalGetters . compactMap ( \. from )
33- + file . types . compactMap ( \. from )
34- for case . module ( let path ) in origins {
35- references . insert ( Reference ( swiftModuleName : skeleton . moduleName , path : path ) )
36- }
78+ forEachOrigin ( skeleton : skeleton ) { reference in
79+ references . insert ( reference )
3780 }
3881 }
39- return references . sorted {
40- ( $0 . swiftModuleName , $0 . path ) < ( $1 . swiftModuleName , $1 . path )
82+ return references . sorted ( by : isOrderedBefore )
83+ }
84+
85+ static func collectSnippetFiles ( skeletons : [ BridgeJSSkeleton ] ) -> [ SnippetFile ] {
86+ collectReferences ( skeletons : skeletons ) . compactMap { reference in
87+ guard case . snippet ( let swiftModuleName , let path ) = reference else { return nil }
88+ return SnippetFile ( swiftModuleName : swiftModuleName , path : path )
4189 }
4290 }
4391
44- func namespaceExpression ( swiftModuleName : String , from : JSImportFrom ? ) throws -> String {
92+ /// Visits every module origin mentioned by the skeleton, whether or not code
93+ /// generation looks a member up on it.
94+ ///
95+ /// This is what decides which modules are imported at all, and for snippets which
96+ /// files packaging copies. It stays broader than `forEachMemberLookup` so
97+ /// that a module mentioned only by a wrapper-only `@JSClass` is still imported,
98+ /// preserving its side effects.
99+ private static func forEachOrigin (
100+ skeleton : BridgeJSSkeleton ,
101+ _ body : ( Reference ) -> Void
102+ ) {
103+ func visit ( from : JSImportFrom ? ) {
104+ guard let reference = Self . reference ( swiftModuleName : skeleton . moduleName , from : from ) else { return }
105+ body ( reference )
106+ }
107+ for file in skeleton . imported ? . children ?? [ ] {
108+ for function in file . functions { visit ( from : function . from ) }
109+ for getter in file . globalGetters { visit ( from : getter . from ) }
110+ for type in file . types { visit ( from : type . from ) }
111+ }
112+ }
113+
114+ /// Visits every module member lookup that code generation will emit.
115+ ///
116+ /// The member name taken here must match what the corresponding emitter in
117+ /// `BridgeJSLink` looks up, and must not include names it never emits: a named
118+ /// import is a hard link-time requirement, so recording a member that no
119+ /// generated code references would make the module fail to load whenever the
120+ /// module does not happen to export that name.
121+ ///
122+ /// A class contributes a single binding that serves both its constructor and its
123+ /// static methods, and only when it has one of those. Instance methods, getters,
124+ /// and setters contribute nothing because they go through an already-constructed
125+ /// instance, so a wrapper-only `@JSClass` needs no export from the module at all.
126+ private static func forEachMemberLookup (
127+ skeleton : BridgeJSSkeleton ,
128+ _ body : ( Reference , String ) -> Void
129+ ) {
130+ func visit ( from : JSImportFrom ? , memberName : String ) {
131+ guard let reference = Self . reference ( swiftModuleName : skeleton . moduleName , from : from ) else { return }
132+ body ( reference , memberName )
133+ }
134+ for file in skeleton . imported ? . children ?? [ ] {
135+ for function in file . functions {
136+ visit ( from : function . from , memberName : function . jsName ?? function . name )
137+ }
138+ for getter in file . globalGetters {
139+ visit ( from : getter . from , memberName : getter . jsName ?? getter . name )
140+ }
141+ for type in file . types {
142+ guard type . constructor != nil || ! type . staticMethods . isEmpty else { continue }
143+ visit ( from : type . from , memberName : type . jsName ?? type . name )
144+ }
145+ }
146+ }
147+
148+ private static func reference ( swiftModuleName : String , from : JSImportFrom ? ) -> Reference ? {
149+ switch from {
150+ case . snippet ( let path ) :
151+ return . snippet ( swiftModuleName : swiftModuleName , path : path )
152+ case . module ( let specifier ) :
153+ return . module ( specifier : specifier )
154+ case . global , nil :
155+ return nil
156+ }
157+ }
158+
159+ private static func isOrderedBefore ( _ lhs : Reference , _ rhs : Reference ) -> Bool {
160+ switch ( lhs , rhs ) {
161+ case ( . snippet ( let lhsModule , let lhsPath ) , . snippet ( let rhsModule , let rhsPath ) ) :
162+ return ( lhsModule , lhsPath ) < ( rhsModule , rhsPath )
163+ case ( . module ( let lhsSpecifier ) , . module ( let rhsSpecifier ) ) :
164+ return lhsSpecifier < rhsSpecifier
165+ case ( . snippet , . module ) :
166+ return true
167+ case ( . module , . snippet ) :
168+ return false
169+ }
170+ }
171+
172+ /// Whether `name` can appear as a bare identifier in generated JavaScript.
173+ static func isValidJSIdentifier ( _ name : String ) -> Bool {
174+ name . range ( of : #"^[$A-Z_][0-9A-Z_$]*$"# , options : [ . regularExpression , . caseInsensitive ] ) != nil
175+ }
176+
177+ /// Returns the JavaScript expression that evaluates to `memberName` of the given origin.
178+ func memberExpression (
179+ swiftModuleName : String ,
180+ from : JSImportFrom ? ,
181+ memberName : String
182+ ) throws -> String {
45183 switch from {
46184 case nil :
47- return " imports "
185+ return BridgeJSLink . ImportedThunkBuilder . propertyAccessExpr ( objectExpr : " imports " , propertyName : memberName )
48186 case . global :
49- return " globalThis "
50- case . module ( let path ) :
51- let reference = Reference ( swiftModuleName : swiftModuleName , path : path )
52- guard let alias = aliases [ reference ] else {
187+ return BridgeJSLink . ImportedThunkBuilder . propertyAccessExpr (
188+ objectExpr : " globalThis " ,
189+ propertyName : memberName
190+ )
191+ case . snippet , . module :
192+ guard let reference = Self . reference ( swiftModuleName : swiftModuleName , from : from ) ,
193+ let binding = bindings [ reference ]
194+ else {
53195 throw BridgeJSLinkError (
54- message : " Missing JavaScript module \( swiftModuleName ) \( path ) "
196+ message :
197+ " Missing JavaScript module \( swiftModuleName ) \( from ? . snippetPath ?? from ? . moduleSpecifier ?? " " ) "
55198 )
56199 }
57- return alias
200+ if binding . usesNamedImports {
201+ return Self . namedImportBinding ( index : binding . index , memberName : memberName )
202+ }
203+ return BridgeJSLink . ImportedThunkBuilder . propertyAccessExpr (
204+ objectExpr : Self . namespaceAlias ( index : binding . index ) ,
205+ propertyName : memberName
206+ )
58207 }
59208 }
60209
210+ private static func namespaceAlias ( index : Int ) -> String {
211+ " __bjs_imported_module_ \( index ) "
212+ }
213+
214+ private static func namedImportBinding ( index : Int , memberName : String ) -> String {
215+ " __bjs_import_ \( index ) _ \( memberName ) "
216+ }
217+
61218 var importLines : [ String ] {
62- references . enumerated ( ) . map { index , reference in
63- let path = BridgeJSLink . escapeForJavaScriptStringLiteral ( reference . relativeOutputPath )
64- return " import * as __bjs_imported_module_ \( index ) from \" ./ \( path ) \" ; "
219+ references . compactMap { reference in
220+ guard let binding = bindings [ reference ] else { return nil }
221+ let specifier : String
222+ switch reference {
223+ case . snippet ( let swiftModuleName , let path ) :
224+ let output = SnippetFile ( swiftModuleName : swiftModuleName , path : path ) . relativeOutputPath
225+ specifier = " ./ " + BridgeJSLink . escapeForJavaScriptStringLiteral ( output )
226+ case . module ( let moduleSpecifier ) :
227+ specifier = BridgeJSLink . escapeForJavaScriptStringLiteral ( moduleSpecifier )
228+ }
229+ guard binding . usesNamedImports else {
230+ return " import * as \( Self . namespaceAlias ( index : binding . index ) ) from \" \( specifier ) \" ; "
231+ }
232+ let clauses = binding . members . map {
233+ " \( $0 ) as \( Self . namedImportBinding ( index : binding . index , memberName : $0 ) ) "
234+ }
235+ return " import { \( clauses . joined ( separator : " , " ) ) } from \" \( specifier ) \" ; "
65236 }
66237 }
67238}
0 commit comments