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