Skip to content

Commit 2754184

Browse files
authored
fix(install): store tags associated with package in node_modules dir (#26000)
Fixes #25998. Fixes #25928. Originally I was just going to make this an error message instead of a panic, but once I got to a minimal repro I felt that this really should work. The panic occurs when you have `nodeModulesDir: manual` (or a package.json present), and you have an npm package with a tag in your deno.json (see the spec test that illustrates this). This code path only actually executes when trying to choose an appropriate package version from `node_modules/.deno`, so we should be able to fix it by storing some extra data at install time. The fix proposed here is to repurpose the `.initialized` file that we store in `node_modules` to store the tags associated with a package. Basically, if you have a version requirement with a tag (e.g. `npm:chalk@latest`), when we set up the node_modules folder for that package, we store the tag (`latest`) in `.initialized`. Then, when doing BYONM resolution, if we have a version requirement with a tag, we read that file and check if the tag is present. The downside is that we do more work when setting up `node_modules`. We _could_ do this only when BYONM is enabled, but that would have the downside of needing to re-run `deno install` when you switch from auto -> manual, though maybe that's not a big deal.
1 parent 1e0c9b8 commit 2754184

File tree

8 files changed

+109
-3
lines changed

8 files changed

+109
-3
lines changed

cli/npm/managed/resolvers/local.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ async fn sync_resolution_with_fs(
343343
},
344344
);
345345
let packages_with_deprecation_warnings = Arc::new(Mutex::new(Vec::new()));
346+
347+
let mut package_tags: HashMap<&PackageNv, Vec<&str>> = HashMap::new();
348+
for (package_req, package_nv) in snapshot.package_reqs() {
349+
if let Some(tag) = package_req.version_req.tag() {
350+
package_tags.entry(package_nv).or_default().push(tag);
351+
}
352+
}
353+
346354
for package in &package_partitions.packages {
347355
if let Some(current_pkg) =
348356
newest_packages_by_name.get_mut(&package.id.nv.name)
@@ -357,11 +365,29 @@ async fn sync_resolution_with_fs(
357365
let package_folder_name =
358366
get_package_folder_id_folder_name(&package.get_package_cache_folder_id());
359367
let folder_path = deno_local_registry_dir.join(&package_folder_name);
368+
let tags = package_tags
369+
.get(&package.id.nv)
370+
.map(|tags| tags.join(","))
371+
.unwrap_or_default();
372+
enum PackageFolderState {
373+
UpToDate,
374+
Uninitialized,
375+
TagsOutdated,
376+
}
360377
let initialized_file = folder_path.join(".initialized");
378+
let package_state = std::fs::read_to_string(&initialized_file)
379+
.map(|s| {
380+
if s != tags {
381+
PackageFolderState::TagsOutdated
382+
} else {
383+
PackageFolderState::UpToDate
384+
}
385+
})
386+
.unwrap_or(PackageFolderState::Uninitialized);
361387
if !cache
362388
.cache_setting()
363389
.should_use_for_npm_package(&package.id.nv.name)
364-
|| !initialized_file.exists()
390+
|| matches!(package_state, PackageFolderState::Uninitialized)
365391
{
366392
// cache bust the dep from the dep setup cache so the symlinks
367393
// are forced to be recreated
@@ -371,6 +397,7 @@ async fn sync_resolution_with_fs(
371397
let bin_entries_to_setup = bin_entries.clone();
372398
let packages_with_deprecation_warnings =
373399
packages_with_deprecation_warnings.clone();
400+
374401
cache_futures.push(async move {
375402
tarball_cache
376403
.ensure_package(&package.id.nv, &package.dist)
@@ -389,7 +416,7 @@ async fn sync_resolution_with_fs(
389416
move || {
390417
clone_dir_recursive(&cache_folder, &package_path)?;
391418
// write out a file that indicates this folder has been initialized
392-
fs::write(initialized_file, "")?;
419+
fs::write(initialized_file, tags)?;
393420

394421
Ok::<_, AnyError>(())
395422
}
@@ -410,6 +437,8 @@ async fn sync_resolution_with_fs(
410437
drop(pb_guard); // explicit for clarity
411438
Ok::<_, AnyError>(())
412439
});
440+
} else if matches!(package_state, PackageFolderState::TagsOutdated) {
441+
fs::write(initialized_file, tags)?;
413442
}
414443

415444
let sub_node_modules = folder_path.join("node_modules");

resolvers/deno/npm/byonm.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,24 @@ impl<Fs: DenoResolverFs> ByonmNpmResolver<Fs> {
253253
let Ok(version) = Version::parse_from_npm(version) else {
254254
continue;
255255
};
256-
if req.version_req.matches(&version) {
256+
if let Some(tag) = req.version_req.tag() {
257+
let initialized_file =
258+
node_modules_deno_dir.join(&entry.name).join(".initialized");
259+
let Ok(contents) = self.fs.read_to_string_lossy(&initialized_file)
260+
else {
261+
continue;
262+
};
263+
let mut tags = contents.split(',').map(str::trim);
264+
if tags.any(|t| t == tag) {
265+
if let Some((best_version_version, _)) = &best_version {
266+
if version > *best_version_version {
267+
best_version = Some((version, entry.name));
268+
}
269+
} else {
270+
best_version = Some((version, entry.name));
271+
}
272+
}
273+
} else if req.version_req.matches(&version) {
257274
if let Some((best_version_version, _)) = &best_version {
258275
if version > *best_version_version {
259276
best_version = Some((version, entry.name));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"tempDir": true,
3+
4+
"tests": {
5+
"tag_with_byonm": {
6+
"steps": [
7+
{
8+
"args": "install",
9+
"output": "[WILDCARD]"
10+
},
11+
{
12+
"args": "run -A main.ts",
13+
"output": ""
14+
}
15+
]
16+
},
17+
"no_tag_then_tag": {
18+
"steps": [
19+
{
20+
"args": "run -A replace-version-req.ts 1.0.0",
21+
"output": ""
22+
},
23+
{
24+
"args": "install",
25+
"output": "[WILDCARD]"
26+
},
27+
{
28+
"args": "run -A replace-version-req.ts latest",
29+
"output": ""
30+
},
31+
{
32+
"args": "run -A main.ts",
33+
"output": "node_modules_out_of_date.out",
34+
"exitCode": 1
35+
},
36+
{
37+
"args": "install",
38+
"output": "[WILDCARD]"
39+
},
40+
{ "args": "run -A main.ts", "output": "" }
41+
]
42+
}
43+
}
44+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"imports": {
3+
"@denotest/esm-basic": "npm:@denotest/esm-basic@latest"
4+
}
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import { add } from "@denotest/esm-basic";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
error: Could not find a matching package for 'npm:@denotest/esm-basic@latest' in the node_modules directory. Ensure you have all your JSR and npm dependencies listed in your deno.json or package.json, then run `deno install`. Alternatively, turn on auto-install by specifying `"nodeModulesDir": "auto"` in your deno.json file.
2+
at [WILDCARD]main.ts:1:21
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const newReq = Deno.args[0]?.trim();
2+
if (!newReq) {
3+
throw new Error("Missing required argument");
4+
}
5+
const config = JSON.parse(Deno.readTextFileSync("deno.json"));
6+
config.imports["@denotest/esm-basic"] = `npm:@denotest/esm-basic@${newReq}`;
7+
Deno.writeTextFileSync("deno.json", JSON.stringify(config));

0 commit comments

Comments
 (0)