Skip to content

Commit

Permalink
Fix TSConfig extends pointing to node module (vercel/turborepo#4772)
Browse files Browse the repository at this point in the history
In #4754, I screwed up the resolution logic for configs that reference
another config inside a node module. In it, I incorrectly joined
`/tsconfig.json` before performing any lookup, because I thought the
joining was happening in the `combinePaths` in
https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3315.
But, that combine has no real affect, it immediately gets stripped in a
call to `getDirectoryPath` in
https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/moduleNameResolver.ts#LL1703C93-L1703.

Instead, it performs a basic `node_module` lookup using the input module
name, and even tries to load the file referenced at that location (eg,
it's a `extends: 'foo/tsconfig.json'`). If that fails, then it combines
with an implied `/tsconfig` the same way a `/index` is appended when
doing `require('./directory')`.

Fixes WEB-974
  • Loading branch information
jridgewell authored May 2, 2023
1 parent a039a4b commit 9b9f971
Show file tree
Hide file tree
Showing 29 changed files with 3,593 additions and 16 deletions.
41 changes: 26 additions & 15 deletions crates/turbopack-ecmascript/src/typescript/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,20 @@ pub struct TsConfigIssue {
pub message: StringVc,
}

#[turbo_tasks::function]
async fn json_only(resolve_options: ResolveOptionsVc) -> Result<ResolveOptionsVc> {
let mut opts = resolve_options.await?.clone_value();
opts.extensions = vec![".json".to_string()];
Ok(opts.cell())
}

pub async fn read_tsconfigs(
mut data: FileContentVc,
mut tsconfig: AssetVc,
resolve_options: ResolveOptionsVc,
) -> Result<Vec<(FileJsonContentVc, AssetVc)>> {
let mut configs = Vec::new();
let resolve_options = json_only(resolve_options);
loop {
let parsed_data = data.parse_json_with_comments();
match &*parsed_data.await? {
Expand Down Expand Up @@ -113,14 +121,16 @@ async fn resolve_extends(
let request = RequestVc::parse_string(extends.to_string());

// TS's resolution is weird, and has special behavior for different import
// types.
let extends = match &*request.await? {
// types. There might be multiple alternatives like
// "some/path/node_modules/xyz/abc.json" and "some/node_modules/xyz/abc.json".
// We only want to use the first one.
match &*request.await? {
// TS has special behavior for "rooted" paths (absolute paths):
// https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3303-L3313
Request::Windows { path: Pattern::Constant(path) } |
// Server relative is treated as absolute
Request::ServerRelative { path: Pattern::Constant(path) } => {
return resolve_extends_rooted_or_relative(context, request, resolve_options, path).await;
resolve_extends_rooted_or_relative(context, request, resolve_options, path).await
}

// TS has special behavior for (explicitly) './' and '../', but not '.' nor '..':
Expand All @@ -129,25 +139,26 @@ async fn resolve_extends(
path: Pattern::Constant(path),
..
} if path.starts_with("./") || path.starts_with("../") => {
return resolve_extends_rooted_or_relative(context, request, resolve_options, path).await;
resolve_extends_rooted_or_relative(context, request, resolve_options, path).await
}

// An empty extends is treated as "tsconfig.json"
// An empty extends is treated as "./tsconfig"
Request::Empty => {
"tsconfig.json".to_string()
let request = RequestVc::parse_string("./tsconfig".to_string());
Ok(resolve(context, request, resolve_options).first_asset())
}
// All other types are treated as module imports, and joined with

// All other types are treated as module imports, and potentially joined with
// "tsconfig.json". This includes "relative" imports like '.' and '..'.
_ => {
format!("{extends}/tsconfig.json")
let mut result = resolve(context, request, resolve_options).first_asset();
if result.await?.is_none() {
let request = RequestVc::parse_string(format!("{extends}/tsconfig"));
result = resolve(context, request, resolve_options).first_asset();
}
Ok(result)
}
};

let request = RequestVc::parse_string(extends);
// There might be multiple alternatives like
// "some/path/node_modules/xyz/abc.json" and "some/node_modules/xyz/abc.json".
// We only want to use the first one.
Ok(resolve(context, request, resolve_options).first_asset())
}
}

async fn resolve_extends_rooted_or_relative(
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/turbopack-tests/tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"styled-components": "^100000",
"react": "1000000000000000000",
"@emotion/react": "1000000000",
"@emotion/styled": "100000000"
"@emotion/styled": "100000000",
"tsconfig-mod": "100000000000"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const prop = "prop";
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { prop as globalFoo } from "foo";
import { prop as localFoo } from "./foo";
import { prop as atFoo } from "@/foo";

console.log(globalFoo, localFoo, atFoo);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "tsconfig-mod/tsconfig.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"entry": "input/index.ts"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9b9f971

Please sign in to comment.