Skip to content

Commit

Permalink
Support module_resolution: "nodenext" (vercel/turborepo#8748)
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
mischnic authored Jul 29, 2024
1 parent 0365d92 commit 21fe711
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 2 deletions.
43 changes: 41 additions & 2 deletions crates/turbopack-core/src/resolve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand All @@ -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<Pattern> {
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,
Expand Down
3 changes: 3 additions & 0 deletions crates/turbopack-core/src/resolve/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@ pub struct ResolveOptions {
pub resolved_map: Option<Vc<ResolvedMap>>,
pub before_resolve_plugins: Vec<Vc<Box<dyn BeforeResolvePlugin>>>,
pub plugins: Vec<Vc<Box<dyn AfterResolvePlugin>>>,
/// Support resolving *.js requests to *.ts files
pub enable_typescript_with_output_extension: bool,

pub placeholder_for_future_extensions: (),
}

Expand Down
101 changes: 101 additions & 0 deletions crates/turbopack-core/src/resolve/pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pattern>) -> 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 {
Expand Down Expand Up @@ -1111,6 +1138,7 @@ pub async fn read_matches(
#[cfg(test)]
mod tests {
use rstest::*;
use turbo_tasks::RcStr;

use super::Pattern;

Expand Down Expand Up @@ -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>) -> Pattern {
p.replace_final_constants(cb);
p
}

let js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
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()),
])
]),
])
);
}
}
13 changes: 13 additions & 0 deletions crates/turbopack-resolve/src/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ pub async fn read_from_tsconfigs<T>(
pub struct TsConfigResolveOptions {
base_url: Option<Vc<FileSystemPath>>,
import_map: Option<Vc<ImportMap>>,
is_module_resolution_nodenext: bool,
}

#[turbo_tasks::value_impl]
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default "bar";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = "fooCjs";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default "fooEsm";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
throw new Error("Should have a lower precedence than foo.ts");
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default "foo";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}

0 comments on commit 21fe711

Please sign in to comment.