Skip to content

Commit d6504c2

Browse files
authored
Add duplicate package detection to rewatch (#8180)
* Add duplicate package detection to rewatch * Add changelog * Warn to stderr * Set runtime env var * fmt
1 parent 522c2a3 commit d6504c2

File tree

4 files changed

+48
-31
lines changed

4 files changed

+48
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#### :bug: Bug fix
2525

2626
- Reanalyze: fix reactive/server stale results when cross-file references change without changing dead declarations (non-transitive mode). https://github.com/rescript-lang/rescript/pull/8173
27+
- Add duplicate package detection to rewatch. https://github.com/rescript-lang/rescript/pull/8180
2728

2829
#### :memo: Documentation
2930

rewatch/src/build/packages.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ pub fn read_dependency(
284284
/// registered for the parent packages. Especially relevant for peerDependencies.
285285
/// 2. In parallel performs IO to read the dependencies config and
286286
/// recursively continues operation for their dependencies as well.
287+
/// 3. Detects and warns about duplicate packages (same name, different paths).
287288
fn read_dependencies(
288289
registered_dependencies_set: &mut AHashSet<String>,
289290
project_context: &ProjectContext,
@@ -302,6 +303,37 @@ fn read_dependencies(
302303
.iter()
303304
.filter_map(|package_name| {
304305
if registered_dependencies_set.contains(package_name) {
306+
// Package already registered - check for duplicate (different path)
307+
// Re-resolve from current package and from root to compare paths
308+
if let Ok(current_path) = read_dependency(package_name, package_config, project_context)
309+
&& let Ok(chosen_path) = read_dependency(package_name, &project_context.current_config, project_context)
310+
&& current_path != chosen_path
311+
{
312+
// Different paths - this is a duplicate
313+
let root_path = project_context.get_root_path();
314+
let chosen_relative = chosen_path
315+
.strip_prefix(root_path)
316+
.unwrap_or(&chosen_path);
317+
let duplicate_relative = current_path
318+
.strip_prefix(root_path)
319+
.unwrap_or(&current_path);
320+
let current_package_path = package_config
321+
.path
322+
.parent()
323+
.map(|p| p.to_path_buf())
324+
.unwrap_or_else(|| PathBuf::from("."));
325+
let parent_relative = current_package_path
326+
.strip_prefix(root_path)
327+
.unwrap_or(&current_package_path);
328+
329+
eprintln!(
330+
"Duplicated package: {} ./{} (chosen) vs ./{} in ./{}",
331+
package_name,
332+
chosen_relative.to_string_lossy(),
333+
duplicate_relative.to_string_lossy(),
334+
parent_relative.to_string_lossy()
335+
);
336+
}
305337
None
306338
} else {
307339
registered_dependencies_set.insert(package_name.to_owned());
@@ -481,6 +513,7 @@ This inconsistency will cause issues with package resolution.\n",
481513
fn read_packages(project_context: &ProjectContext, show_progress: bool) -> Result<AHashMap<String, Package>> {
482514
// Store all packages and completely deduplicate them
483515
let mut map: AHashMap<String, Package> = AHashMap::new();
516+
484517
let current_package = {
485518
let config = &project_context.current_config;
486519
let folder = config
@@ -500,6 +533,7 @@ fn read_packages(project_context: &ProjectContext, show_progress: bool) -> Resul
500533
show_progress,
501534
/* is local dep */ true,
502535
));
536+
503537
dependencies.iter().for_each(|d| {
504538
if !map.contains_key(&d.name) {
505539
let package = make_package(d.config.to_owned(), &d.path, false, d.is_local_dep);
Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,27 @@
11
// @ts-check
22

3-
import * as fs from "node:fs/promises";
3+
import * as assert from "node:assert";
4+
import { runtimePath } from "#cli/runtime";
45
import { setup } from "#dev/process";
56

6-
const { execBuildLegacy, execCleanLegacy } = setup(import.meta.dirname);
7+
// Set runtime path for rewatch to find
8+
process.env.RESCRIPT_RUNTIME = runtimePath;
79

8-
const expectedFilePath = "./out.expected";
9-
10-
const updateTests = process.argv[2] === "update";
11-
12-
/**
13-
* @param {string} output
14-
* @return {string}
15-
*/
16-
function postProcessErrorOutput(output) {
17-
return output.trimEnd().replace(new RegExp(import.meta.dirname, "gi"), ".");
18-
}
10+
const { execBuild, execClean } = setup(import.meta.dirname);
1911

2012
if (process.platform === "win32") {
2113
console.log("Skipping test on Windows");
2214
process.exit(0);
2315
}
2416

25-
await execCleanLegacy();
26-
const { stderr } = await execBuildLegacy();
17+
await execClean();
18+
const { stderr } = await execBuild();
19+
20+
const expectedWarning =
21+
"Duplicated package: z ./node_modules/z (chosen) vs ./a/node_modules/z in ./a";
2722

28-
const actualErrorOutput = postProcessErrorOutput(stderr.toString());
29-
if (updateTests) {
30-
await fs.writeFile(expectedFilePath, actualErrorOutput);
31-
} else {
32-
const expectedErrorOutput = postProcessErrorOutput(
33-
await fs.readFile(expectedFilePath, { encoding: "utf-8" }),
23+
if (!stderr.includes(expectedWarning)) {
24+
assert.fail(
25+
`Expected duplicate package warning not found in stderr.\nExpected: ${expectedWarning}\nActual stderr:\n${stderr}`,
3426
);
35-
if (expectedErrorOutput !== actualErrorOutput) {
36-
console.error(`The old and new error output aren't the same`);
37-
console.error("\n=== Old:");
38-
console.error(expectedErrorOutput);
39-
console.error("\n=== New:");
40-
console.error(actualErrorOutput);
41-
process.exit(1);
42-
}
4327
}

tests/build_tests/duplicated_symlinked_packages/out.expected

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)