@@ -23,6 +23,8 @@ public final class SwiftToSkeleton {
2323
2424 private var sourceFiles : [ ( sourceFile : SourceFileSyntax , inputFilePath : String ) ] = [ ]
2525 private var usedExternalModules = Set < String > ( )
26+ private let javaScriptModuleExists : ( String ) throws -> Bool
27+ private var validatedJavaScriptModulePaths = Set < String > ( )
2628
2729 /// Non-fatal diagnostics collected during `finalize()`. These do not fail the build.
2830 public private( set) var warnings : [ ( file : String , diagnostic : DiagnosticError ) ] = [ ]
@@ -32,12 +34,14 @@ public final class SwiftToSkeleton {
3234 moduleName : String ,
3335 exposeToGlobal : Bool ,
3436 externalModuleIndex : ExternalModuleIndex ,
35- identityMode : String ? = nil
37+ identityMode : String ? = nil ,
38+ javaScriptModuleExists : @escaping ( String ) throws -> Bool = { _ in false }
3639 ) {
3740 self . progress = progress
3841 self . moduleName = moduleName
3942 self . exposeToGlobal = exposeToGlobal
4043 self . identityMode = identityMode
44+ self . javaScriptModuleExists = javaScriptModuleExists
4145 self . typeDeclResolver = TypeDeclResolver ( )
4246 self . externalModuleIndex = externalModuleIndex
4347
@@ -90,6 +94,57 @@ public final class SwiftToSkeleton {
9094 )
9195 importCollector . walk ( sourceFile )
9296
97+ let importOrigins =
98+ importCollector . importedFunctions . compactMap ( \. from )
99+ + importCollector . importedTypes . compactMap ( \. from )
100+ + importCollector . importedGlobalGetters . compactMap ( \. from )
101+ let modulePaths = Set ( importOrigins . compactMap ( \. modulePath ) )
102+ for path in modulePaths . sorted ( ) {
103+ if validatedJavaScriptModulePaths . contains ( path ) {
104+ continue
105+ }
106+ let pathNode = importCollector . importedModulePathNodes [ path ] ?? Syntax ( sourceFile )
107+ guard path . hasPrefix ( " / " ) else {
108+ importCollector . errors . append (
109+ DiagnosticError (
110+ node : pathNode ,
111+ message : " JavaScript module paths must start with '/' to indicate the Swift target root: "
112+ + " ' \( path ) '. "
113+ )
114+ )
115+ continue
116+ }
117+ guard ! path . split ( separator : " / " ) . contains ( " .. " ) else {
118+ importCollector . errors . append (
119+ DiagnosticError (
120+ node : pathNode ,
121+ message : " JavaScript module paths must not contain '..': ' \( path ) '. "
122+ )
123+ )
124+ continue
125+ }
126+ let lowercasedPath = path . lowercased ( )
127+ guard lowercasedPath . hasSuffix ( " .js " ) || lowercasedPath . hasSuffix ( " .mjs " ) else {
128+ importCollector . errors . append (
129+ DiagnosticError (
130+ node : pathNode ,
131+ message : " JavaScript modules must use a '.js' or '.mjs' extension: ' \( path ) '. "
132+ )
133+ )
134+ continue
135+ }
136+ guard try javaScriptModuleExists ( path ) else {
137+ importCollector . errors . append (
138+ DiagnosticError (
139+ node : pathNode ,
140+ message : " JavaScript module file was not found at ' \( path ) '. "
141+ )
142+ )
143+ continue
144+ }
145+ validatedJavaScriptModulePaths . insert ( path )
146+ }
147+
93148 let exportErrors = exportCollector . errors . filter { $0 . severity == . error }
94149 let importErrorsFatal = importCollector . errors . filter {
95150 $0 . severity == . error && ! $0 . message . contains ( " Unsupported type ' " )
@@ -2413,6 +2468,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
24132468 var importedFunctions : [ ImportedFunctionSkeleton ] = [ ]
24142469 var importedTypes : [ ImportedTypeSkeleton ] = [ ]
24152470 var importedGlobalGetters : [ ImportedGetterSkeleton ] = [ ]
2471+ var importedModulePathNodes : [ String : Syntax ] = [ : ]
24162472 var errors : [ DiagnosticError ] = [ ]
24172473
24182474 private let inputFilePath : String
@@ -2507,22 +2563,41 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25072563 }
25082564 return nil
25092565 }
2566+ }
2567+
2568+ private func extractJSImportFrom ( from attribute : AttributeSyntax ) -> JSImportFrom ? {
2569+ guard let arguments = attribute . arguments ? . as ( LabeledExprListSyntax . self ) ,
2570+ let argument = arguments . first ( where : { $0 . label ? . text == " from " } )
2571+ else {
2572+ return nil
2573+ }
25102574
2511- /// Extracts the `from` argument value from an attribute, if present.
2512- static func extractJSImportFrom ( from attribute : AttributeSyntax ) -> JSImportFrom ? {
2513- guard let arguments = attribute . arguments ? . as ( LabeledExprListSyntax . self ) else {
2575+ if let call = argument . expression . as ( FunctionCallExprSyntax . self ) ,
2576+ call . calledExpression . trimmedDescription . split ( separator : " . " ) . last == " module "
2577+ {
2578+ guard call . arguments . count == 1 ,
2579+ let pathExpression = call . arguments . first ? . expression ,
2580+ let literal = pathExpression . as ( StringLiteralExprSyntax . self ) ,
2581+ let path = literal . representedLiteralValue
2582+ else {
2583+ errors . append (
2584+ DiagnosticError (
2585+ node : call . arguments . first ? . expression ?? argument . expression ,
2586+ message : " JavaScript module path must be a string literal. "
2587+ )
2588+ )
25142589 return nil
25152590 }
2516- for argument in arguments {
2517- guard argument . label ? . text == " from " else { continue }
2518-
2519- // Accept `.global`, `JSImportFrom.global`, etc.
2520- let description = argument . expression . trimmedDescription
2521- let caseName = description . split ( separator : " . " ) . last . map ( String . init ) ?? description
2522- return JSImportFrom ( rawValue : caseName )
2591+ if importedModulePathNodes [ path ] == nil {
2592+ importedModulePathNodes [ path ] = Syntax ( literal )
25232593 }
2524- return nil
2594+ return . module ( path )
25252595 }
2596+
2597+ // Accept `.global`, `JSImportFrom.global`, etc.
2598+ let description = argument . expression . trimmedDescription
2599+ let caseName = description . split ( separator : " . " ) . last . map ( String . init ) ?? description
2600+ return caseName == " global " ? . global : nil
25262601 }
25272602
25282603 // MARK: - Validation Helpers
@@ -2705,7 +2780,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27052780 if AttributeChecker . hasJSClassAttribute ( node . attributes ) {
27062781 let attribute = AttributeChecker . firstJSClassAttribute ( node . attributes )
27072782 let jsName = attribute . flatMap ( AttributeChecker . extractJSName )
2708- let from = attribute . flatMap ( AttributeChecker . extractJSImportFrom )
2783+ let from = attribute . flatMap { extractJSImportFrom ( from : $0 ) }
27092784 let accessLevel = Self . bridgeAccessLevel ( from : node . modifiers )
27102785 enterJSClass ( node . name . text , jsName : jsName , from : from , accessLevel : accessLevel )
27112786 }
@@ -2722,7 +2797,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27222797 if AttributeChecker . hasJSClassAttribute ( node . attributes ) {
27232798 let attribute = AttributeChecker . firstJSClassAttribute ( node . attributes )
27242799 let jsName = attribute . flatMap ( AttributeChecker . extractJSName )
2725- let from = attribute . flatMap ( AttributeChecker . extractJSImportFrom )
2800+ let from = attribute . flatMap { extractJSImportFrom ( from : $0 ) }
27262801 let accessLevel = Self . bridgeAccessLevel ( from : node . modifiers )
27272802 enterJSClass ( node . name . text , jsName : jsName , from : from , accessLevel : accessLevel )
27282803 }
@@ -2916,7 +2991,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
29162991
29172992 let baseName = SwiftToSkeleton . normalizeIdentifier ( node . name . text )
29182993 let jsName = AttributeChecker . extractJSName ( from : jsFunction )
2919- let from = AttributeChecker . extractJSImportFrom ( from : jsFunction )
2994+ let from = extractJSImportFrom ( from : jsFunction )
29202995 let name = baseName
29212996
29222997 let parameters = parseParameters ( from : node . signature . parameterClause )
@@ -2969,7 +3044,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
29693044 }
29703045 let propertyName = SwiftToSkeleton . normalizeIdentifier ( identifier . identifier . text )
29713046 let jsName = AttributeChecker . extractJSName ( from : jsGetter )
2972- let from = AttributeChecker . extractJSImportFrom ( from : jsGetter )
3047+ let from = extractJSImportFrom ( from : jsGetter )
29733048 let accessLevel = Self . bridgeAccessLevel ( from : node . modifiers )
29743049 return ImportedGetterSkeleton (
29753050 name : propertyName ,
0 commit comments