Skip to content

Commit ff90c33

Browse files
committed
feat(npm): add flag for creating and resolving npm packages to a local node_modules folder (#15971)
1 parent 3427922 commit ff90c33

File tree

23 files changed

+744
-126
lines changed

23 files changed

+744
-126
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ monch = "=0.2.0"
8585
notify = "=5.0.0"
8686
once_cell = "=1.14.0"
8787
os_pipe = "=1.0.1"
88-
path-clean = "=0.1.0"
8988
percent-encoding = "=2.2.0"
9089
pin-project = "1.0.11" # don't pin because they yank crates from cargo
9190
rand = { version = "=0.8.5", features = ["small_rng"] }

cli/args/flags.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ pub struct Flags {
301301
pub cached_only: bool,
302302
pub type_check_mode: TypeCheckMode,
303303
pub config_flag: ConfigFlag,
304+
pub node_modules_dir: bool,
304305
pub coverage_dir: Option<String>,
305306
pub enable_testing_features: bool,
306307
pub ignore: Vec<PathBuf>,
@@ -1732,6 +1733,7 @@ fn compile_args(app: Command) -> Command {
17321733
app
17331734
.arg(import_map_arg())
17341735
.arg(no_remote_arg())
1736+
.arg(local_npm_arg())
17351737
.arg(no_config_arg())
17361738
.arg(config_arg())
17371739
.arg(no_check_arg())
@@ -1746,6 +1748,7 @@ fn compile_args_without_check_args(app: Command) -> Command {
17461748
app
17471749
.arg(import_map_arg())
17481750
.arg(no_remote_arg())
1751+
.arg(local_npm_arg())
17491752
.arg(config_arg())
17501753
.arg(no_config_arg())
17511754
.arg(reload_arg())
@@ -2144,6 +2147,12 @@ fn no_remote_arg<'a>() -> Arg<'a> {
21442147
.help("Do not resolve remote modules")
21452148
}
21462149

2150+
fn local_npm_arg<'a>() -> Arg<'a> {
2151+
Arg::new("node-modules-dir")
2152+
.long("node-modules-dir")
2153+
.help("Creates a local node_modules folder")
2154+
}
2155+
21472156
fn unsafely_ignore_certificate_errors_arg<'a>() -> Arg<'a> {
21482157
Arg::new("unsafely-ignore-certificate-errors")
21492158
.long("unsafely-ignore-certificate-errors")
@@ -2789,6 +2798,7 @@ fn vendor_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
27892798
fn compile_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
27902799
import_map_arg_parse(flags, matches);
27912800
no_remote_arg_parse(flags, matches);
2801+
local_npm_args_parse(flags, matches);
27922802
config_args_parse(flags, matches);
27932803
no_check_arg_parse(flags, matches);
27942804
check_arg_parse(flags, matches);
@@ -2803,6 +2813,7 @@ fn compile_args_without_no_check_parse(
28032813
) {
28042814
import_map_arg_parse(flags, matches);
28052815
no_remote_arg_parse(flags, matches);
2816+
local_npm_args_parse(flags, matches);
28062817
config_args_parse(flags, matches);
28072818
reload_arg_parse(flags, matches);
28082819
lock_args_parse(flags, matches);
@@ -3044,6 +3055,12 @@ fn no_remote_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
30443055
}
30453056
}
30463057

3058+
fn local_npm_args_parse(flags: &mut Flags, matches: &ArgMatches) {
3059+
if matches.is_present("node-modules-dir") {
3060+
flags.node_modules_dir = true;
3061+
}
3062+
}
3063+
30473064
fn inspect_arg_validate(val: &str) -> Result<(), String> {
30483065
match val.parse::<SocketAddr>() {
30493066
Ok(_) => Ok(()),
@@ -5002,6 +5019,22 @@ mod tests {
50025019
);
50035020
}
50045021

5022+
#[test]
5023+
fn local_npm() {
5024+
let r =
5025+
flags_from_vec(svec!["deno", "run", "--node-modules-dir", "script.ts"]);
5026+
assert_eq!(
5027+
r.unwrap(),
5028+
Flags {
5029+
subcommand: DenoSubcommand::Run(RunFlags {
5030+
script: "script.ts".to_string(),
5031+
}),
5032+
node_modules_dir: true,
5033+
..Flags::default()
5034+
}
5035+
);
5036+
}
5037+
50055038
#[test]
50065039
fn cached_only() {
50075040
let r = flags_from_vec(svec!["deno", "run", "--cached-only", "script.ts"]);

cli/args/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use crate::emit::TsConfigWithIgnoredOptions;
4242
use crate::emit::TsTypeLib;
4343
use crate::file_fetcher::get_root_cert_store;
4444
use crate::file_fetcher::CacheSetting;
45+
use crate::fs_util;
4546
use crate::lockfile::Lockfile;
4647
use crate::version;
4748

@@ -146,6 +147,24 @@ impl CliOptions {
146147
self.overrides.import_map_specifier = Some(path);
147148
}
148149

150+
/// Resolves the path to use for a local node_modules folder.
151+
pub fn resolve_local_node_modules_folder(
152+
&self,
153+
) -> Result<Option<PathBuf>, AnyError> {
154+
let path = if !self.flags.node_modules_dir {
155+
return Ok(None);
156+
} else if let Some(config_path) = self
157+
.maybe_config_file
158+
.as_ref()
159+
.and_then(|c| c.specifier.to_file_path().ok())
160+
{
161+
config_path.parent().unwrap().join("node_modules")
162+
} else {
163+
std::env::current_dir()?.join("node_modules")
164+
};
165+
Ok(Some(fs_util::canonicalize_path_maybe_not_exists(&path)?))
166+
}
167+
149168
pub fn resolve_root_cert_store(&self) -> Result<RootCertStore, AnyError> {
150169
get_root_cert_store(
151170
None,

cli/fs_util.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use deno_core::error::{uri_error, AnyError};
55
pub use deno_core::normalize_path;
66
use deno_core::ModuleSpecifier;
77
use deno_runtime::deno_crypto::rand;
8+
use deno_runtime::deno_node::PathClean;
89
use std::borrow::Cow;
910
use std::env::current_dir;
1011
use std::fs::OpenOptions;
11-
use std::io::{Error, Write};
12+
use std::io::{Error, ErrorKind, Write};
1213
use std::path::{Path, PathBuf};
1314
use walkdir::WalkDir;
1415

@@ -75,6 +76,35 @@ pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> {
7576
return Ok(path);
7677
}
7778

79+
/// Canonicalizes a path which might be non-existent by going up the
80+
/// ancestors until it finds a directory that exists, canonicalizes
81+
/// that path, then adds back the remaining path components.
82+
///
83+
/// Note: When using this, you should be aware that a symlink may
84+
/// subsequently be created along this path by some other code.
85+
pub fn canonicalize_path_maybe_not_exists(
86+
path: &Path,
87+
) -> Result<PathBuf, Error> {
88+
let path = path.to_path_buf().clean();
89+
let mut path = path.as_path();
90+
let mut names_stack = Vec::new();
91+
loop {
92+
match canonicalize_path(path) {
93+
Ok(mut canonicalized_path) => {
94+
for name in names_stack.into_iter().rev() {
95+
canonicalized_path = canonicalized_path.join(name);
96+
}
97+
return Ok(canonicalized_path);
98+
}
99+
Err(err) if err.kind() == ErrorKind::NotFound => {
100+
names_stack.push(path.file_name().unwrap());
101+
path = path.parent().unwrap();
102+
}
103+
Err(err) => return Err(err),
104+
}
105+
}
106+
}
107+
78108
#[cfg(windows)]
79109
fn strip_unc_prefix(path: PathBuf) -> PathBuf {
80110
use std::path::Component;
@@ -294,6 +324,60 @@ pub async fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> {
294324
}
295325
}
296326

327+
/// Copies a directory to another directory.
328+
///
329+
/// Note: Does not handle symlinks.
330+
pub fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> {
331+
std::fs::create_dir_all(&to)
332+
.with_context(|| format!("Creating {}", to.display()))?;
333+
let read_dir = std::fs::read_dir(&from)
334+
.with_context(|| format!("Reading {}", from.display()))?;
335+
336+
for entry in read_dir {
337+
let entry = entry?;
338+
let file_type = entry.file_type()?;
339+
let new_from = from.join(entry.file_name());
340+
let new_to = to.join(entry.file_name());
341+
342+
if file_type.is_dir() {
343+
copy_dir_recursive(&new_from, &new_to).with_context(|| {
344+
format!("Dir {} to {}", new_from.display(), new_to.display())
345+
})?;
346+
} else if file_type.is_file() {
347+
std::fs::copy(&new_from, &new_to).with_context(|| {
348+
format!("Copying {} to {}", new_from.display(), new_to.display())
349+
})?;
350+
}
351+
}
352+
353+
Ok(())
354+
}
355+
356+
pub fn symlink_dir(oldpath: &Path, newpath: &Path) -> Result<(), AnyError> {
357+
let err_mapper = |err: Error| {
358+
Error::new(
359+
err.kind(),
360+
format!(
361+
"{}, symlink '{}' -> '{}'",
362+
err,
363+
oldpath.display(),
364+
newpath.display()
365+
),
366+
)
367+
};
368+
#[cfg(unix)]
369+
{
370+
use std::os::unix::fs::symlink;
371+
symlink(&oldpath, &newpath).map_err(err_mapper)?;
372+
}
373+
#[cfg(not(unix))]
374+
{
375+
use std::os::windows::fs::symlink_dir;
376+
symlink_dir(&oldpath, &newpath).map_err(err_mapper)?;
377+
}
378+
Ok(())
379+
}
380+
297381
/// Attempts to convert a specifier to a file path. By default, uses the Url
298382
/// crate's `to_file_path()` method, but falls back to try and resolve unix-style
299383
/// paths on Windows.

cli/node/mod.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ use deno_runtime::deno_node::package_imports_resolve;
2626
use deno_runtime::deno_node::package_resolve;
2727
use deno_runtime::deno_node::NodeModuleKind;
2828
use deno_runtime::deno_node::PackageJson;
29+
use deno_runtime::deno_node::PathClean;
2930
use deno_runtime::deno_node::RequireNpmResolver;
3031
use deno_runtime::deno_node::DEFAULT_CONDITIONS;
3132
use deno_runtime::deno_node::NODE_GLOBAL_THIS_NAME;
3233
use once_cell::sync::Lazy;
33-
use path_clean::PathClean;
3434
use regex::Regex;
3535

3636
use crate::file_fetcher::FileFetcher;
@@ -433,9 +433,8 @@ pub fn node_resolve_npm_reference(
433433
reference: &NpmPackageReference,
434434
npm_resolver: &NpmPackageResolver,
435435
) -> Result<Option<NodeResolution>, AnyError> {
436-
let package_folder = npm_resolver
437-
.resolve_package_from_deno_module(&reference.req)?
438-
.folder_path;
436+
let package_folder =
437+
npm_resolver.resolve_package_folder_from_deno_module(&reference.req)?;
439438
let resolved_path = package_config_resolve(
440439
&reference
441440
.sub_path
@@ -462,15 +461,28 @@ pub fn node_resolve_binary_export(
462461
bin_name: Option<&str>,
463462
npm_resolver: &NpmPackageResolver,
464463
) -> Result<NodeResolution, AnyError> {
465-
let pkg = npm_resolver.resolve_package_from_deno_module(pkg_req)?;
466-
let package_folder = pkg.folder_path;
464+
fn get_package_display_name(package_json: &PackageJson) -> String {
465+
package_json
466+
.name
467+
.as_ref()
468+
.and_then(|name| {
469+
package_json
470+
.version
471+
.as_ref()
472+
.map(|version| format!("{}@{}", name, version))
473+
})
474+
.unwrap_or_else(|| format!("{}", package_json.path.display()))
475+
}
476+
477+
let package_folder =
478+
npm_resolver.resolve_package_folder_from_deno_module(pkg_req)?;
467479
let package_json_path = package_folder.join("package.json");
468480
let package_json = PackageJson::load(npm_resolver, package_json_path)?;
469481
let bin = match &package_json.bin {
470482
Some(bin) => bin,
471483
None => bail!(
472484
"package {} did not have a 'bin' property in its package.json",
473-
pkg.id
485+
get_package_display_name(&package_json),
474486
),
475487
};
476488
let bin_entry = match bin {
@@ -490,21 +502,21 @@ pub fn node_resolve_binary_export(
490502
o.get(&pkg_req.name)
491503
}
492504
},
493-
_ => bail!("package {} did not have a 'bin' property with a string or object value in its package.json", pkg.id),
505+
_ => bail!("package {} did not have a 'bin' property with a string or object value in its package.json", get_package_display_name(&package_json)),
494506
};
495507
let bin_entry = match bin_entry {
496508
Some(e) => e,
497509
None => bail!(
498510
"package {} did not have a 'bin' entry for {} in its package.json",
499-
pkg.id,
511+
get_package_display_name(&package_json),
500512
bin_name.unwrap_or(&pkg_req.name),
501513
),
502514
};
503515
let bin_entry = match bin_entry {
504516
Value::String(s) => s,
505517
_ => bail!(
506518
"package {} had a non-string sub property of 'bin' in its package.json",
507-
pkg.id
519+
get_package_display_name(&package_json),
508520
),
509521
};
510522

cli/npm/resolution.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use std::cmp::Ordering;
44
use std::collections::HashMap;
5+
use std::collections::HashSet;
56
use std::collections::VecDeque;
67

78
use deno_ast::ModuleSpecifier;
@@ -185,6 +186,26 @@ impl NpmResolutionSnapshot {
185186
}
186187
}
187188

189+
pub fn top_level_packages(&self) -> Vec<NpmPackageId> {
190+
self
191+
.package_reqs
192+
.iter()
193+
.map(|(req, version)| NpmPackageId {
194+
name: req.name.clone(),
195+
version: version.clone(),
196+
})
197+
.collect::<HashSet<_>>()
198+
.into_iter()
199+
.collect::<Vec<_>>()
200+
}
201+
202+
pub fn package_from_id(
203+
&self,
204+
id: &NpmPackageId,
205+
) -> Option<&NpmResolutionPackage> {
206+
self.packages.get(id)
207+
}
208+
188209
pub fn resolve_package_from_package(
189210
&self,
190211
name: &str,
@@ -471,8 +492,6 @@ impl NpmResolution {
471492
!self.snapshot.read().packages.is_empty()
472493
}
473494

474-
// todo(dsherret): for use in the lsp
475-
#[allow(dead_code)]
476495
pub fn snapshot(&self) -> NpmResolutionSnapshot {
477496
self.snapshot.read().clone()
478497
}

0 commit comments

Comments
 (0)