Skip to content

feat(npm): add flag for creating and resolving npm packages to a local node_modules folder #15971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ monch = "=0.2.0"
notify = "=5.0.0"
once_cell = "=1.14.0"
os_pipe = "=1.0.1"
path-clean = "=0.1.0"
percent-encoding = "=2.2.0"
pin-project = "1.0.11" # don't pin because they yank crates from cargo
rand = { version = "=0.8.5", features = ["small_rng"] }
Expand Down
32 changes: 32 additions & 0 deletions cli/args/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ pub struct Flags {
pub cached_only: bool,
pub type_check_mode: TypeCheckMode,
pub config_flag: ConfigFlag,
pub local_npm: bool,
pub coverage_dir: Option<String>,
pub enable_testing_features: bool,
pub ignore: Vec<PathBuf>,
Expand Down Expand Up @@ -1734,6 +1735,7 @@ fn compile_args(app: Command) -> Command {
.arg(import_map_arg())
.arg(no_remote_arg())
.arg(no_npm_arg())
.arg(local_npm_arg())
.arg(no_config_arg())
.arg(config_arg())
.arg(no_check_arg())
Expand All @@ -1749,6 +1751,7 @@ fn compile_args_without_check_args(app: Command) -> Command {
.arg(import_map_arg())
.arg(no_remote_arg())
.arg(no_npm_arg())
.arg(local_npm_arg())
.arg(config_arg())
.arg(no_config_arg())
.arg(reload_arg())
Expand Down Expand Up @@ -2149,6 +2152,12 @@ fn no_npm_arg<'a>() -> Arg<'a> {
.help("Do not resolve npm modules")
}

fn local_npm_arg<'a>() -> Arg<'a> {
Arg::new("local-npm")
.long("local-npm")
.help("Creates a local node_modules folder")
}

fn unsafely_ignore_certificate_errors_arg<'a>() -> Arg<'a> {
Arg::new("unsafely-ignore-certificate-errors")
.long("unsafely-ignore-certificate-errors")
Expand Down Expand Up @@ -2795,6 +2804,7 @@ fn compile_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
import_map_arg_parse(flags, matches);
no_remote_arg_parse(flags, matches);
no_npm_arg_parse(flags, matches);
local_npm_args_parse(flags, matches);
config_args_parse(flags, matches);
no_check_arg_parse(flags, matches);
check_arg_parse(flags, matches);
Expand All @@ -2810,6 +2820,7 @@ fn compile_args_without_no_check_parse(
import_map_arg_parse(flags, matches);
no_remote_arg_parse(flags, matches);
no_npm_arg_parse(flags, matches);
local_npm_args_parse(flags, matches);
config_args_parse(flags, matches);
reload_arg_parse(flags, matches);
lock_args_parse(flags, matches);
Expand Down Expand Up @@ -3057,6 +3068,12 @@ fn no_npm_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
}
}

fn local_npm_args_parse(flags: &mut Flags, matches: &ArgMatches) {
if matches.is_present("local-npm") {
flags.local_npm = true;
}
}

fn inspect_arg_validate(val: &str) -> Result<(), String> {
match val.parse::<SocketAddr>() {
Ok(_) => Ok(()),
Expand Down Expand Up @@ -5030,6 +5047,21 @@ mod tests {
);
}

#[test]
fn local_npm() {
let r = flags_from_vec(svec!["deno", "run", "--local-npm", "script.ts"]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Run(RunFlags {
script: "script.ts".to_string(),
}),
local_npm: true,
..Flags::default()
}
);
}

#[test]
fn cached_only() {
let r = flags_from_vec(svec!["deno", "run", "--cached-only", "script.ts"]);
Expand Down
18 changes: 18 additions & 0 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ impl CliOptions {
self.overrides.import_map_specifier = Some(path);
}

/// Resolves the folder to use for a local node_modules folder.
pub fn resolve_local_node_modules_folder(
&self,
) -> Result<Option<PathBuf>, AnyError> {
let path = if !self.flags.local_npm {
return Ok(None);
} else if let Some(config_path) = self
.maybe_config_file
.as_ref()
.and_then(|c| c.specifier.to_file_path().ok())
{
config_path.parent().unwrap().join("node_modules")
} else {
std::env::current_dir()?.join("node_modules")
};
Ok(Some(path))
}

pub fn resolve_root_cert_store(&self) -> Result<RootCertStore, AnyError> {
get_root_cert_store(
None,
Expand Down
54 changes: 54 additions & 0 deletions cli/fs_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,60 @@ pub async fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> {
}
}

/// Copies a directory to another directory.
///
/// Note: Does not handle symlinks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it handle symlinks at some point?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. For now it doesn't because it's not necessary for npm packages from my understanding.

pub fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> {
std::fs::create_dir_all(&to)
.with_context(|| format!("Creating {}", to.display()))?;
let read_dir = std::fs::read_dir(&from)
.with_context(|| format!("Reading {}", from.display()))?;

for entry in read_dir {
let entry = entry?;
let file_type = entry.file_type()?;
let new_from = from.join(entry.file_name());
let new_to = to.join(entry.file_name());

if file_type.is_dir() {
copy_dir_recursive(&new_from, &new_to).with_context(|| {
format!("Dir {} to {}", new_from.display(), new_to.display())
})?;
} else if file_type.is_file() {
std::fs::copy(&new_from, &new_to).with_context(|| {
format!("Copying {} to {}", new_from.display(), new_to.display())
})?;
}
}

Ok(())
}

pub fn symlink_dir(oldpath: &Path, newpath: &Path) -> Result<(), AnyError> {
let err_mapper = |err: Error| {
Error::new(
err.kind(),
format!(
"{}, symlink '{}' -> '{}'",
err,
oldpath.display(),
newpath.display()
),
)
};
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
symlink(&oldpath, &newpath).map_err(err_mapper)?;
}
#[cfg(not(unix))]
{
use std::os::windows::fs::symlink_dir;
symlink_dir(&oldpath, &newpath).map_err(err_mapper)?;
}
Ok(())
}

/// Attempts to convert a specifier to a file path. By default, uses the Url
/// crate's `to_file_path()` method, but falls back to try and resolve unix-style
/// paths on Windows.
Expand Down
32 changes: 22 additions & 10 deletions cli/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ use deno_runtime::deno_node::package_imports_resolve;
use deno_runtime::deno_node::package_resolve;
use deno_runtime::deno_node::NodeModuleKind;
use deno_runtime::deno_node::PackageJson;
use deno_runtime::deno_node::PathClean;
use deno_runtime::deno_node::RequireNpmResolver;
use deno_runtime::deno_node::DEFAULT_CONDITIONS;
use deno_runtime::deno_node::NODE_GLOBAL_THIS_NAME;
use once_cell::sync::Lazy;
use path_clean::PathClean;
use regex::Regex;

use crate::file_fetcher::FileFetcher;
Expand Down Expand Up @@ -433,9 +433,8 @@ pub fn node_resolve_npm_reference(
reference: &NpmPackageReference,
npm_resolver: &NpmPackageResolver,
) -> Result<Option<NodeResolution>, AnyError> {
let package_folder = npm_resolver
.resolve_package_from_deno_module(&reference.req)?
.folder_path;
let package_folder =
npm_resolver.resolve_package_folder_from_deno_module(&reference.req)?;
let resolved_path = package_config_resolve(
&reference
.sub_path
Expand All @@ -462,15 +461,28 @@ pub fn node_resolve_binary_export(
bin_name: Option<&str>,
npm_resolver: &NpmPackageResolver,
) -> Result<NodeResolution, AnyError> {
let pkg = npm_resolver.resolve_package_from_deno_module(pkg_req)?;
let package_folder = pkg.folder_path;
fn get_package_display_name(package_json: &PackageJson) -> String {
package_json
.name
.as_ref()
.and_then(|name| {
package_json
.version
.as_ref()
.map(|version| format!("{}@{}", name, version))
})
.unwrap_or_else(|| format!("{}", package_json.path.display()))
}

let package_folder =
npm_resolver.resolve_package_folder_from_deno_module(pkg_req)?;
let package_json_path = package_folder.join("package.json");
let package_json = PackageJson::load(npm_resolver, package_json_path)?;
let bin = match &package_json.bin {
Some(bin) => bin,
None => bail!(
"package {} did not have a 'bin' property in its package.json",
pkg.id
get_package_display_name(&package_json),
),
};
let bin_entry = match bin {
Expand All @@ -490,21 +502,21 @@ pub fn node_resolve_binary_export(
o.get(&pkg_req.name)
}
},
_ => bail!("package {} did not have a 'bin' property with a string or object value in its package.json", pkg.id),
_ => bail!("package {} did not have a 'bin' property with a string or object value in its package.json", get_package_display_name(&package_json)),
};
let bin_entry = match bin_entry {
Some(e) => e,
None => bail!(
"package {} did not have a 'bin' entry for {} in its package.json",
pkg.id,
get_package_display_name(&package_json),
bin_name.unwrap_or(&pkg_req.name),
),
};
let bin_entry = match bin_entry {
Value::String(s) => s,
_ => bail!(
"package {} had a non-string sub property of 'bin' in its package.json",
pkg.id
get_package_display_name(&package_json),
),
};

Expand Down
23 changes: 21 additions & 2 deletions cli/npm/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;

use deno_ast::ModuleSpecifier;
Expand Down Expand Up @@ -185,6 +186,26 @@ impl NpmResolutionSnapshot {
}
}

pub fn top_level_packages(&self) -> Vec<NpmPackageId> {
self
.package_reqs
.iter()
.map(|(req, version)| NpmPackageId {
name: req.name.clone(),
version: version.clone(),
})
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>()
}

pub fn package_from_id(
&self,
id: &NpmPackageId,
) -> Option<&NpmResolutionPackage> {
self.packages.get(id)
}

pub fn resolve_package_from_package(
&self,
name: &str,
Expand Down Expand Up @@ -471,8 +492,6 @@ impl NpmResolution {
!self.snapshot.read().packages.is_empty()
}

// todo(dsherret): for use in the lsp
#[allow(dead_code)]
pub fn snapshot(&self) -> NpmResolutionSnapshot {
self.snapshot.read().clone()
}
Expand Down
52 changes: 37 additions & 15 deletions cli/npm/resolvers/common.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;

Expand All @@ -9,34 +10,25 @@ use deno_core::futures::future::BoxFuture;
use deno_core::url::Url;

use crate::npm::NpmCache;
use crate::npm::NpmPackageId;
use crate::npm::NpmPackageReq;
use crate::npm::NpmResolutionPackage;

/// Information about the local npm package.
pub struct LocalNpmPackageInfo {
/// Unique identifier.
pub id: NpmPackageId,
/// Local folder path of the npm package.
pub folder_path: PathBuf,
}

pub trait InnerNpmPackageResolver: Send + Sync {
fn resolve_package_from_deno_module(
fn resolve_package_folder_from_deno_module(
&self,
pkg_req: &NpmPackageReq,
) -> Result<LocalNpmPackageInfo, AnyError>;
) -> Result<PathBuf, AnyError>;

fn resolve_package_from_package(
fn resolve_package_folder_from_package(
&self,
name: &str,
referrer: &ModuleSpecifier,
) -> Result<LocalNpmPackageInfo, AnyError>;
) -> Result<PathBuf, AnyError>;

fn resolve_package_from_specifier(
fn resolve_package_folder_from_specifier(
&self,
specifier: &ModuleSpecifier,
) -> Result<LocalNpmPackageInfo, AnyError>;
) -> Result<PathBuf, AnyError>;

fn has_packages(&self) -> bool;

Expand Down Expand Up @@ -87,3 +79,33 @@ pub async fn cache_packages(
}
Ok(())
}

pub fn ensure_registry_read_permission(
registry_path: &Path,
path: &Path,
) -> Result<(), AnyError> {
// allow reading if it's in the node_modules
if path.starts_with(&registry_path)
&& path
.components()
.all(|c| !matches!(c, std::path::Component::ParentDir))
{
// todo(dsherret): cache this?
if let Ok(registry_path) = std::fs::canonicalize(registry_path) {
match std::fs::canonicalize(path) {
Ok(path) if path.starts_with(registry_path) => {
return Ok(());
}
Err(e) if e.kind() == ErrorKind::NotFound => {
return Ok(());
}
_ => {} // ignore
}
}
}

Err(deno_core::error::custom_error(
"PermissionDenied",
format!("Reading {} is not allowed", path.display()),
))
}
Loading