Skip to content

Commit 21fe711

Browse files
authored
Support module_resolution: "nodenext" (vercel/turborepo#8748)
### Description Allow resolving for example `./foo.js` to `./foo.ts` `.js` can resolve to `.ts` and `.tsx` `.mjs` can resolve to `.mts` `.cjs` can resolve to `.cts` Closes PACK-3031 This is what `tsc --traceResolution` says about priority of the various possible resolution: ``` ======== Resolving module '../libs/f.js' from '/Users/niklas/Desktop/nodenext-app/app/page.tsx'. ======== Explicitly specified module resolution kind: 'NodeNext'. Resolving in CJS mode with conditions 'require', 'types', 'node'. Loading module as file / folder, candidate module location '/Users/niklas/Desktop/nodenext-app/libs/f.js', target file types: TypeScript, JavaScript, Declaration. File name '/Users/niklas/Desktop/nodenext-app/libs/f.js' has a '.js' extension - stripping it. File '/Users/niklas/Desktop/nodenext-app/libs/f.ts' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.tsx' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.d.ts' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.js' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.jsx' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.js.ts' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.js.tsx' does not exist. File '/Users/niklas/Desktop/nodenext-app/libs/f.js.d.ts' does not exist. ``` ### Testing Instructions I also added a test case
1 parent 0365d92 commit 21fe711

File tree

11 files changed

+180
-2
lines changed

11 files changed

+180
-2
lines changed

crates/turbopack-core/src/resolve/mod.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,7 +1922,6 @@ async fn resolve_relative_request(
19221922
let mut new_path = path_pattern.clone();
19231923

19241924
let fragment_val = fragment.await?;
1925-
19261925
if !fragment_val.is_empty() {
19271926
new_path.push(Pattern::Alternatives(
19281927
once(Pattern::Constant("".into()))
@@ -1944,10 +1943,50 @@ async fn resolve_relative_request(
19441943
)
19451944
.collect(),
19461945
));
1947-
19481946
new_path.normalize();
19491947
};
19501948

1949+
if options_value.enable_typescript_with_output_extension {
1950+
new_path.replace_final_constants(&|c: &RcStr| -> Option<Pattern> {
1951+
let result = match c.rsplit_once(".") {
1952+
Some((base, "js")) => Some((
1953+
base,
1954+
vec![
1955+
Pattern::Constant(".ts".into()),
1956+
Pattern::Constant(".tsx".into()),
1957+
Pattern::Constant(".js".into()),
1958+
],
1959+
)),
1960+
Some((base, "mjs")) => Some((
1961+
base,
1962+
vec![
1963+
Pattern::Constant(".mts".into()),
1964+
Pattern::Constant(".js".into()),
1965+
],
1966+
)),
1967+
Some((base, "cjs")) => Some((
1968+
base,
1969+
vec![
1970+
Pattern::Constant(".cts".into()),
1971+
Pattern::Constant(".js".into()),
1972+
],
1973+
)),
1974+
_ => None,
1975+
};
1976+
result.map(|(base, replacement)| {
1977+
if base.is_empty() {
1978+
Pattern::Alternatives(replacement)
1979+
} else {
1980+
Pattern::Concatenation(vec![
1981+
Pattern::Constant(base.into()),
1982+
Pattern::Alternatives(replacement),
1983+
])
1984+
}
1985+
})
1986+
});
1987+
new_path.normalize();
1988+
}
1989+
19511990
let mut results = Vec::new();
19521991
let matches = read_matches(
19531992
lookup_path,

crates/turbopack-core/src/resolve/options.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,9 @@ pub struct ResolveOptions {
442442
pub resolved_map: Option<Vc<ResolvedMap>>,
443443
pub before_resolve_plugins: Vec<Vc<Box<dyn BeforeResolvePlugin>>>,
444444
pub plugins: Vec<Vc<Box<dyn AfterResolvePlugin>>>,
445+
/// Support resolving *.js requests to *.ts files
446+
pub enable_typescript_with_output_extension: bool,
447+
445448
pub placeholder_for_future_extensions: (),
446449
}
447450

crates/turbopack-core/src/resolve/pattern.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,33 @@ impl Pattern {
627627
new.normalize();
628628
Pattern::alternatives([self.clone(), new])
629629
}
630+
631+
/// Calls `cb` on all constants that are at the end of the pattern and
632+
/// replaces the given final constant with the returned pattern. Returns
633+
/// true if replacements were performed.
634+
pub fn replace_final_constants(&mut self, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> bool {
635+
let mut replaced = false;
636+
match self {
637+
Pattern::Constant(c) => {
638+
if let Some(replacement) = cb(c) {
639+
*self = replacement;
640+
replaced = true;
641+
}
642+
}
643+
Pattern::Dynamic => {}
644+
Pattern::Alternatives(list) => {
645+
for i in list {
646+
replaced = i.replace_final_constants(cb) || replaced;
647+
}
648+
}
649+
Pattern::Concatenation(list) => {
650+
if let Some(i) = list.last_mut() {
651+
replaced = i.replace_final_constants(cb) || replaced;
652+
}
653+
}
654+
}
655+
replaced
656+
}
630657
}
631658

632659
impl Pattern {
@@ -1111,6 +1138,7 @@ pub async fn read_matches(
11111138
#[cfg(test)]
11121139
mod tests {
11131140
use rstest::*;
1141+
use turbo_tasks::RcStr;
11141142

11151143
use super::Pattern;
11161144

@@ -1358,4 +1386,77 @@ mod tests {
13581386
) {
13591387
assert_eq!(pat.next_constants(value), expected);
13601388
}
1389+
1390+
#[test]
1391+
fn replace_final_constants() {
1392+
fn f(mut p: Pattern, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> Pattern {
1393+
p.replace_final_constants(cb);
1394+
p
1395+
}
1396+
1397+
let js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
1398+
c.strip_suffix(".js").map(|rest| {
1399+
let new_ending = Pattern::Alternatives(vec![
1400+
Pattern::Constant(".ts".into()),
1401+
Pattern::Constant(".tsx".into()),
1402+
Pattern::Constant(".js".into()),
1403+
]);
1404+
if !rest.is_empty() {
1405+
Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending])
1406+
} else {
1407+
new_ending
1408+
}
1409+
})
1410+
};
1411+
1412+
assert_eq!(
1413+
f(
1414+
Pattern::Concatenation(vec![
1415+
Pattern::Constant(".".into()),
1416+
Pattern::Constant("/".into()),
1417+
Pattern::Dynamic,
1418+
Pattern::Alternatives(vec![
1419+
Pattern::Constant(".js".into()),
1420+
Pattern::Constant(".node".into()),
1421+
])
1422+
]),
1423+
&js_to_ts_tsx
1424+
),
1425+
Pattern::Concatenation(vec![
1426+
Pattern::Constant(".".into()),
1427+
Pattern::Constant("/".into()),
1428+
Pattern::Dynamic,
1429+
Pattern::Alternatives(vec![
1430+
Pattern::Alternatives(vec![
1431+
Pattern::Constant(".ts".into()),
1432+
Pattern::Constant(".tsx".into()),
1433+
Pattern::Constant(".js".into()),
1434+
]),
1435+
Pattern::Constant(".node".into()),
1436+
])
1437+
]),
1438+
);
1439+
assert_eq!(
1440+
f(
1441+
Pattern::Concatenation(vec![
1442+
Pattern::Constant(".".into()),
1443+
Pattern::Constant("/".into()),
1444+
Pattern::Constant("abc.js".into()),
1445+
]),
1446+
&js_to_ts_tsx
1447+
),
1448+
Pattern::Concatenation(vec![
1449+
Pattern::Constant(".".into()),
1450+
Pattern::Constant("/".into()),
1451+
Pattern::Concatenation(vec![
1452+
Pattern::Constant("abc".into()),
1453+
Pattern::Alternatives(vec![
1454+
Pattern::Constant(".ts".into()),
1455+
Pattern::Constant(".tsx".into()),
1456+
Pattern::Constant(".js".into()),
1457+
])
1458+
]),
1459+
])
1460+
);
1461+
}
13611462
}

crates/turbopack-resolve/src/typescript.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ pub async fn read_from_tsconfigs<T>(
215215
pub struct TsConfigResolveOptions {
216216
base_url: Option<Vc<FileSystemPath>>,
217217
import_map: Option<Vc<ImportMap>>,
218+
is_module_resolution_nodenext: bool,
218219
}
219220

220221
#[turbo_tasks::value_impl]
@@ -318,9 +319,18 @@ pub async fn tsconfig_resolve_options(
318319
None
319320
};
320321

322+
let is_module_resolution_nodenext = read_from_tsconfigs(&configs, |json, _| {
323+
json["compilerOptions"]["moduleResolution"]
324+
.as_str()
325+
.map(|module_resolution| module_resolution.eq_ignore_ascii_case("nodenext"))
326+
})
327+
.await?
328+
.unwrap_or_default();
329+
321330
Ok(TsConfigResolveOptions {
322331
base_url,
323332
import_map,
333+
is_module_resolution_nodenext,
324334
}
325335
.cell())
326336
}
@@ -352,6 +362,9 @@ pub async fn apply_tsconfig_resolve_options(
352362
.unwrap_or(tsconfig_import_map),
353363
);
354364
}
365+
resolve_options.enable_typescript_with_output_extension =
366+
tsconfig_resolve_options.is_module_resolution_nodenext;
367+
355368
Ok(resolve_options.cell())
356369
}
357370

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import foo from "./src/foo.js";
2+
import bar from "./src/bar.js";
3+
import fooEsm from "./src/foo-esm.mjs";
4+
import fooCjs from "./src/foo-cjs.cjs";
5+
6+
it("should correctly resolve explicit extensions with nodenext", () => {
7+
expect(foo).toBe("foo");
8+
expect(bar).toBe("bar");
9+
expect(fooEsm).toBe("fooEsm");
10+
expect(fooCjs).toBe("fooCjs");
11+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "bar";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "fooCjs";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "fooEsm";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error("Should have a lower precedence than foo.ts");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "foo";

0 commit comments

Comments
 (0)