Skip to content

Commit a8ef8b8

Browse files
authored
fix: local files at uneven dir depths (#489)
1 parent 8a0d242 commit a8ef8b8

2 files changed

Lines changed: 240 additions & 14 deletions

File tree

rs-lib/src/mappings.rs

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,43 @@ impl Mappings {
4949
) -> Result<Self> {
5050
let mut mappings = HashMap::new();
5151
let mut mapped_filepaths_no_ext = HashSet::new();
52-
let base_dir = get_base_dir(&specifiers.local)?;
52+
// the main files keep their paths relative to the main files' root so that
53+
// where the tests happen to live doesn't shift the distributed code
54+
let main_specifiers = specifiers
55+
.local
56+
.iter()
57+
.filter(|s| !specifiers.test_modules.contains(s))
58+
.cloned()
59+
.collect::<Vec<_>>();
60+
let main_base_dir = if main_specifiers.is_empty() {
61+
get_base_dir(&specifiers.local)?
62+
} else {
63+
get_base_dir(&main_specifiers)?
64+
};
65+
// the tests may be in a directory outside the main files' root, so they get
66+
// a root that encompasses both
67+
let mut test_base_dir_candidates = vec![main_base_dir.clone()];
68+
for specifier in specifiers
69+
.local
70+
.iter()
71+
.filter(|s| specifiers.test_modules.contains(s))
72+
{
73+
let file_path = url_to_file_path(specifier)?;
74+
test_base_dir_candidates.push(file_path.parent().unwrap().to_path_buf());
75+
}
76+
let test_base_dir = get_common_dir(test_base_dir_candidates);
77+
ensure_nonempty_base_dir(&test_base_dir)?;
5378
let mut root_local_dirs = HashSet::new();
5479

5580
for specifier in specifiers.local.iter() {
5681
let file_path = url_to_file_path(specifier)?;
82+
let base_dir = if specifiers.test_modules.contains(specifier) {
83+
&test_base_dir
84+
} else {
85+
&main_base_dir
86+
};
5787
let relative_file_path =
58-
file_path.strip_prefix(&base_dir).map_err(|_| {
88+
file_path.strip_prefix(base_dir).map_err(|_| {
5989
anyhow::anyhow!(
6090
"Error stripping prefix of {} with base {}",
6191
file_path.display(),
@@ -499,20 +529,37 @@ fn get_base_dir(specifiers: &[ModuleSpecifier]) -> Result<PathBuf> {
499529
if specifiers.is_empty() {
500530
bail!("Did not find any local files. Specifying only remote files is not currently supported.");
501531
}
532+
let base_dir = get_common_dir(
533+
specifiers
534+
.iter()
535+
.map(|specifier| {
536+
Ok(url_to_file_path(specifier)?.parent().unwrap().into())
537+
})
538+
.collect::<Result<Vec<PathBuf>>>()?,
539+
);
540+
ensure_nonempty_base_dir(&base_dir)?;
541+
Ok(base_dir)
542+
}
543+
544+
fn ensure_nonempty_base_dir(base_dir: &Path) -> Result<()> {
545+
if base_dir.as_os_str().is_empty() {
546+
bail!("Local files span different filesystem roots.");
547+
}
548+
Ok(())
549+
}
550+
551+
fn get_common_dir(dirs: impl IntoIterator<Item = PathBuf>) -> PathBuf {
502552
// todo(dsherret): should maybe error on windows when the files
503553
// span different drives...
504-
let mut base_dir = url_to_file_path(&specifiers[0])?
505-
.parent()
506-
.unwrap()
507-
.to_path_buf();
508-
for specifier in specifiers {
509-
let file_path = url_to_file_path(specifier)?;
510-
let parent_dir = file_path.parent().unwrap();
554+
let mut dirs = dirs.into_iter();
555+
let Some(mut base_dir) = dirs.next() else {
556+
return PathBuf::new();
557+
};
558+
for parent_dir in dirs {
511559
if base_dir != parent_dir {
512-
if base_dir.starts_with(parent_dir) {
513-
base_dir = parent_dir.to_path_buf();
514-
} else if base_dir.components().count() == parent_dir.components().count()
515-
{
560+
if base_dir.starts_with(&parent_dir) {
561+
base_dir = parent_dir;
562+
} else {
516563
let mut final_path = PathBuf::new();
517564
for (a, b) in base_dir.components().zip(parent_dir.components()) {
518565
if a == b {
@@ -525,7 +572,7 @@ fn get_base_dir(specifiers: &[ModuleSpecifier]) -> Result<PathBuf> {
525572
}
526573
}
527574
}
528-
Ok(base_dir)
575+
base_dir
529576
}
530577

531578
#[cfg(test)]
@@ -553,6 +600,13 @@ mod test {
553600
vec!["file:///project/b/other.ts", "file:///project/a/other.ts"],
554601
"/project",
555602
);
603+
run_test(
604+
vec![
605+
"file:///project-one/mod.ts",
606+
"file:///project-two/deeper/mod.ts",
607+
],
608+
"/",
609+
);
556610

557611
fn run_test(urls: Vec<&str>, expected: &str) {
558612
let result = get_base_dir(

rs-lib/tests/integration_test.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,178 @@ async fn transform_import_map() {
15861586
);
15871587
}
15881588

1589+
#[tokio::test]
1590+
async fn transform_import_map_uneven_sibling_depths() {
1591+
let result = TestBuilder::new()
1592+
.entry_point("file:///project-one/mod.ts")
1593+
.with_loader(|loader| {
1594+
loader
1595+
.add_local_file(
1596+
"/project-one/mod.ts",
1597+
"import value from 'project-two/deeper/mod.ts';",
1598+
)
1599+
.add_local_file(
1600+
"/project-one/deno.json",
1601+
r#"{
1602+
"imports": {
1603+
"project-two/": "../project-two/"
1604+
}
1605+
}"#,
1606+
)
1607+
.add_local_file(
1608+
"/project-two/deeper/mod.ts",
1609+
"export default 'hello world';",
1610+
);
1611+
})
1612+
.set_import_map("file:///project-one/deno.json")
1613+
.transform()
1614+
.await
1615+
.unwrap();
1616+
1617+
// the sibling is at a different depth, so the shared root moves up to
1618+
// encompass both projects, the same as it already does for siblings at
1619+
// equal depths
1620+
assert_files!(
1621+
result.main.files,
1622+
&[
1623+
(
1624+
"project-one/mod.ts",
1625+
"import value from '../project-two/deeper/mod.js';",
1626+
),
1627+
("project-two/deeper/mod.ts", "export default 'hello world';",),
1628+
]
1629+
);
1630+
assert_eq!(
1631+
result.main.entry_points,
1632+
&[PathBuf::from("project-one/mod.ts")]
1633+
);
1634+
}
1635+
1636+
#[tokio::test]
1637+
async fn transform_import_map_local_sibling_and_remote() {
1638+
let result = TestBuilder::new()
1639+
.entry_point("file:///example-project/mod.ts")
1640+
.with_loader(|loader| {
1641+
loader
1642+
.add_local_file(
1643+
"/example-project/mod.ts",
1644+
"export * from './src/mod.ts';",
1645+
)
1646+
.add_local_file(
1647+
"/example-project/src/mod.ts",
1648+
"export * from 'example-shared/mod.ts';\nexport * from 'https://example.com/mod.ts';",
1649+
)
1650+
.add_local_file(
1651+
"/example-project/deno.json",
1652+
r#"{
1653+
"imports": {
1654+
"example-shared/": "../example-shared/"
1655+
}
1656+
}"#,
1657+
)
1658+
.add_local_file("/example-shared/mod.ts", "export const shared = 'shared';")
1659+
.add_remote_file("https://example.com/mod.ts", "export const remote = 'remote';");
1660+
})
1661+
.set_import_map("file:///example-project/deno.json")
1662+
.transform()
1663+
.await
1664+
.unwrap();
1665+
1666+
// the local sibling shares a root with the project, while the remote module
1667+
// keeps going to `deps`
1668+
assert_files!(
1669+
result.main.files,
1670+
&[
1671+
(
1672+
"example-project/mod.ts",
1673+
"export * from './src/mod.js';",
1674+
),
1675+
(
1676+
"example-project/src/mod.ts",
1677+
"export * from '../../example-shared/mod.js';\nexport * from '../../deps/example.com/mod.js';",
1678+
),
1679+
(
1680+
"example-shared/mod.ts",
1681+
"export const shared = 'shared';",
1682+
),
1683+
(
1684+
"deps/example.com/mod.ts",
1685+
"export const remote = 'remote';",
1686+
),
1687+
]
1688+
);
1689+
assert_eq!(
1690+
result.main.entry_points,
1691+
&[PathBuf::from("example-project/mod.ts")]
1692+
);
1693+
}
1694+
1695+
#[tokio::test]
1696+
async fn transform_entry_point_with_sibling_dir_in_project() {
1697+
let result = TestBuilder::new()
1698+
.entry_point("file:///project/src/mod.ts")
1699+
.with_loader(|loader| {
1700+
loader
1701+
.add_local_file(
1702+
"/project/src/mod.ts",
1703+
"export * from '../lib/util.ts';",
1704+
)
1705+
.add_local_file("/project/lib/util.ts", "export const util = 1;");
1706+
})
1707+
.transform()
1708+
.await
1709+
.unwrap();
1710+
1711+
// a sibling directory of the entry point is still part of the project, so
1712+
// it must not be treated as a dependency
1713+
assert_files!(
1714+
result.main.files,
1715+
&[
1716+
("src/mod.ts", "export * from '../lib/util.js';"),
1717+
("lib/util.ts", "export const util = 1;"),
1718+
]
1719+
);
1720+
assert_eq!(result.main.entry_points, &[PathBuf::from("src/mod.ts")]);
1721+
}
1722+
1723+
#[tokio::test]
1724+
async fn transform_test_entry_point_uses_test_root_without_moving_main() {
1725+
let result = TestBuilder::new()
1726+
.entry_point("file:///example-project/src/mod.ts")
1727+
.with_loader(|loader| {
1728+
loader
1729+
.add_local_file(
1730+
"/example-project/src/mod.ts",
1731+
"export const value = 'value';",
1732+
)
1733+
.add_local_file(
1734+
"/example-project/tests/integration/functions/mod.test.ts",
1735+
"import '../../../src/mod.ts';",
1736+
);
1737+
})
1738+
.add_test_entry_point(
1739+
"file:///example-project/tests/integration/functions/mod.test.ts",
1740+
)
1741+
.transform()
1742+
.await
1743+
.unwrap();
1744+
1745+
// the tests are in a directory outside the main files' root, which must not
1746+
// push the main files down a directory
1747+
assert_eq!(result.main.entry_points, &[PathBuf::from("mod.ts")]);
1748+
assert_eq!(
1749+
result.test.entry_points,
1750+
&[PathBuf::from("tests/integration/functions/mod.test.ts")]
1751+
);
1752+
assert_files!(
1753+
result.test.files,
1754+
&[(
1755+
"tests/integration/functions/mod.test.ts",
1756+
"import '../../../mod.js';",
1757+
)]
1758+
);
1759+
}
1760+
15891761
#[tokio::test]
15901762
async fn transform_config_file() {
15911763
let result = TestBuilder::new()

0 commit comments

Comments
 (0)