2024-09-30 09:33:32 -04:00
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std ::borrow ::Cow ;
use std ::path ::Path ;
use std ::path ::PathBuf ;
use std ::sync ::Arc ;
use deno_package_json ::PackageJson ;
use deno_package_json ::PackageJsonDepValue ;
use deno_path_util ::url_to_file_path ;
use deno_semver ::package ::PackageReq ;
use deno_semver ::Version ;
2024-11-01 12:27:00 -04:00
use node_resolver ::env ::NodeResolverEnv ;
2024-09-30 09:33:32 -04:00
use node_resolver ::errors ::PackageFolderResolveError ;
use node_resolver ::errors ::PackageFolderResolveIoError ;
use node_resolver ::errors ::PackageJsonLoadError ;
use node_resolver ::errors ::PackageNotFoundError ;
2024-11-01 12:27:00 -04:00
use node_resolver ::InNpmPackageChecker ;
2024-11-14 15:24:25 -05:00
use node_resolver ::NpmPackageFolderResolver ;
2024-11-01 12:27:00 -04:00
use node_resolver ::PackageJsonResolverRc ;
2024-10-02 16:17:39 -04:00
use thiserror ::Error ;
2024-09-30 09:33:32 -04:00
use url ::Url ;
use crate ::fs ::DenoResolverFs ;
use super ::local ::normalize_pkg_name_for_node_modules_deno_folder ;
2024-11-14 15:24:25 -05:00
use super ::CliNpmReqResolver ;
use super ::ResolvePkgFolderFromDenoReqError ;
2024-09-30 09:33:32 -04:00
2024-10-02 16:17:39 -04:00
#[ derive(Debug, Error) ]
pub enum ByonmResolvePkgFolderFromDenoReqError {
#[ error( " Could not find \" {} \" in a node_modules folder. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`? " , .0) ]
MissingAlias ( String ) ,
#[ error(transparent) ]
PackageJson ( #[ from ] PackageJsonLoadError ) ,
#[ error( " Could not find a matching package for 'npm:{}' 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. " , .0) ]
UnmatchedReq ( PackageReq ) ,
#[ error(transparent) ]
Io ( #[ from ] std ::io ::Error ) ,
}
2024-11-01 12:27:00 -04:00
pub struct ByonmNpmResolverCreateOptions <
Fs : DenoResolverFs ,
TEnv : NodeResolverEnv ,
> {
2024-09-30 09:33:32 -04:00
// todo(dsherret): investigate removing this
pub root_node_modules_dir : Option < PathBuf > ,
2024-11-01 12:27:00 -04:00
pub fs : Fs ,
pub pkg_json_resolver : PackageJsonResolverRc < TEnv > ,
2024-09-30 09:33:32 -04:00
}
#[ derive(Debug) ]
2024-11-01 12:27:00 -04:00
pub struct ByonmNpmResolver < Fs : DenoResolverFs , TEnv : NodeResolverEnv > {
2024-09-30 09:33:32 -04:00
fs : Fs ,
2024-11-01 12:27:00 -04:00
pkg_json_resolver : PackageJsonResolverRc < TEnv > ,
2024-09-30 09:33:32 -04:00
root_node_modules_dir : Option < PathBuf > ,
}
2024-11-01 12:27:00 -04:00
impl < Fs : DenoResolverFs + Clone , TEnv : NodeResolverEnv > Clone
for ByonmNpmResolver < Fs , TEnv >
{
2024-09-30 09:33:32 -04:00
fn clone ( & self ) -> Self {
Self {
fs : self . fs . clone ( ) ,
2024-11-01 12:27:00 -04:00
pkg_json_resolver : self . pkg_json_resolver . clone ( ) ,
2024-09-30 09:33:32 -04:00
root_node_modules_dir : self . root_node_modules_dir . clone ( ) ,
}
}
}
2024-11-01 12:27:00 -04:00
impl < Fs : DenoResolverFs , TEnv : NodeResolverEnv > ByonmNpmResolver < Fs , TEnv > {
pub fn new ( options : ByonmNpmResolverCreateOptions < Fs , TEnv > ) -> Self {
2024-09-30 09:33:32 -04:00
Self {
root_node_modules_dir : options . root_node_modules_dir ,
2024-11-01 12:27:00 -04:00
fs : options . fs ,
pkg_json_resolver : options . pkg_json_resolver ,
2024-09-30 09:33:32 -04:00
}
}
pub fn root_node_modules_dir ( & self ) -> Option < & Path > {
self . root_node_modules_dir . as_deref ( )
}
fn load_pkg_json (
& self ,
path : & Path ,
) -> Result < Option < Arc < PackageJson > > , PackageJsonLoadError > {
2024-11-01 12:27:00 -04:00
self . pkg_json_resolver . load_package_json ( path )
2024-09-30 09:33:32 -04:00
}
/// Finds the ancestor package.json that contains the specified dependency.
pub fn find_ancestor_package_json_with_dep (
& self ,
dep_name : & str ,
referrer : & Url ,
) -> Option < Arc < PackageJson > > {
let referrer_path = url_to_file_path ( referrer ) . ok ( ) ? ;
let mut current_folder = referrer_path . parent ( ) ? ;
loop {
let pkg_json_path = current_folder . join ( " package.json " ) ;
if let Ok ( Some ( pkg_json ) ) = self . load_pkg_json ( & pkg_json_path ) {
if let Some ( deps ) = & pkg_json . dependencies {
if deps . contains_key ( dep_name ) {
return Some ( pkg_json ) ;
}
}
if let Some ( deps ) = & pkg_json . dev_dependencies {
if deps . contains_key ( dep_name ) {
return Some ( pkg_json ) ;
}
}
}
if let Some ( parent ) = current_folder . parent ( ) {
current_folder = parent ;
} else {
return None ;
}
}
}
pub fn resolve_pkg_folder_from_deno_module_req (
& self ,
req : & PackageReq ,
referrer : & Url ,
2024-10-02 16:17:39 -04:00
) -> Result < PathBuf , ByonmResolvePkgFolderFromDenoReqError > {
2024-09-30 09:33:32 -04:00
fn node_resolve_dir < Fs : DenoResolverFs > (
fs : & Fs ,
alias : & str ,
start_dir : & Path ,
2024-10-02 16:17:39 -04:00
) -> std ::io ::Result < Option < PathBuf > > {
2024-09-30 09:33:32 -04:00
for ancestor in start_dir . ancestors ( ) {
let node_modules_folder = ancestor . join ( " node_modules " ) ;
let sub_dir = join_package_name ( & node_modules_folder , alias ) ;
if fs . is_dir_sync ( & sub_dir ) {
return Ok ( Some ( deno_path_util ::canonicalize_path_maybe_not_exists (
& sub_dir ,
& | path | fs . realpath_sync ( path ) ,
) ? ) ) ;
}
}
Ok ( None )
}
// now attempt to resolve if it's found in any package.json
let maybe_pkg_json_and_alias =
self . resolve_pkg_json_and_alias_for_req ( req , referrer ) ? ;
match maybe_pkg_json_and_alias {
Some ( ( pkg_json , alias ) ) = > {
// now try node resolution
if let Some ( resolved ) =
node_resolve_dir ( & self . fs , & alias , pkg_json . dir_path ( ) ) ?
{
return Ok ( resolved ) ;
}
2024-10-02 16:17:39 -04:00
Err ( ByonmResolvePkgFolderFromDenoReqError ::MissingAlias ( alias ) )
2024-09-30 09:33:32 -04:00
}
None = > {
// now check if node_modules/.deno/ matches this constraint
if let Some ( folder ) = self . resolve_folder_in_root_node_modules ( req ) {
return Ok ( folder ) ;
}
2024-10-02 16:17:39 -04:00
Err ( ByonmResolvePkgFolderFromDenoReqError ::UnmatchedReq (
req . clone ( ) ,
) )
2024-09-30 09:33:32 -04:00
}
}
}
fn resolve_pkg_json_and_alias_for_req (
& self ,
req : & PackageReq ,
referrer : & Url ,
2024-10-02 16:17:39 -04:00
) -> Result < Option < ( Arc < PackageJson > , String ) > , PackageJsonLoadError > {
2024-09-30 09:33:32 -04:00
fn resolve_alias_from_pkg_json (
req : & PackageReq ,
pkg_json : & PackageJson ,
) -> Option < String > {
let deps = pkg_json . resolve_local_package_json_deps ( ) ;
2024-11-29 18:54:26 -05:00
for ( key , value ) in deps
. dependencies
. into_iter ( )
. chain ( deps . dev_dependencies . into_iter ( ) )
{
2024-09-30 09:33:32 -04:00
if let Ok ( value ) = value {
match value {
PackageJsonDepValue ::Req ( dep_req ) = > {
if dep_req . name = = req . name
& & dep_req . version_req . intersects ( & req . version_req )
{
return Some ( key ) ;
}
}
PackageJsonDepValue ::Workspace ( _workspace ) = > {
if key = = req . name & & req . version_req . tag ( ) = = Some ( " workspace " ) {
return Some ( key ) ;
}
}
}
}
}
None
}
// attempt to resolve the npm specifier from the referrer's package.json,
if let Ok ( file_path ) = url_to_file_path ( referrer ) {
let mut current_path = file_path . as_path ( ) ;
while let Some ( dir_path ) = current_path . parent ( ) {
let package_json_path = dir_path . join ( " package.json " ) ;
if let Some ( pkg_json ) = self . load_pkg_json ( & package_json_path ) ? {
if let Some ( alias ) =
resolve_alias_from_pkg_json ( req , pkg_json . as_ref ( ) )
{
return Ok ( Some ( ( pkg_json , alias ) ) ) ;
}
}
current_path = dir_path ;
}
}
// otherwise, fall fallback to the project's package.json
if let Some ( root_node_modules_dir ) = & self . root_node_modules_dir {
let root_pkg_json_path =
root_node_modules_dir . parent ( ) . unwrap ( ) . join ( " package.json " ) ;
if let Some ( pkg_json ) = self . load_pkg_json ( & root_pkg_json_path ) ? {
if let Some ( alias ) = resolve_alias_from_pkg_json ( req , pkg_json . as_ref ( ) )
{
return Ok ( Some ( ( pkg_json , alias ) ) ) ;
}
}
}
Ok ( None )
}
fn resolve_folder_in_root_node_modules (
& self ,
req : & PackageReq ,
) -> Option < PathBuf > {
// now check if node_modules/.deno/ matches this constraint
let root_node_modules_dir = self . root_node_modules_dir . as_ref ( ) ? ;
let node_modules_deno_dir = root_node_modules_dir . join ( " .deno " ) ;
let Ok ( entries ) = self . fs . read_dir_sync ( & node_modules_deno_dir ) else {
return None ;
} ;
let search_prefix = format! (
" {}@ " ,
normalize_pkg_name_for_node_modules_deno_folder ( & req . name )
) ;
let mut best_version = None ;
// example entries:
// - @denotest+add@1.0.0
// - @denotest+add@1.0.0_1
for entry in entries {
if ! entry . is_directory {
continue ;
}
let Some ( version_and_copy_idx ) = entry . name . strip_prefix ( & search_prefix )
else {
continue ;
} ;
let version = version_and_copy_idx
. rsplit_once ( '_' )
. map ( | ( v , _ ) | v )
. unwrap_or ( version_and_copy_idx ) ;
let Ok ( version ) = Version ::parse_from_npm ( version ) else {
continue ;
} ;
fix(install): store tags associated with package in node_modules dir (#26000)
Fixes #25998. Fixes https://github.com/denoland/deno/issues/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.
2024-10-02 20:16:46 -04:00
if let Some ( tag ) = req . version_req . tag ( ) {
let initialized_file =
node_modules_deno_dir . join ( & entry . name ) . join ( " .initialized " ) ;
let Ok ( contents ) = self . fs . read_to_string_lossy ( & initialized_file )
else {
continue ;
} ;
let mut tags = contents . split ( ',' ) . map ( str ::trim ) ;
if tags . any ( | t | t = = tag ) {
if let Some ( ( best_version_version , _ ) ) = & best_version {
if version > * best_version_version {
best_version = Some ( ( version , entry . name ) ) ;
}
} else {
best_version = Some ( ( version , entry . name ) ) ;
}
}
} else if req . version_req . matches ( & version ) {
2024-09-30 09:33:32 -04:00
if let Some ( ( best_version_version , _ ) ) = & best_version {
if version > * best_version_version {
best_version = Some ( ( version , entry . name ) ) ;
}
} else {
best_version = Some ( ( version , entry . name ) ) ;
}
}
}
best_version . map ( | ( _version , entry_name ) | {
join_package_name (
& node_modules_deno_dir . join ( entry_name ) . join ( " node_modules " ) ,
& req . name ,
)
} )
}
}
2024-11-01 12:27:00 -04:00
impl <
Fs : DenoResolverFs + Send + Sync + std ::fmt ::Debug ,
TEnv : NodeResolverEnv ,
2024-11-14 15:24:25 -05:00
> CliNpmReqResolver for ByonmNpmResolver < Fs , TEnv >
{
fn resolve_pkg_folder_from_deno_module_req (
& self ,
req : & PackageReq ,
referrer : & Url ,
) -> Result < PathBuf , ResolvePkgFolderFromDenoReqError > {
ByonmNpmResolver ::resolve_pkg_folder_from_deno_module_req (
self , req , referrer ,
)
. map_err ( ResolvePkgFolderFromDenoReqError ::Byonm )
}
}
impl <
Fs : DenoResolverFs + Send + Sync + std ::fmt ::Debug ,
TEnv : NodeResolverEnv ,
> NpmPackageFolderResolver for ByonmNpmResolver < Fs , TEnv >
2024-09-30 09:33:32 -04:00
{
fn resolve_package_folder_from_package (
& self ,
name : & str ,
referrer : & Url ,
) -> Result < PathBuf , PackageFolderResolveError > {
fn inner < Fs : DenoResolverFs > (
fs : & Fs ,
name : & str ,
referrer : & Url ,
) -> Result < PathBuf , PackageFolderResolveError > {
let maybe_referrer_file = url_to_file_path ( referrer ) . ok ( ) ;
let maybe_start_folder =
maybe_referrer_file . as_ref ( ) . and_then ( | f | f . parent ( ) ) ;
if let Some ( start_folder ) = maybe_start_folder {
for current_folder in start_folder . ancestors ( ) {
let node_modules_folder = if current_folder . ends_with ( " node_modules " )
{
Cow ::Borrowed ( current_folder )
} else {
Cow ::Owned ( current_folder . join ( " node_modules " ) )
} ;
let sub_dir = join_package_name ( & node_modules_folder , name ) ;
if fs . is_dir_sync ( & sub_dir ) {
return Ok ( sub_dir ) ;
}
}
}
Err (
PackageNotFoundError {
package_name : name . to_string ( ) ,
referrer : referrer . clone ( ) ,
referrer_extra : None ,
}
. into ( ) ,
)
}
let path = inner ( & self . fs , name , referrer ) ? ;
self . fs . realpath_sync ( & path ) . map_err ( | err | {
PackageFolderResolveIoError {
package_name : name . to_string ( ) ,
referrer : referrer . clone ( ) ,
source : err ,
}
. into ( )
} )
}
2024-11-01 12:27:00 -04:00
}
#[ derive(Debug) ]
pub struct ByonmInNpmPackageChecker ;
2024-09-30 09:33:32 -04:00
2024-11-01 12:27:00 -04:00
impl InNpmPackageChecker for ByonmInNpmPackageChecker {
2024-09-30 09:33:32 -04:00
fn in_npm_package ( & self , specifier : & Url ) -> bool {
specifier . scheme ( ) = = " file "
& & specifier
. path ( )
. to_ascii_lowercase ( )
. contains ( " /node_modules/ " )
}
}
fn join_package_name ( path : & Path , package_name : & str ) -> PathBuf {
let mut path = path . to_path_buf ( ) ;
// ensure backslashes are used on windows
for part in package_name . split ( '/' ) {
path = path . join ( part ) ;
}
path
}