@@ -98,22 +98,16 @@ public final class SwiftToSkeleton {
9898 importCollector. importedFunctions. compactMap ( \. from)
9999 + importCollector. importedTypes. compactMap ( \. from)
100100 + importCollector. importedGlobalGetters. compactMap ( \. from)
101- let modulePaths = Set ( importOrigins. compactMap ( \. modulePath) )
101+ // Only target-local module paths are validated here. Bare specifiers are
102+ // resolved by the JavaScript host (a bundler, an import map, or Node's
103+ // `node_modules` lookup), so there is nothing we can check without
104+ // rejecting setups that legitimately work.
105+ let modulePaths = Set ( importOrigins. compactMap ( \. localModulePath) )
102106 for path in modulePaths. sorted ( ) {
103107 if validatedJavaScriptModulePaths. contains ( path) {
104108 continue
105109 }
106110 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- }
117111 guard !path. split ( separator: " / " ) . contains ( " .. " ) else {
118112 importCollector. errors. append (
119113 DiagnosticError (
@@ -2548,21 +2542,101 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25482542 }
25492543 }
25502544
2551- /// Extracts the `jsName` argument value from an attribute, if present.
2552- static func extractJSName( from attribute: AttributeSyntax ) -> String ? {
2553- guard let arguments = attribute. arguments? . as ( LabeledExprListSyntax . self) else {
2554- return nil
2555- }
2556- for argument in arguments {
2557- if argument. label? . text == " jsName " ,
2558- let stringLiteral = argument. expression. as ( StringLiteralExprSyntax . self) ,
2559- let segment = stringLiteral. segments. first? . as ( StringSegmentSyntax . self)
2560- {
2561- return segment. content. text
2562- }
2563- }
2545+ }
2546+
2547+ /// The result of reading a `jsName:` argument.
2548+ struct ExtractedJSName {
2549+ /// The JavaScript member name to look up.
2550+ ///
2551+ /// `.default` normalizes to `"default"`: in ECMAScript a module's default
2552+ /// export *is* its `default` named export, so no separate representation
2553+ /// is needed downstream.
2554+ let memberName : String
2555+ /// True when the source spelled `.default` rather than a string literal.
2556+ let isDefaultExportSpelling : Bool
2557+ }
2558+
2559+ /// Extracts the `jsName` argument value from an attribute, if present.
2560+ private func extractJSName( from attribute: AttributeSyntax ) -> ExtractedJSName ? {
2561+ guard let arguments = attribute. arguments? . as ( LabeledExprListSyntax . self) ,
2562+ let argument = arguments. first ( where: { $0. label? . text == " jsName " } )
2563+ else {
25642564 return nil
25652565 }
2566+
2567+ if let stringLiteral = argument. expression. as ( StringLiteralExprSyntax . self) ,
2568+ let value = stringLiteral. representedLiteralValue
2569+ {
2570+ return ExtractedJSName ( memberName: value, isDefaultExportSpelling: false )
2571+ }
2572+
2573+ // Accept `.default`, `JSName.default`, and the backticked spellings.
2574+ let description = argument. expression. trimmedDescription
2575+ let caseName = description. split ( separator: " . " ) . last. map ( String . init) ?? description
2576+ if caseName == " default " || caseName == " `default` " {
2577+ return ExtractedJSName ( memberName: " default " , isDefaultExportSpelling: true )
2578+ }
2579+
2580+ errors. append (
2581+ DiagnosticError (
2582+ node: argument. expression,
2583+ message: " jsName must be a string literal or '.default'. "
2584+ )
2585+ )
2586+ return nil
2587+ }
2588+
2589+ /// Validates that a `jsName: .default` spelling appears somewhere it can mean something.
2590+ ///
2591+ /// `.default` names the default export of an ECMAScript module, so it only makes
2592+ /// sense on a top-level declaration that has a `from: .module(...)` origin.
2593+ private func validateDefaultExportUsage(
2594+ _ extracted: ExtractedJSName ? ,
2595+ from: JSImportFrom ? ,
2596+ node: some SyntaxProtocol ,
2597+ isSetter: Bool = false
2598+ ) {
2599+ guard let extracted, extracted. isDefaultExportSpelling else { return }
2600+
2601+ if isSetter {
2602+ errors. append (
2603+ DiagnosticError (
2604+ node: node,
2605+ message: " 'jsName: .default' is not supported on @JSSetter; "
2606+ + " ECMAScript module bindings are read-only. "
2607+ )
2608+ )
2609+ return
2610+ }
2611+ if case . jsClassBody = state {
2612+ errors. append (
2613+ DiagnosticError (
2614+ node: node,
2615+ message: " 'jsName: .default' is not supported on a class member; "
2616+ + " members have no module origin. Did you mean jsName: \" default \" ? "
2617+ )
2618+ )
2619+ return
2620+ }
2621+ switch from {
2622+ case . module:
2623+ return
2624+ case . global:
2625+ errors. append (
2626+ DiagnosticError (
2627+ node: node,
2628+ message: " 'jsName: .default' requires 'from: .module(...)'; "
2629+ + " globalThis has no default export. "
2630+ )
2631+ )
2632+ case nil :
2633+ errors. append (
2634+ DiagnosticError (
2635+ node: node,
2636+ message: " 'jsName: .default' requires 'from: .module(...)'. "
2637+ )
2638+ )
2639+ }
25662640 }
25672641
25682642 private func extractJSImportFrom( from attribute: AttributeSyntax ) -> JSImportFrom ? {
@@ -2588,6 +2662,26 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25882662 )
25892663 return nil
25902664 }
2665+ guard !path. isEmpty else {
2666+ errors. append (
2667+ DiagnosticError (
2668+ node: literal,
2669+ message: " JavaScript module specifier must not be empty. "
2670+ )
2671+ )
2672+ return nil
2673+ }
2674+ guard !path. hasPrefix ( " ./ " ) , !path. hasPrefix ( " ../ " ) , path != " . " , path != " .. " else {
2675+ errors. append (
2676+ DiagnosticError (
2677+ node: literal,
2678+ message: " Relative JavaScript module specifiers are not supported: ' \( path) '. "
2679+ + " Use a '/'-prefixed path for a file in this target (e.g. '/Modules/utils.mjs'), "
2680+ + " or a bare specifier for an external module (e.g. 'node:path'). "
2681+ )
2682+ )
2683+ return nil
2684+ }
25912685 if importedModulePathNodes [ path] == nil {
25922686 importedModulePathNodes [ path] = Syntax ( literal)
25932687 }
@@ -2645,7 +2739,9 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
26452739 return nil
26462740 }
26472741
2648- let jsName = AttributeChecker . extractJSName ( from: jsSetter)
2742+ let extractedJSName = extractJSName ( from: jsSetter)
2743+ validateDefaultExportUsage ( extractedJSName, from: nil , node: node, isSetter: true )
2744+ let jsName = extractedJSName? . memberName
26492745 let parameters = node. signature. parameterClause. parameters
26502746
26512747 guard let firstParam = parameters. first else {
@@ -2779,10 +2875,16 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27792875 override func visit( _ node: StructDeclSyntax ) -> SyntaxVisitorContinueKind {
27802876 if AttributeChecker . hasJSClassAttribute ( node. attributes) {
27812877 let attribute = AttributeChecker . firstJSClassAttribute ( node. attributes)
2782- let jsName = attribute. flatMap ( AttributeChecker . extractJSName)
2878+ let extractedJSName = attribute. flatMap { extractJSName ( from : $0 ) }
27832879 let from = attribute. flatMap { extractJSImportFrom ( from: $0) }
2880+ validateDefaultExportUsage ( extractedJSName, from: from, node: node)
27842881 let accessLevel = Self . bridgeAccessLevel ( from: node. modifiers)
2785- enterJSClass ( node. name. text, jsName: jsName, from: from, accessLevel: accessLevel)
2882+ enterJSClass (
2883+ node. name. text,
2884+ jsName: extractedJSName? . memberName,
2885+ from: from,
2886+ accessLevel: accessLevel
2887+ )
27862888 }
27872889 return . visitChildren
27882890 }
@@ -2796,10 +2898,16 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27962898 override func visit( _ node: ClassDeclSyntax ) -> SyntaxVisitorContinueKind {
27972899 if AttributeChecker . hasJSClassAttribute ( node. attributes) {
27982900 let attribute = AttributeChecker . firstJSClassAttribute ( node. attributes)
2799- let jsName = attribute. flatMap ( AttributeChecker . extractJSName)
2901+ let extractedJSName = attribute. flatMap { extractJSName ( from : $0 ) }
28002902 let from = attribute. flatMap { extractJSImportFrom ( from: $0) }
2903+ validateDefaultExportUsage ( extractedJSName, from: from, node: node)
28012904 let accessLevel = Self . bridgeAccessLevel ( from: node. modifiers)
2802- enterJSClass ( node. name. text, jsName: jsName, from: from, accessLevel: accessLevel)
2905+ enterJSClass (
2906+ node. name. text,
2907+ jsName: extractedJSName? . memberName,
2908+ from: from,
2909+ accessLevel: accessLevel
2910+ )
28032911 }
28042912 return . visitChildren
28052913 }
@@ -2990,8 +3098,10 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
29903098 }
29913099
29923100 let baseName = SwiftToSkeleton . normalizeIdentifier ( node. name. text)
2993- let jsName = AttributeChecker . extractJSName ( from: jsFunction)
3101+ let extractedJSName = extractJSName ( from: jsFunction)
29943102 let from = extractJSImportFrom ( from: jsFunction)
3103+ validateDefaultExportUsage ( extractedJSName, from: from, node: node)
3104+ let jsName = extractedJSName? . memberName
29953105 let name = baseName
29963106
29973107 let parameters = parseParameters ( from: node. signature. parameterClause)
@@ -3043,12 +3153,13 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
30433153 return nil
30443154 }
30453155 let propertyName = SwiftToSkeleton . normalizeIdentifier ( identifier. identifier. text)
3046- let jsName = AttributeChecker . extractJSName ( from: jsGetter)
3156+ let extractedJSName = extractJSName ( from: jsGetter)
30473157 let from = extractJSImportFrom ( from: jsGetter)
3158+ validateDefaultExportUsage ( extractedJSName, from: from, node: node)
30483159 let accessLevel = Self . bridgeAccessLevel ( from: node. modifiers)
30493160 return ImportedGetterSkeleton (
30503161 name: propertyName,
3051- jsName: jsName ,
3162+ jsName: extractedJSName ? . memberName ,
30523163 from: from,
30533164 type: propertyType,
30543165 documentation: nil ,
0 commit comments