@@ -47,27 +47,61 @@ impl PackageInference {
4747 pkg_inference_path
4848 ) ;
4949 let full_inference_path = turbo_root. resolve ( pkg_inference_path) ;
50+
51+ // Track the best matching package (the one whose path is the longest prefix
52+ // of the inference path, i.e., the most specific package containing our
53+ // current directory)
54+ let mut best_match: Option < ( String , AnchoredSystemPathBuf ) > = None ;
55+ let mut found_package_below = false ;
56+
5057 for ( workspace_name, workspace_entry) in pkg_graph. packages ( ) {
5158 let pkg_path = turbo_root. resolve ( workspace_entry. package_path ( ) ) ;
59+
60+ // Check if the inference path is inside this package (pkg_path is a prefix of
61+ // full_inference_path)
5262 let inferred_path_is_below = pkg_path. contains ( & full_inference_path) ;
63+
5364 // We skip over the root package as the inferred path will always be below it
5465 if inferred_path_is_below && ( & pkg_path as & AbsoluteSystemPath ) != turbo_root {
55- // set both. The user might have set a parent directory filter,
56- // in which case we *should* fail to find any packages, but we should
57- // do so in a consistent manner
58- return Self {
59- package_name : Some ( workspace_name . to_string ( ) ) ,
60- directory_root : workspace_entry . package_path ( ) . to_owned ( ) ,
66+ // This package contains the inference path. Track it if it's a better
67+ // (longer/more specific) match than what we've found so far.
68+ let pkg_path_len = workspace_entry . package_path ( ) . as_str ( ) . len ( ) ;
69+ let is_better_match = match & best_match {
70+ None => true ,
71+ Some ( ( _ , existing_path ) ) => pkg_path_len > existing_path . as_str ( ) . len ( ) ,
6172 } ;
73+
74+ if is_better_match {
75+ best_match = Some ( (
76+ workspace_name. to_string ( ) ,
77+ workspace_entry. package_path ( ) . to_owned ( ) ,
78+ ) ) ;
79+ }
6280 }
81+
82+ // Check if this package is below the inference path (full_inference_path is a
83+ // prefix of pkg_path)
6384 let inferred_path_is_between_root_and_pkg = full_inference_path. contains ( & pkg_path) ;
6485 if inferred_path_is_between_root_and_pkg {
65- // we've found *some* package below our inference directory. We can stop now and
66- // conclude that we're looking for all packages in a
67- // subdirectory
68- break ;
86+ // We've found *some* package below our inference directory
87+ found_package_below = true ;
6988 }
7089 }
90+
91+ // If we found a package that contains our inference path, use it
92+ if let Some ( ( package_name, directory_root) ) = best_match {
93+ return Self {
94+ package_name : Some ( package_name) ,
95+ directory_root,
96+ } ;
97+ }
98+
99+ // If we found packages below the inference path, or no packages matched at all,
100+ // use the inference path as the directory root
101+ if found_package_below {
102+ // We're in a directory that contains packages
103+ }
104+
71105 Self {
72106 package_name : None ,
73107 directory_root : pkg_inference_path. to_owned ( ) ,
@@ -718,7 +752,10 @@ pub enum ResolutionError {
718752
719753#[ cfg( test) ]
720754mod test {
721- use std:: collections:: { HashMap , HashSet } ;
755+ use std:: {
756+ collections:: { HashMap , HashSet } ,
757+ str:: FromStr ,
758+ } ;
722759
723760 use pretty_assertions:: assert_eq;
724761 use tempfile:: TempDir ;
@@ -1390,4 +1427,268 @@ mod test {
13901427 . expect ( "unsupported range" ) )
13911428 }
13921429 }
1430+
1431+ /// Creates a package graph for testing PackageInference::calculate
1432+ fn make_package_graph (
1433+ package_paths : & [ & str ] ,
1434+ ) -> ( TempDir , & ' static AbsoluteSystemPathBuf , PackageGraph ) {
1435+ let temp_folder = tempfile:: tempdir ( ) . unwrap ( ) ;
1436+ let turbo_root = Box :: leak ( Box :: new (
1437+ AbsoluteSystemPathBuf :: new ( temp_folder. path ( ) . as_os_str ( ) . to_str ( ) . unwrap ( ) ) . unwrap ( ) ,
1438+ ) ) ;
1439+
1440+ let package_jsons = package_paths
1441+ . iter ( )
1442+ . map ( |package_path| {
1443+ let ( _, name) = get_name ( package_path) ;
1444+ (
1445+ turbo_root. join_unix_path (
1446+ RelativeUnixPathBuf :: new ( format ! ( "{package_path}/package.json" ) ) . unwrap ( ) ,
1447+ ) ,
1448+ PackageJson {
1449+ name : Some ( Spanned :: new ( name. to_string ( ) ) ) ,
1450+ ..Default :: default ( )
1451+ } ,
1452+ )
1453+ } )
1454+ . collect :: < HashMap < _ , _ > > ( ) ;
1455+
1456+ for package_dir in package_jsons. keys ( ) {
1457+ package_dir. ensure_dir ( ) . unwrap ( ) ;
1458+ }
1459+
1460+ let graph = {
1461+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
1462+ . enable_all ( )
1463+ . build ( )
1464+ . unwrap ( ) ;
1465+ rt. block_on (
1466+ PackageGraph :: builder ( turbo_root, Default :: default ( ) )
1467+ . with_package_discovery ( MockDiscovery )
1468+ . with_package_jsons ( Some ( package_jsons) )
1469+ . build ( ) ,
1470+ )
1471+ . unwrap ( )
1472+ } ;
1473+
1474+ ( temp_folder, turbo_root, graph)
1475+ }
1476+
1477+ /// Test that PackageInference::calculate is deterministic when invoked from
1478+ /// a directory that contains nested packages (regression test for
1479+ /// GitHub issue #11428).
1480+ ///
1481+ /// The bug was that HashMap iteration order is non-deterministic, and the
1482+ /// previous implementation would return early when it found ANY package
1483+ /// below the inference path, rather than finding the most specific one.
1484+ #[ test]
1485+ fn test_package_inference_deterministic_with_nested_packages ( ) {
1486+ // Simulate the structure from the issue:
1487+ // apps/onprem/ <- invocation directory
1488+ // apps/onprem/backend/ <- nested package
1489+ // apps/onprem/web/ <- nested package
1490+ // packages/shared/ <- unrelated package
1491+ let ( _tempdir, turbo_root, graph) =
1492+ make_package_graph ( & [ "apps/onprem/backend" , "apps/onprem/web" , "packages/shared" ] ) ;
1493+
1494+ // Invoke from apps/onprem (a directory containing packages)
1495+ let inference_path = AnchoredSystemPathBuf :: try_from ( "apps/onprem" ) . unwrap ( ) ;
1496+
1497+ // Run the calculation multiple times to verify determinism
1498+ // (with the bug, different runs could give different results)
1499+ for _ in 0 ..10 {
1500+ let inference = PackageInference :: calculate ( turbo_root, & inference_path, & graph) ;
1501+
1502+ // We should NOT infer a specific package - we're in a directory that contains
1503+ // packages, not inside a specific package
1504+ assert ! (
1505+ inference. package_name. is_none( ) ,
1506+ "Expected no package to be inferred when running from a directory containing \
1507+ packages, but got {:?}",
1508+ inference. package_name
1509+ ) ;
1510+
1511+ // The directory root should be the inference path itself
1512+ assert_eq ! (
1513+ inference. directory_root, inference_path,
1514+ "Expected directory_root to be the inference path"
1515+ ) ;
1516+ }
1517+ }
1518+
1519+ /// Test that PackageInference::calculate correctly identifies when we're
1520+ /// inside a specific package (at or below the package root).
1521+ #[ test]
1522+ fn test_package_inference_inside_package ( ) {
1523+ let ( _tempdir, turbo_root, graph) =
1524+ make_package_graph ( & [ "apps/onprem/backend" , "apps/onprem/web" , "packages/shared" ] ) ;
1525+
1526+ // Invoke from inside apps/onprem/backend
1527+ let inference_path = AnchoredSystemPathBuf :: try_from ( "apps/onprem/backend" ) . unwrap ( ) ;
1528+ let inference = PackageInference :: calculate ( turbo_root, & inference_path, & graph) ;
1529+
1530+ assert_eq ! (
1531+ inference. package_name,
1532+ Some ( "backend" . to_string( ) ) ,
1533+ "Expected to infer 'backend' package"
1534+ ) ;
1535+ assert_eq ! (
1536+ inference. directory_root,
1537+ AnchoredSystemPathBuf :: try_from( "apps/onprem/backend" ) . unwrap( )
1538+ ) ;
1539+
1540+ // Invoke from a subdirectory inside apps/onprem/backend/src
1541+ let inference_path = AnchoredSystemPathBuf :: try_from ( "apps/onprem/backend/src" ) . unwrap ( ) ;
1542+ let inference = PackageInference :: calculate ( turbo_root, & inference_path, & graph) ;
1543+
1544+ assert_eq ! (
1545+ inference. package_name,
1546+ Some ( "backend" . to_string( ) ) ,
1547+ "Expected to infer 'backend' package from subdirectory"
1548+ ) ;
1549+ assert_eq ! (
1550+ inference. directory_root,
1551+ AnchoredSystemPathBuf :: try_from( "apps/onprem/backend" ) . unwrap( )
1552+ ) ;
1553+ }
1554+
1555+ /// Test that PackageInference selects the most specific (deepest) package
1556+ /// when packages are nested.
1557+ #[ test]
1558+ fn test_package_inference_selects_deepest_package ( ) {
1559+ // Create a structure with nested packages
1560+ let ( _tempdir, turbo_root, graph) = make_package_graph ( & [
1561+ "apps" , // shallow package
1562+ "apps/web" , // deeper package inside apps
1563+ "apps/web/ui" , // even deeper package
1564+ "packages/shared" ,
1565+ ] ) ;
1566+
1567+ // When invoked from apps/web/ui/src, should infer apps/web/ui (the deepest)
1568+ let inference_path = AnchoredSystemPathBuf :: try_from ( "apps/web/ui/src" ) . unwrap ( ) ;
1569+ let inference = PackageInference :: calculate ( turbo_root, & inference_path, & graph) ;
1570+
1571+ assert_eq ! (
1572+ inference. package_name,
1573+ Some ( "ui" . to_string( ) ) ,
1574+ "Expected to infer the deepest package 'ui'"
1575+ ) ;
1576+ assert_eq ! (
1577+ inference. directory_root,
1578+ AnchoredSystemPathBuf :: try_from( "apps/web/ui" ) . unwrap( )
1579+ ) ;
1580+
1581+ // When invoked from apps/web/src (outside ui), should infer apps/web
1582+ let inference_path = AnchoredSystemPathBuf :: try_from ( "apps/web/src" ) . unwrap ( ) ;
1583+ let inference = PackageInference :: calculate ( turbo_root, & inference_path, & graph) ;
1584+
1585+ assert_eq ! (
1586+ inference. package_name,
1587+ Some ( "web" . to_string( ) ) ,
1588+ "Expected to infer 'web' package"
1589+ ) ;
1590+ assert_eq ! (
1591+ inference. directory_root,
1592+ AnchoredSystemPathBuf :: try_from( "apps/web" ) . unwrap( )
1593+ ) ;
1594+ }
1595+
1596+ /// End-to-end test for GitHub issue #11428: running `turbo -F "{./*}^..."
1597+ /// build` from a directory containing packages should select those
1598+ /// packages' dependencies.
1599+ ///
1600+ /// This test verifies that running from apps/onprem with filter {./*}^...
1601+ /// correctly selects packages under apps/onprem/* and their dependencies,
1602+ /// rather than randomly returning 0 packages.
1603+ #[ test]
1604+ fn test_issue_11428_filter_from_directory_with_nested_packages ( ) {
1605+ // Create the structure from the issue:
1606+ // apps/onprem/backend depends on packages/shared
1607+ // apps/onprem/web depends on packages/shared
1608+ let ( _tempdir, resolver) = make_project (
1609+ & [
1610+ ( "apps/onprem/backend" , "packages/shared" ) ,
1611+ ( "apps/onprem/web" , "packages/shared" ) ,
1612+ ] ,
1613+ & [ ] ,
1614+ // Simulate running from apps/onprem
1615+ Some ( PackageInference {
1616+ package_name : None ,
1617+ directory_root : AnchoredSystemPathBuf :: try_from ( "apps/onprem" ) . unwrap ( ) ,
1618+ } ) ,
1619+ TestChangeDetector :: new ( & [ ] ) ,
1620+ ) ;
1621+
1622+ // The filter {./*}^... means:
1623+ // - {./*} = packages matching "./*" relative to inference directory
1624+ // (apps/onprem/*)
1625+ // - ^... = exclude self, include dependencies
1626+ //
1627+ // So this should select: packages/shared (dependency of backend and web)
1628+ // but NOT backend or web themselves (^ excludes self)
1629+ let selector = TargetSelector :: from_str ( "{./*}^..." ) . unwrap ( ) ;
1630+ let packages = resolver. get_filtered_packages ( vec ! [ selector] ) . unwrap ( ) ;
1631+
1632+ // We should get the dependencies of apps/onprem/* packages
1633+ // which is packages/shared
1634+ assert ! (
1635+ !packages. is_empty( ) ,
1636+ "Expected to find packages, but got none. This is the bug from issue #11428!"
1637+ ) ;
1638+
1639+ assert ! (
1640+ packages. contains_key( & PackageName :: from( "shared" ) ) ,
1641+ "Expected 'shared' to be selected as a dependency. Got: {:?}" ,
1642+ packages. keys( ) . collect:: <Vec <_>>( )
1643+ ) ;
1644+
1645+ // backend and web should NOT be included (^ excludes self)
1646+ assert ! (
1647+ !packages. contains_key( & PackageName :: from( "backend" ) ) ,
1648+ "backend should be excluded due to ^ in filter"
1649+ ) ;
1650+ assert ! (
1651+ !packages. contains_key( & PackageName :: from( "web" ) ) ,
1652+ "web should be excluded due to ^ in filter"
1653+ ) ;
1654+ }
1655+
1656+ /// Test that running from a directory containing packages with filter {./*}
1657+ /// (without dependency traversal) selects those packages directly.
1658+ #[ test]
1659+ fn test_filter_from_directory_selects_child_packages ( ) {
1660+ let ( _tempdir, resolver) = make_project (
1661+ & [
1662+ ( "apps/onprem/backend" , "packages/shared" ) ,
1663+ ( "apps/onprem/web" , "packages/shared" ) ,
1664+ ] ,
1665+ & [ ] ,
1666+ Some ( PackageInference {
1667+ package_name : None ,
1668+ directory_root : AnchoredSystemPathBuf :: try_from ( "apps/onprem" ) . unwrap ( ) ,
1669+ } ) ,
1670+ TestChangeDetector :: new ( & [ ] ) ,
1671+ ) ;
1672+
1673+ // {./*} without ^... should select the packages themselves
1674+ let selector = TargetSelector :: from_str ( "{./*}" ) . unwrap ( ) ;
1675+ let packages = resolver. get_filtered_packages ( vec ! [ selector] ) . unwrap ( ) ;
1676+
1677+ assert ! (
1678+ packages. contains_key( & PackageName :: from( "backend" ) ) ,
1679+ "Expected 'backend' to be selected. Got: {:?}" ,
1680+ packages. keys( ) . collect:: <Vec <_>>( )
1681+ ) ;
1682+ assert ! (
1683+ packages. contains_key( & PackageName :: from( "web" ) ) ,
1684+ "Expected 'web' to be selected. Got: {:?}" ,
1685+ packages. keys( ) . collect:: <Vec <_>>( )
1686+ ) ;
1687+
1688+ // shared should NOT be selected (it's not under apps/onprem/*)
1689+ assert ! (
1690+ !packages. contains_key( & PackageName :: from( "shared" ) ) ,
1691+ "shared should not be selected as it's not under apps/onprem/*"
1692+ ) ;
1693+ }
13931694}
0 commit comments