diff --git a/crates/turbopack-core/src/resolve/mod.rs b/crates/turbopack-core/src/resolve/mod.rs index 4f3ee4ef74e3e..15961659554dc 100644 --- a/crates/turbopack-core/src/resolve/mod.rs +++ b/crates/turbopack-core/src/resolve/mod.rs @@ -1922,7 +1922,6 @@ async fn resolve_relative_request( let mut new_path = path_pattern.clone(); let fragment_val = fragment.await?; - if !fragment_val.is_empty() { new_path.push(Pattern::Alternatives( once(Pattern::Constant("".into())) @@ -1944,10 +1943,50 @@ async fn resolve_relative_request( ) .collect(), )); - new_path.normalize(); }; + if options_value.enable_typescript_with_output_extension { + new_path.replace_final_constants(&|c: &RcStr| -> Option { + let result = match c.rsplit_once(".") { + Some((base, "js")) => Some(( + base, + vec![ + Pattern::Constant(".ts".into()), + Pattern::Constant(".tsx".into()), + Pattern::Constant(".js".into()), + ], + )), + Some((base, "mjs")) => Some(( + base, + vec![ + Pattern::Constant(".mts".into()), + Pattern::Constant(".js".into()), + ], + )), + Some((base, "cjs")) => Some(( + base, + vec![ + Pattern::Constant(".cts".into()), + Pattern::Constant(".js".into()), + ], + )), + _ => None, + }; + result.map(|(base, replacement)| { + if base.is_empty() { + Pattern::Alternatives(replacement) + } else { + Pattern::Concatenation(vec![ + Pattern::Constant(base.into()), + Pattern::Alternatives(replacement), + ]) + } + }) + }); + new_path.normalize(); + } + let mut results = Vec::new(); let matches = read_matches( lookup_path, diff --git a/crates/turbopack-core/src/resolve/options.rs b/crates/turbopack-core/src/resolve/options.rs index 7472b946f9635..c4c69142a9b26 100644 --- a/crates/turbopack-core/src/resolve/options.rs +++ b/crates/turbopack-core/src/resolve/options.rs @@ -442,6 +442,9 @@ pub struct ResolveOptions { pub resolved_map: Option>, pub before_resolve_plugins: Vec>>, pub plugins: Vec>>, + /// Support resolving *.js requests to *.ts files + pub enable_typescript_with_output_extension: bool, + pub placeholder_for_future_extensions: (), } diff --git a/crates/turbopack-core/src/resolve/pattern.rs b/crates/turbopack-core/src/resolve/pattern.rs index e2d84edb1908c..26702a53640b3 100644 --- a/crates/turbopack-core/src/resolve/pattern.rs +++ b/crates/turbopack-core/src/resolve/pattern.rs @@ -627,6 +627,33 @@ impl Pattern { new.normalize(); Pattern::alternatives([self.clone(), new]) } + + /// Calls `cb` on all constants that are at the end of the pattern and + /// replaces the given final constant with the returned pattern. Returns + /// true if replacements were performed. + pub fn replace_final_constants(&mut self, cb: &impl Fn(&RcStr) -> Option) -> bool { + let mut replaced = false; + match self { + Pattern::Constant(c) => { + if let Some(replacement) = cb(c) { + *self = replacement; + replaced = true; + } + } + Pattern::Dynamic => {} + Pattern::Alternatives(list) => { + for i in list { + replaced = i.replace_final_constants(cb) || replaced; + } + } + Pattern::Concatenation(list) => { + if let Some(i) = list.last_mut() { + replaced = i.replace_final_constants(cb) || replaced; + } + } + } + replaced + } } impl Pattern { @@ -1111,6 +1138,7 @@ pub async fn read_matches( #[cfg(test)] mod tests { use rstest::*; + use turbo_tasks::RcStr; use super::Pattern; @@ -1358,4 +1386,77 @@ mod tests { ) { assert_eq!(pat.next_constants(value), expected); } + + #[test] + fn replace_final_constants() { + fn f(mut p: Pattern, cb: &impl Fn(&RcStr) -> Option) -> Pattern { + p.replace_final_constants(cb); + p + } + + let js_to_ts_tsx = |c: &RcStr| -> Option { + c.strip_suffix(".js").map(|rest| { + let new_ending = Pattern::Alternatives(vec![ + Pattern::Constant(".ts".into()), + Pattern::Constant(".tsx".into()), + Pattern::Constant(".js".into()), + ]); + if !rest.is_empty() { + Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending]) + } else { + new_ending + } + }) + }; + + assert_eq!( + f( + Pattern::Concatenation(vec![ + Pattern::Constant(".".into()), + Pattern::Constant("/".into()), + Pattern::Dynamic, + Pattern::Alternatives(vec![ + Pattern::Constant(".js".into()), + Pattern::Constant(".node".into()), + ]) + ]), + &js_to_ts_tsx + ), + Pattern::Concatenation(vec![ + Pattern::Constant(".".into()), + Pattern::Constant("/".into()), + Pattern::Dynamic, + Pattern::Alternatives(vec![ + Pattern::Alternatives(vec![ + Pattern::Constant(".ts".into()), + Pattern::Constant(".tsx".into()), + Pattern::Constant(".js".into()), + ]), + Pattern::Constant(".node".into()), + ]) + ]), + ); + assert_eq!( + f( + Pattern::Concatenation(vec![ + Pattern::Constant(".".into()), + Pattern::Constant("/".into()), + Pattern::Constant("abc.js".into()), + ]), + &js_to_ts_tsx + ), + Pattern::Concatenation(vec![ + Pattern::Constant(".".into()), + Pattern::Constant("/".into()), + Pattern::Concatenation(vec![ + Pattern::Constant("abc".into()), + Pattern::Alternatives(vec![ + Pattern::Constant(".ts".into()), + Pattern::Constant(".tsx".into()), + Pattern::Constant(".js".into()), + ]) + ]), + ]) + ); + } } diff --git a/crates/turbopack-resolve/src/typescript.rs b/crates/turbopack-resolve/src/typescript.rs index 0a3282f7dffe4..5d04cb9c027f3 100644 --- a/crates/turbopack-resolve/src/typescript.rs +++ b/crates/turbopack-resolve/src/typescript.rs @@ -215,6 +215,7 @@ pub async fn read_from_tsconfigs( pub struct TsConfigResolveOptions { base_url: Option>, import_map: Option>, + is_module_resolution_nodenext: bool, } #[turbo_tasks::value_impl] @@ -318,9 +319,18 @@ pub async fn tsconfig_resolve_options( None }; + let is_module_resolution_nodenext = read_from_tsconfigs(&configs, |json, _| { + json["compilerOptions"]["moduleResolution"] + .as_str() + .map(|module_resolution| module_resolution.eq_ignore_ascii_case("nodenext")) + }) + .await? + .unwrap_or_default(); + Ok(TsConfigResolveOptions { base_url, import_map, + is_module_resolution_nodenext, } .cell()) } @@ -352,6 +362,9 @@ pub async fn apply_tsconfig_resolve_options( .unwrap_or(tsconfig_import_map), ); } + resolve_options.enable_typescript_with_output_extension = + tsconfig_resolve_options.is_module_resolution_nodenext; + Ok(resolve_options.cell()) } diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/index.js b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/index.js new file mode 100644 index 0000000000000..3a0b2a12345ec --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/index.js @@ -0,0 +1,11 @@ +import foo from "./src/foo.js"; +import bar from "./src/bar.js"; +import fooEsm from "./src/foo-esm.mjs"; +import fooCjs from "./src/foo-cjs.cjs"; + +it("should correctly resolve explicit extensions with nodenext", () => { + expect(foo).toBe("foo"); + expect(bar).toBe("bar"); + expect(fooEsm).toBe("fooEsm"); + expect(fooCjs).toBe("fooCjs"); +}); diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/bar.tsx b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/bar.tsx new file mode 100644 index 0000000000000..801821e109c18 --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/bar.tsx @@ -0,0 +1 @@ +export default "bar"; diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo-cjs.cts b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo-cjs.cts new file mode 100644 index 0000000000000..122dbfc3ec216 --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo-cjs.cts @@ -0,0 +1 @@ +module.exports = "fooCjs"; diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo-esm.mts b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo-esm.mts new file mode 100644 index 0000000000000..4c5f085924c15 --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo-esm.mts @@ -0,0 +1 @@ +export default "fooEsm"; diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo.js b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo.js new file mode 100644 index 0000000000000..fd7935a602d71 --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo.js @@ -0,0 +1 @@ +throw new Error("Should have a lower precedence than foo.ts"); diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo.ts b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo.ts new file mode 100644 index 0000000000000..60c6c8d8b04f9 --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/src/foo.ts @@ -0,0 +1 @@ +export default "foo"; diff --git a/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/tsconfig.json b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/tsconfig.json new file mode 100644 index 0000000000000..cb7f695badd8e --- /dev/null +++ b/crates/turbopack-tests/tests/execution/turbopack/resolving/tsconfig-nodenext/input/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}