@@ -139,6 +139,124 @@ import Testing
139139 try snapshot ( bridgeJSLink: bridgeJSLink, name: " MixedModules " )
140140 }
141141
142+ private func linkedJS( forFixture input: String ) throws -> String {
143+ let url = Self . inputsDirectory. appendingPathComponent ( input)
144+ let name = url. deletingPathExtension ( ) . lastPathComponent
145+ let sourceFile = Parser . parse ( source: try String ( contentsOf: url, encoding: . utf8) )
146+ let importSwift = SwiftToSkeleton (
147+ progress: . silent,
148+ moduleName: " TestModule " ,
149+ exposeToGlobal: false ,
150+ externalModuleIndex: . empty
151+ )
152+ importSwift. addSourceFile ( sourceFile, inputFilePath: " \( name) .swift " )
153+ let importResult = try importSwift. finalize ( )
154+ var bridgeJSLink = BridgeJSLink ( sharedMemory: false )
155+ let encoder = JSONEncoder ( )
156+ encoder. outputFormatting = [ . prettyPrinted, . sortedKeys]
157+ let unifiedData = try encoder. encode ( importResult)
158+ try bridgeJSLink. addSkeletonFile ( data: unifiedData)
159+ return try bridgeJSLink. link ( ) . 0
160+ }
161+
162+ @Test
163+ func genericRuntimeIsGatedToGenericBuilds( ) throws {
164+ let genericJS = try linkedJS ( forFixture: " GenericImports.swift " )
165+ #expect( genericJS. contains ( " __bjs_codecByTypeId " ) )
166+ #expect( genericJS. contains ( " __bjs_primitiveCodecs " ) )
167+ #expect( genericJS. contains ( " bjs[ \" bjs_TestModule_register_type_handles \" ] = function(base, count) { " ) )
168+ #expect( genericJS. contains ( " instance.exports[ \" bjs_TestModule_register_type_handles \" ](); " ) )
169+ #expect( genericJS. contains ( " function __bjs_codecForTypeId(typeId) { \n __bjs_registerTypeHandles(); " ) )
170+ #expect( !genericJS. contains ( " afterInitialize: () => { " ) )
171+ #expect( !genericJS. contains ( " type handle registration mismatch " ) )
172+
173+ // Modules with @JS types but no generic declarations still emit a Swift
174+ // registration export (their types may be used by a dependent module's
175+ // generic function), so the link layer must install a no-op hook for the
176+ // wasm import — but the generic runtime itself must be omitted. The
177+ // shared codec runtime (combinators + primitive codecs) gates
178+ // independently: it is emitted because the fixture bridges an optional
179+ // struct through the container stack ABI.
180+ let nonGenericJS = try linkedJS ( forFixture: " SwiftStructImports.swift " )
181+ #expect( !nonGenericJS. contains ( " __bjs_codecByTypeId " ) )
182+ #expect( nonGenericJS. contains ( " __bjs_optionalCodec(structHelpers.Point) " ) )
183+ #expect( nonGenericJS. contains ( " __bjs_primitiveCodecs " ) )
184+ #expect( nonGenericJS. contains ( " bjs[ \" bjs_TestModule_register_type_handles \" ] = function() {}; " ) )
185+ #expect( !nonGenericJS. contains ( " instance.exports[ \" bjs_TestModule_register_type_handles \" ](); " ) )
186+ #expect( !nonGenericJS. contains ( " afterInitialize: () => { " ) )
187+
188+ // Builds that bridge no containers at all pay nothing for the shared
189+ // codec runtime either.
190+ let containerFreeJS = try linkedJS ( forFixture: " PrimitiveParameters.swift " )
191+ #expect( !containerFreeJS. contains ( " __bjs_arrayCodec " ) )
192+ #expect( !containerFreeJS. contains ( " __bjs_optionalCodec " ) )
193+ #expect( !containerFreeJS. contains ( " __bjs_dictCodec " ) )
194+ #expect( !containerFreeJS. contains ( " __bjs_primitiveCodecs " ) )
195+ #expect( !containerFreeJS. contains ( " __bjs_stringCodec " ) )
196+ }
197+
198+ @Test
199+ func sameTypeNameAcrossModulesLinksWithHandleIdentity( ) throws {
200+ // Type identity is pointer-based (each type owns a BridgeJSTypeHandle),
201+ // so two modules defining a same-named @JS type must link fine: each
202+ // module registers its own handle IDs against its own codec array.
203+ let structSource = """
204+ @JS public struct Point {
205+ public var x: Int
206+ @JS public init(x: Int) { self.x = x }
207+ }
208+ """
209+ let first = try makeSkeleton (
210+ structSource + """
211+
212+ @JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
213+ """ ,
214+ moduleName: " FirstModule "
215+ )
216+ let second = try makeSkeleton ( structSource, moduleName: " SecondModule " )
217+ let bridgeJSLink = BridgeJSLink ( skeletons: [ first, second] , sharedMemory: false )
218+ let js = try bridgeJSLink. link ( ) . outputJs
219+ #expect( js. contains ( " bjs[ \" bjs_FirstModule_register_type_handles \" ] = function(base, count) { " ) )
220+ #expect( js. contains ( " bjs[ \" bjs_SecondModule_register_type_handles \" ] = function(base, count) { " ) )
221+ }
222+
223+ @Test
224+ func moduleWithoutGenericsStillRegistersItsTypeCodecs( ) throws {
225+ // A module cannot know whether a dependent module will pass its types to
226+ // a generic function, so a module with @JS types but no generic
227+ // declaration of its own still registers a codec for each of them, and
228+ // the linked glue drives every module's registration export. This is
229+ // what lets a type defined in Core be the generic argument of a generic
230+ // import declared in App.
231+ let core = try makeSkeleton (
232+ """
233+ @JS public struct Vector3D {
234+ public var x: Int
235+ @JS public init(x: Int) { self.x = x }
236+ }
237+ """ ,
238+ moduleName: " Core "
239+ )
240+ let app = try makeSkeleton (
241+ """
242+ @JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
243+ """ ,
244+ moduleName: " App "
245+ )
246+
247+ // Core declares nothing imported at all, yet still registers its types.
248+ #expect( core. imported == nil )
249+ let coreEntries = try #require( core. typeRegistrationEntries)
250+ #expect( coreEntries. contains { $0. swiftName == " Vector3D " } )
251+
252+ let js = try BridgeJSLink ( skeletons: [ core, app] , sharedMemory: false ) . link ( ) . outputJs
253+ #expect( js. contains ( " instance.exports[ \" bjs_Core_register_type_handles \" ](); " ) )
254+ #expect( js. contains ( " instance.exports[ \" bjs_App_register_type_handles \" ](); " ) )
255+ // `lower(v)` is unique to a codec literal in a registration array;
256+ // `structHelpers.Vector3D` on its own is emitted for every @JS struct.
257+ #expect( js. contains ( " structHelpers.Vector3D.lower(v); " ) )
258+ }
259+
142260 @Test
143261 func perClassIdentityModeFromAnnotation( ) throws {
144262 let url = Self . inputsDirectory. appendingPathComponent ( " IdentityModeClass.swift " )
0 commit comments