Skip to content

Commit 500e7d9

Browse files
committed
fix(boundaries): detect node_modules by path instead of package_json() presence
The previous approach used `resolution.package_json().is_some()` to distinguish npm packages from local tsconfig path alias targets. However, oxc_resolver may return a package.json for any file inside a package directory — including files reached via tsconfig `paths` aliases in a package that has its own package.json. Switch to checking whether the resolved path contains a `node_modules` component, which is the canonical way to distinguish npm packages from local source files. Also adds a regression test that verifies alias resolution still works when a package.json is present in the package root.
1 parent a94be96 commit 500e7d9

1 file changed

Lines changed: 73 additions & 7 deletions

File tree

crates/turborepo-boundaries/src/imports.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,19 @@ fn check_import_as_tsconfig_path_alias(
115115

116116
match resolver.resolve(dir, import) {
117117
Ok(resolution) => {
118-
// If the resolver found a package.json the resolution went through
119-
// node_modules (a real npm package), not a tsconfig path alias pointing
120-
// to a local file. Return false so the caller falls through to
121-
// `check_package_import`.
122-
if resolution.package_json().is_some() {
118+
// If the resolved path goes through node_modules, the import resolved to
119+
// a real npm package rather than a tsconfig path alias pointing to a local
120+
// file. Return false so the caller falls through to `check_package_import`.
121+
//
122+
// We intentionally do NOT use `resolution.package_json().is_some()` here
123+
// because oxc_resolver may return a package.json for any file inside a
124+
// package directory — including files that are local tsconfig path alias
125+
// targets. Checking for `node_modules` in the resolved path is the
126+
// canonical way to distinguish npm packages from local source files.
127+
let path = resolution.path();
128+
if path.components().any(|c| c.as_os_str() == "node_modules") {
123129
return Ok(false);
124130
}
125-
let path = resolution.path();
126131
let Some(utf8_path) = Utf8Path::from_path(path) else {
127132
result.diagnostics.push(BoundariesDiagnostic::InvalidPath {
128133
path: path.to_string_lossy().to_string(),
@@ -562,4 +567,65 @@ mod test {
562567
"expected no boundary violations for a locally-aliased import"
563568
);
564569
}
565-
}
570+
/// Regression test: a tsconfig path alias must still be resolved as a local
571+
/// import even when the package root contains a `package.json` file.
572+
///
573+
/// Previously, using `resolution.package_json().is_some()` caused the check
574+
/// to incorrectly treat tsconfig aliases as npm packages in any real project
575+
/// that has a `package.json` in its root directory.
576+
#[test]
577+
fn tsconfig_alias_resolves_with_package_json_present() {
578+
let tmp = tempfile::tempdir().unwrap();
579+
let root = tmp.path();
580+
581+
// Create a package.json so oxc_resolver can find it during resolution
582+
std::fs::write(
583+
root.join("package.json"),
584+
r#"{ "name": "test-pkg", "version": "1.0.0" }"#,
585+
)
586+
.unwrap();
587+
588+
let tsconfig = root.join("tsconfig.json");
589+
std::fs::write(
590+
&tsconfig,
591+
r#"{ "compilerOptions": { "paths": { "@/*": ["./*"] } } }"#,
592+
)
593+
.unwrap();
594+
595+
std::fs::create_dir_all(root.join("utils")).unwrap();
596+
std::fs::write(root.join("utils").join("helper.ts"), "export const x = 1;").unwrap();
597+
598+
let file_content = "import { x } from \"@/utils/helper\";";
599+
std::fs::write(root.join("index.ts"), file_content).unwrap();
600+
601+
let package_root = AbsoluteSystemPath::new(root.to_str().unwrap()).unwrap();
602+
let tsconfig_path = AbsoluteSystemPath::new(tsconfig.to_str().unwrap()).unwrap();
603+
let file_path = package_root.join_component("index.ts");
604+
let package_name = PackageName::from("test-pkg");
605+
let span = SourceSpan::new(0.into(), 0);
606+
let mut result = BoundariesResult::default();
607+
608+
let resolver = Tracer::create_resolver(Some(tsconfig_path));
609+
610+
let resolved = check_import_as_tsconfig_path_alias(
611+
&resolver,
612+
&package_name,
613+
package_root,
614+
span,
615+
&file_path,
616+
file_content,
617+
"@/utils/helper",
618+
&mut result,
619+
)
620+
.unwrap();
621+
622+
assert!(
623+
resolved,
624+
"@/utils/helper should be resolved as a tsconfig path alias even when package.json is present"
625+
);
626+
assert!(
627+
result.diagnostics.is_empty(),
628+
"expected no boundary violations for a locally-aliased import"
629+
);
630+
}
631+
}

0 commit comments

Comments
 (0)