@@ -2258,23 +2258,25 @@ async fn resolve_relative_request(
22582258
22592259 if options_value. enable_typescript_with_output_extension {
22602260 let replaced = new_path. replace_final_constants ( & mut |c : & RcStr | -> Option < Pattern > {
2261- let ( base, replacement) = match c. rsplit_once ( "." ) {
2262- Some ( ( base, "js" ) ) => ( base, vec ! [ rcstr!( ".ts" ) , rcstr!( ".tsx" ) , rcstr!( ".js" ) ] ) ,
2263- Some ( ( base, "mjs" ) ) => ( base, vec ! [ rcstr!( ".mts" ) , rcstr!( ".mjs" ) ] ) ,
2264- Some ( ( base, "cjs" ) ) => ( base, vec ! [ rcstr!( ".cts" ) , rcstr!( ".cjs" ) ] ) ,
2261+ let ( base, replacement) : ( & str , & [ RcStr ] ) = match c. rsplit_once ( "." ) {
2262+ Some ( ( base, "js" ) ) => ( base, & [ rcstr ! ( ".ts" ) , rcstr ! ( ".tsx" ) , rcstr ! ( ".js" ) ] ) ,
2263+ Some ( ( base, "mjs" ) ) => ( base, & [ rcstr ! ( ".mts" ) , rcstr ! ( ".mjs" ) ] ) ,
2264+ Some ( ( base, "cjs" ) ) => ( base, & [ rcstr ! ( ".cts" ) , rcstr ! ( ".cjs" ) ] ) ,
22652265 _ => {
22662266 return None ;
22672267 }
22682268 } ;
22692269 added_extension_alternatives. extend ( replacement. iter ( ) . cloned ( ) ) ;
22702270 if base. is_empty ( ) {
22712271 Some ( Pattern :: Alternatives (
2272- replacement. into_iter ( ) . map ( Pattern :: Constant ) . collect ( ) ,
2272+ replacement. iter ( ) . cloned ( ) . map ( Pattern :: Constant ) . collect ( ) ,
22732273 ) )
22742274 } else {
22752275 Some ( Pattern :: Concatenation ( vec ! [
22762276 Pattern :: Constant ( base. into( ) ) ,
2277- Pattern :: Alternatives ( replacement. into_iter( ) . map( Pattern :: Constant ) . collect( ) ) ,
2277+ Pattern :: Alternatives (
2278+ replacement. iter( ) . cloned( ) . map( Pattern :: Constant ) . collect( ) ,
2279+ ) ,
22782280 ] ) )
22792281 }
22802282 } ) ;
@@ -2321,6 +2323,10 @@ async fn resolve_relative_request(
23212323 continue ;
23222324 } ;
23232325
2326+ // Check if we've already seen this filename without the extension. If so, skip
2327+ // this match since we already processed a higher-priority extension.
2328+ // Only do this check if the pattern doesn't contain dynamic parts - for dynamic
2329+ // imports, we need multiple keys for runtime resolution.
23242330 if !seen_base_patterns. insert ( matched_pattern) {
23252331 continue ' matches; // Skip this entire file
23262332 }
@@ -3347,88 +3353,88 @@ mod tests {
33473353
33483354 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
33493355 async fn test_explicit_js_resolves_to_ts ( ) {
3350- resolve_relative_request_test (
3351- vec ! [ "foo.js" , "foo.ts" ] ,
3352- rcstr ! ( "./foo.js" ) . into ( ) ,
3353- true ,
3354- false ,
3355- vec ! [ ( "./foo.ts" , "foo.ts" ) ] ,
3356- )
3356+ resolve_relative_request_test ( ResolveTestParams {
3357+ files : vec ! [ "foo.js" , "foo.ts" ] ,
3358+ pattern : rcstr ! ( "./foo.js" ) . into ( ) ,
3359+ enable_typescript_with_output_extension : true ,
3360+ fully_specified : false ,
3361+ expected : vec ! [ ( "./foo.ts" , "foo.ts" ) ] ,
3362+ } )
33573363 . await ;
33583364 }
33593365
33603366 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
33613367 async fn test_implicit_request_ts_priority ( ) {
3362- resolve_relative_request_test (
3363- vec ! [ "foo.js" , "foo.ts" ] ,
3364- rcstr ! ( "./foo" ) . into ( ) ,
3365- true ,
3366- false ,
3367- vec ! [ ( "./foo" , "foo.ts" ) ] , // Implicit resolution uses extensionless key
3368- )
3368+ resolve_relative_request_test ( ResolveTestParams {
3369+ files : vec ! [ "foo.js" , "foo.ts" ] ,
3370+ pattern : rcstr ! ( "./foo" ) . into ( ) ,
3371+ enable_typescript_with_output_extension : true ,
3372+ fully_specified : false ,
3373+ expected : vec ! [ ( "./foo" , "foo.ts" ) ] , // Implicit resolution uses extensionless key
3374+ } )
33693375 . await ;
33703376 }
33713377
33723378 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
33733379 async fn test_ts_priority_over_json ( ) {
3374- resolve_relative_request_test (
3375- vec ! [ "posts.json" , "posts.ts" ] ,
3376- rcstr ! ( "./posts" ) . into ( ) ,
3377- true ,
3378- false ,
3379- vec ! [ ( "./posts" , "posts.ts" ) ] , // Implicit resolution uses extensionless key
3380- )
3380+ resolve_relative_request_test ( ResolveTestParams {
3381+ files : vec ! [ "posts.json" , "posts.ts" ] ,
3382+ pattern : rcstr ! ( "./posts" ) . into ( ) ,
3383+ enable_typescript_with_output_extension : true ,
3384+ fully_specified : false ,
3385+ expected : vec ! [ ( "./posts" , "posts.ts" ) ] , // Implicit resolution uses extensionless key
3386+ } )
33813387 . await ;
33823388 }
33833389
33843390 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
33853391 async fn test_only_js_file_no_ts ( ) {
3386- resolve_relative_request_test (
3387- vec ! [ "bar.js" ] ,
3388- rcstr ! ( "./bar.js" ) . into ( ) ,
3389- true ,
3390- false ,
3391- vec ! [ ( "./bar.js" , "bar.js" ) ] ,
3392- )
3392+ resolve_relative_request_test ( ResolveTestParams {
3393+ files : vec ! [ "bar.js" ] ,
3394+ pattern : rcstr ! ( "./bar.js" ) . into ( ) ,
3395+ enable_typescript_with_output_extension : true ,
3396+ fully_specified : false ,
3397+ expected : vec ! [ ( "./bar.js" , "bar.js" ) ] ,
3398+ } )
33933399 . await ;
33943400 }
33953401
33963402 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
33973403 async fn test_explicit_ts_request ( ) {
3398- resolve_relative_request_test (
3399- vec ! [ "foo.js" , "foo.ts" ] ,
3400- rcstr ! ( "./foo.ts" ) . into ( ) ,
3401- true ,
3402- false ,
3403- vec ! [ ( "./foo.ts" , "foo.ts" ) ] ,
3404- )
3404+ resolve_relative_request_test ( ResolveTestParams {
3405+ files : vec ! [ "foo.js" , "foo.ts" ] ,
3406+ pattern : rcstr ! ( "./foo.ts" ) . into ( ) ,
3407+ enable_typescript_with_output_extension : true ,
3408+ fully_specified : false ,
3409+ expected : vec ! [ ( "./foo.ts" , "foo.ts" ) ] ,
3410+ } )
34053411 . await ;
34063412 }
34073413
34083414 // Fragment handling tests
34093415 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
34103416 async fn test_fragment_as_part_of_filename ( ) {
34113417 // When a file literally contains '#' in its name, it should be preserved
3412- resolve_relative_request_test (
3413- vec ! [ "client#component.js" , "client#component.ts" ] ,
3414- rcstr ! ( "./client#component.js" ) . into ( ) ,
3415- true ,
3416- false ,
3417- vec ! [ ( "./client#component.ts" , "client#component.ts" ) ] ,
3418- )
3418+ resolve_relative_request_test ( ResolveTestParams {
3419+ files : vec ! [ "client#component.js" , "client#component.ts" ] ,
3420+ pattern : rcstr ! ( "./client#component.js" ) . into ( ) ,
3421+ enable_typescript_with_output_extension : true ,
3422+ fully_specified : false ,
3423+ expected : vec ! [ ( "./client#component.ts" , "client#component.ts" ) ] ,
3424+ } )
34193425 . await ;
34203426 }
34213427
34223428 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
34233429 async fn test_fragment_with_ts_priority ( ) {
34243430 // Fragment handling with extension priority
3425- resolve_relative_request_test (
3426- vec ! [ "page#section.js" , "page#section.ts" ] ,
3427- rcstr ! ( "./page#section" ) . into ( ) ,
3428- true ,
3429- false ,
3430- vec ! [ ( "./page#section" , "page#section.ts" ) ] ,
3431- )
3431+ resolve_relative_request_test ( ResolveTestParams {
3432+ files : vec ! [ "page#section.js" , "page#section.ts" ] ,
3433+ pattern : rcstr ! ( "./page#section" ) . into ( ) ,
3434+ enable_typescript_with_output_extension : true ,
3435+ fully_specified : false ,
3436+ expected : vec ! [ ( "./page#section" , "page#section.ts" ) ] ,
3437+ } )
34323438 . await ;
34333439 }
34343440
@@ -3437,21 +3443,21 @@ mod tests {
34373443 async fn test_dynamic_pattern_with_js_extension ( ) {
34383444 // Pattern: ./src/*.js should generate multiple keys with .ts priority
34393445 // When both foo.js and foo.ts exist, dynamic patterns need both keys for runtime resolution
3440- // Results are sorted alphabetically by key
3441- resolve_relative_request_test (
3442- vec ! [ "src/foo.js" , "src/foo.ts" , "src/bar.js" ] ,
3443- Pattern :: Concatenation ( vec ! [
3446+ // Results maintain priority order: .ts files before .js files
3447+ resolve_relative_request_test ( ResolveTestParams {
3448+ files : vec ! [ "src/foo.js" , "src/foo.ts" , "src/bar.js" ] ,
3449+ pattern : Pattern :: Concatenation ( vec ! [
34443450 Pattern :: Constant ( rcstr!( "./src/" ) ) ,
34453451 Pattern :: Dynamic ,
34463452 Pattern :: Constant ( rcstr!( ".js" ) ) ,
34473453 ] ) ,
3448- true ,
3449- false ,
3450- vec ! [
3454+ enable_typescript_with_output_extension : true ,
3455+ fully_specified : false ,
3456+ expected : vec ! [
34513457 ( "./src/foo.ts" , "src/foo.ts" ) ,
34523458 ( "./src/bar.js" , "src/bar.js" ) ,
34533459 ] ,
3454- )
3460+ } )
34553461 . await ;
34563462 }
34573463
@@ -3460,29 +3466,62 @@ mod tests {
34603466 // Pattern: ./src/* (no extension) with TypeScript priority
34613467 // Dynamic patterns generate keys for all matched files, including extension alternatives
34623468 // Results are sorted alphabetically by key
3463- resolve_relative_request_test (
3464- vec ! [ "src/foo.js" , "src/foo.ts" , "src/bar.js" ] ,
3465- Pattern :: Concatenation ( vec ! [ Pattern :: Constant ( rcstr!( "./src/" ) ) , Pattern :: Dynamic ] ) ,
3466- true ,
3467- false ,
3468- vec ! [
3469+ resolve_relative_request_test ( ResolveTestParams {
3470+ files : vec ! [ "src/foo.js" , "src/foo.ts" , "src/bar.js" ] ,
3471+ pattern : Pattern :: Concatenation ( vec ! [
3472+ Pattern :: Constant ( rcstr!( "./src/" ) ) ,
3473+ Pattern :: Dynamic ,
3474+ ] ) ,
3475+ enable_typescript_with_output_extension : true ,
3476+ fully_specified : false ,
3477+ expected : vec ! [
34693478 ( "./src/bar" , "src/bar.js" ) ,
34703479 ( "./src/bar.js" , "src/bar.js" ) ,
34713480 ( "./src/foo" , "src/foo.js" ) ,
34723481 ( "./src/foo.js" , "src/foo.js" ) ,
34733482 ] ,
3474- )
3483+ } )
34753484 . await ;
34763485 }
34773486
3478- /// Helper function to run a single extension priority test case
3479- async fn resolve_relative_request_test (
3480- files : Vec < & str > ,
3487+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
3488+ async fn test_dts_shouldnt_be_selected ( ) {
3489+ // Pattern: ./src/* (no extension) with TypeScript priority
3490+ // Dynamic patterns generate keys for all matched files, including extension alternatives
3491+ // Results are sorted alphabetically by key
3492+ resolve_relative_request_test ( ResolveTestParams {
3493+ files : vec ! [ "foo.js" , "foo.ts" , "foo.d.ts" ] ,
3494+ pattern : rcstr ! ( "foo.js" ) . into ( ) ,
3495+ enable_typescript_with_output_extension : true ,
3496+ fully_specified : false ,
3497+ expected : vec ! [ ( "foo.ts" , "foo.ts" ) ] ,
3498+ } )
3499+ . await ;
3500+ }
3501+
3502+ /// Test parameters for resolve_relative_request_test
3503+ struct ResolveTestParams < ' a > {
3504+ /// Files to create in the test filesystem
3505+ files : Vec < & ' a str > ,
3506+ /// Pattern to resolve
34813507 pattern : Pattern ,
3508+ /// Whether to enable TypeScript output extension transformation
34823509 enable_typescript_with_output_extension : bool ,
3510+ /// Whether to require fully specified imports
34833511 fully_specified : bool ,
3484- expected : Vec < ( & str , & str ) > ,
3485- ) {
3512+ /// Expected (request_key, resolved_path) pairs
3513+ expected : Vec < ( & ' a str , & ' a str ) > ,
3514+ }
3515+
3516+ /// Helper function to run a single extension priority test case
3517+ async fn resolve_relative_request_test ( params : ResolveTestParams < ' _ > ) {
3518+ let ResolveTestParams {
3519+ files,
3520+ pattern,
3521+ enable_typescript_with_output_extension,
3522+ fully_specified,
3523+ expected,
3524+ } = params;
34863525 let scratch = tempfile:: tempdir ( ) . unwrap ( ) ;
34873526 {
34883527 let path = scratch. path ( ) ;
0 commit comments