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