Skip to content

fix: Resolve package-name-shaped tsconfig path aliases#11998

Merged
anthonyshew merged 8 commits intovercel:mainfrom
sleitor:fix/boundaries-tsconfig-path-aliases
Mar 11, 2026
Merged

fix: Resolve package-name-shaped tsconfig path aliases#11998
anthonyshew merged 8 commits intovercel:mainfrom
sleitor:fix/boundaries-tsconfig-path-aliases

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented Feb 25, 2026

Problem

When a tsconfig.json maps a bare specifier that looks like a package name to a local source path:

{ "compilerOptions": { "paths": { "*": ["./src/*"] } } }

an import like:

import { feature_A } from "features/feature-a";

resolves locally (TypeScript handles it correctly), but turbo boundaries incorrectly reports:

× cannot import package `features` because it is not a dependency

This was introduced in v2.8.7. See #11906 for a reproduction.

Root cause

check_import_as_tsconfig_path_alias had an early-return guard:

if import.starts_with('.') || BoundariesChecker::is_potential_package_name(import) {
    return Ok(false);
}

This prevented the resolver from attempting to resolve any import whose specifier looks like a package name (e.g. features/feature-a). Those imports then fell through to check_package_import, which flagged them as undeclared dependencies.

Fix

Remove the is_potential_package_name guard so all non-relative imports are first tried against the tsconfig resolver:

  • If the resolver resolves the import to a local file → the function returns true and the check stops (no false positive).
  • If the resolver resolves to a path through node_modules → returns false (real npm package, falls through to dependency check).
  • If the resolver fails → returns false (import falls through to check_package_import as before).

Additionally:

  • Structured error handling: replaced blanket Err(_) => Ok(false) with explicit matching on expected errors (NotFound, Builtin, etc.) and tracing::debug! logging for unexpected errors (I/O, broken tsconfig) to aid debugging.
  • Doc comments: added documentation to check_import, check_file_import, get_package_name, and is_potential_package_name.
  • Fixed pre-existing macOS test failures: canonicalized temp dir paths in tests to match the resolver's symlink-resolved output (/tmp/private/tmp).

Tests

  • Renamed tsconfig_alias_check_skips_bare_package_namestsconfig_alias_check_returns_false_for_unresolvable_package_imports to reflect the corrected semantics (the behaviour is unchanged: unresolvable package imports still return false).
  • Added tsconfig_alias_resolves_package_name_shaped_path_alias: regression test that mirrors the exact scenario in turbo boundaries does not understand tsconfig path aliases, incorrectly flags local imports as undeclared dependencies #11906 ("*": ["./src/*"] alias, features/feature-a import).
  • Added tsconfig_alias_check_returns_false_for_node_modules_resolution: verifies the node_modules bail-out path with a real node_modules/ fixture on disk.
  • Added tsconfig_alias_flags_boundary_violation_for_out_of_package_resolution: verifies a tsconfig alias pointing outside the package root produces an ImportLeavesPackage diagnostic.
  • Added warnings.is_empty() assertions to regression tests.

Fixes #11906

When a tsconfig `paths` entry maps a bare specifier that looks like a
package name (e.g. `features/feature-a` → `./src/features/feature-a`)
`turbo boundaries` incorrectly flagged the import as an undeclared
dependency.

The root cause was an early-return guard in
`check_import_as_tsconfig_path_alias` that skipped any import matching
`is_potential_package_name`. This prevented the resolver from even
attempting to resolve such imports as tsconfig aliases, causing them to
fall through to `check_package_import` which then raised a violation.

Fix: remove the `is_potential_package_name` guard so that all
non-relative imports are tried against the resolver. If the import
resolves to a local file the function returns `true` (no further checks);
if it cannot be resolved it returns `false` and the call site falls
through to `check_package_import` as before.

Adds a regression test (`tsconfig_alias_resolves_package_name_shaped_path_alias`)
that reproduces the exact setup reported in the issue.

Fixes vercel#11906
@sleitor sleitor requested a review from a team as a code owner February 25, 2026 18:37
@sleitor sleitor requested review from tknickman and removed request for a team February 25, 2026 18:37
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 25, 2026

@sleitor is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread crates/turborepo-boundaries/src/imports.rs
When check_import_as_tsconfig_path_alias resolves an import to a path
inside node_modules, return false so the caller falls through to
check_package_import for proper dependency validation.
@sleitor
Copy link
Copy Markdown
Contributor Author

sleitor commented Feb 26, 2026

Fixed in latest commit (83be5c2).

The issue was that check_import_as_tsconfig_path_alias returned true for any import the resolver could resolve — including bare package names like lodash that resolve to node_modules/. This bypassed check_package_import entirely.

The fix adds a check after resolution: if the resolved path contains a node_modules component, return false so the import falls through to proper dependency validation via check_package_import. Only genuinely local paths (resolved via tsconfig paths aliases) now return true.

Copy link
Copy Markdown
Contributor

@anthonyshew anthonyshew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for wanting to contribute.

  • There's a failing regression test. We, of course, need for it to pass to be able to merge.
  • The node_modules check is a bit problematic. Depending on the package manager, the package might be in the root of the repository, behind a symlink, in a cache store somewhere else on disk, etc. Let's switch this to either:
    1. check_file_import like at line 264
    2. resolution.package_json() from unrs_resolver (probably the better choice)
  • is_potential_package_name used to be an O(1) string check that skipped most imports. Now its called on every bare import, representing a massive performance regression

Let's get these cleaned up for a first review pass, and then we will see if we can merge. Thanks again!

… guard tsconfig check with is_potential_package_name

- Replace node_modules path component check with resolution.package_json().is_some()
  per anthonyshew's suggestion: if the resolver found a package.json the resolution
  went through node_modules (a real npm package), not a tsconfig path alias.
  More robust than string path component matching.

- Move check_import_as_tsconfig_path_alias inside the is_potential_package_name guard
  to restore O(1) filtering for non-package-name-shaped imports (node:fs, bun:test,
  etc.). Previously the resolver was invoked for every bare import which regressed
  performance. Now the resolver is only called for package-name-shaped imports.
@sleitor
Copy link
Copy Markdown
Contributor Author

sleitor commented Mar 4, 2026

Thanks for the review @anthonyshew! Addressed all three points in the latest commit:

1. Failing regression test — The only failing CI check now is "Validate PR title" (a format lint). All code tests pass locally (cargo test -p turborepo-boundaries: 24/24 ✅). If there's a specific regression test you had in mind from a previous CI run, please let me know and I'll fix it.

2. node_modules check — Switched to resolution.package_json().is_some() as suggested. If the resolver found a package.json the resolution went through node_modules (a real npm package), not a tsconfig path alias pointing to a local file. More reliable than the string path component check.

3. is_potential_package_name performance — Moved check_import_as_tsconfig_path_alias inside the is_potential_package_name guard. Non-package-name-shaped imports (node:fs, bun:test, etc.) now skip the resolver entirely, exactly as before. The resolver is only called for package-name-shaped imports — first trying tsconfig alias resolution, then falling through to check_package_import if no alias matches.

Comment thread crates/turborepo-boundaries/src/imports.rs Outdated
…) 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.
@sleitor
Copy link
Copy Markdown
Contributor Author

sleitor commented Mar 4, 2026

Good catch from VADE — fixed in the latest commit (500e7d9).

The resolution.package_json().is_some() check was indeed incorrect: oxc_resolver returns a package_json for any file within a package directory, not just node_modules packages. So in a real project (where the source package has its own package.json), a tsconfig path alias resolving to a local file would still cause package_json() to return Some, incorrectly returning false and falling through to check_package_import.

The fix switches to checking whether the resolved path contains a node_modules component — the canonical way to distinguish npm packages from local source files. Also added a regression test (tsconfig_alias_resolves_with_package_json_present) that replicates the real-project scenario and confirms the alias is now correctly resolved even when package.json is present.

@sleitor sleitor requested a review from anthonyshew March 4, 2026 17:36
@sleitor sleitor changed the title fix(boundaries): resolve package-name-shaped tsconfig path aliases fix: Resolve package-name-shaped tsconfig path aliases Mar 4, 2026
Structured resolver error handling to distinguish expected failures
(NotFound, Builtin) from unexpected ones (I/O, broken tsconfig) with
tracing::debug logging for the latter. Added test coverage for the
node_modules bail-out path and out-of-package alias boundary violations.
Fixed pre-existing macOS test failures caused by /tmp symlink
canonicalization mismatch. Added doc comments to key functions.
Copy link
Copy Markdown
Contributor

@anthonyshew anthonyshew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for getting us started. I made a few more changes and now we're good to go. Thanks!

…ig-path-aliases

# Conflicts:
#	crates/turborepo-boundaries/src/imports.rs
@anthonyshew anthonyshew enabled auto-merge (squash) March 11, 2026 21:49
Workspace packages symlinked in node_modules resolve to their real
path (without node_modules in it), so the node_modules path check
alone is insufficient. Added a package.json name check: if the
resolution's package.json has a name matching the import's package
name, it's an actual workspace/npm package, not a tsconfig alias.

Also moved tsconfig alias resolution to cover all non-relative imports
(not just package-name-shaped ones) so that aliases like `!` and
`@/foo` are correctly resolved.

Fixed Windows test failure by using dunce::canonicalize to avoid the
\\?\ prefix that breaks path comparison.
@anthonyshew anthonyshew merged commit 0a3166b into vercel:main Mar 11, 2026
44 of 54 checks passed
github-actions Bot added a commit that referenced this pull request Mar 11, 2026
## Release v2.8.17-canary.4

Versioned docs: https://v2-8-17-canary-4.turborepo.dev

### Changes

- release(turborepo): 2.8.17-canary.3 (#12248) (`b3cfcdb`)
- fix: Resolve package-name-shaped tsconfig path aliases (#11998)
(`0a3166b`)

Co-authored-by: Turbobot <turbobot@vercel.com>
@sleitor sleitor deleted the fix/boundaries-tsconfig-path-aliases branch April 16, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

turbo boundaries does not understand tsconfig path aliases, incorrectly flags local imports as undeclared dependencies

2 participants