summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2023-10-25 14:39:00 -0400
committerGitHub <noreply@github.com>2023-10-25 14:39:00 -0400
commitbe97170a193e8cecc5ce03ecd3c1d0add4a06bf7 (patch)
treefab7d266e208db93dcf0870dda70f7da56ade735 /cli
parent093b3eee58181ec45839d0fe10b8157326a102b2 (diff)
feat(unstable): ability to `npm install` then `deno run main.ts` (#20967)
This PR adds a new unstable "bring your own node_modules" (BYONM) functionality currently behind a `--unstable-byonm` flag (`"unstable": ["byonm"]` in a deno.json). This enables users to run a separate install command (ex. `npm install`, `pnpm install`) then run `deno run main.ts` and Deno will respect the layout of the node_modules directory as setup by the separate install command. It also works with npm/yarn/pnpm workspaces. For this PR, the behaviour is opted into by specifying `--unstable-byonm`/`"unstable": ["byonm"]`, but in the future we may make this the default behaviour as outlined in https://github.com/denoland/deno/issues/18967#issuecomment-1761248941 This is an extremely rough initial implementation. Errors are terrible in this and the LSP requires frequent restarts. Improvements will be done in follow up PRs.
Diffstat (limited to 'cli')
-rw-r--r--cli/args/flags.rs16
-rw-r--r--cli/args/mod.rs68
-rw-r--r--cli/cache/mod.rs29
-rw-r--r--cli/factory.rs41
-rw-r--r--cli/graph_util.rs2
-rw-r--r--cli/lsp/analysis.rs2
-rw-r--r--cli/lsp/documents.rs71
-rw-r--r--cli/lsp/language_server.rs48
-rw-r--r--cli/module_loader.rs4
-rw-r--r--cli/npm/byonm.rs274
-rw-r--r--cli/npm/managed/mod.rs45
-rw-r--r--cli/npm/managed/resolvers/local.rs23
-rw-r--r--cli/npm/managed/tarball.rs14
-rw-r--r--cli/npm/mod.rs21
-rw-r--r--cli/resolver.rs121
-rw-r--r--cli/tests/integration/cert_tests.rs2
-rw-r--r--cli/tests/integration/compile_tests.rs18
-rw-r--r--cli/tests/integration/npm_tests.rs237
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/cjs/index.cjs3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/bar.js3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/foo.js3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/index.js3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/m.js3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/index.js3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/foo.js3
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/package.json16
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/bar.js2
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/foo.js2
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/index.js2
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/m.js2
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.cts1
-rw-r--r--cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.mts1
-rw-r--r--cli/tools/vendor/test.rs22
33 files changed, 899 insertions, 206 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index 138d77359..588f75dbb 100644
--- a/cli/args/flags.rs
+++ b/cli/args/flags.rs
@@ -407,6 +407,7 @@ pub struct Flags {
pub seed: Option<u64>,
pub unstable: bool,
pub unstable_bare_node_builtlins: bool,
+ pub unstable_byonm: bool,
pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
pub v8_flags: Vec<String>,
}
@@ -803,9 +804,9 @@ pub fn flags_from_vec(args: Vec<String>) -> clap::error::Result<Flags> {
flags.unstable = true;
}
- if matches.get_flag("unstable-bare-node-builtins") {
- flags.unstable_bare_node_builtlins = true;
- }
+ flags.unstable_bare_node_builtlins =
+ matches.get_flag("unstable-bare-node-builtins");
+ flags.unstable_byonm = matches.get_flag("unstable-byonm");
if matches.get_flag("quiet") {
flags.log_level = Some(Level::Error);
@@ -912,6 +913,15 @@ fn clap_root() -> Command {
.global(true),
)
.arg(
+ Arg::new("unstable-byonm")
+ .long("unstable-byonm")
+ .help("Enable unstable 'bring your own node_modules' feature")
+ .env("DENO_UNSTABLE_BYONM")
+ .value_parser(FalseyValueParser::new())
+ .action(ArgAction::SetTrue)
+ .global(true),
+ )
+ .arg(
Arg::new("log-level")
.short('L')
.long("log-level")
diff --git a/cli/args/mod.rs b/cli/args/mod.rs
index 390e16698..ab8d6b503 100644
--- a/cli/args/mod.rs
+++ b/cli/args/mod.rs
@@ -76,25 +76,29 @@ use deno_config::FmtConfig;
use deno_config::LintConfig;
use deno_config::TestConfig;
-static NPM_REGISTRY_DEFAULT_URL: Lazy<Url> = Lazy::new(|| {
- let env_var_name = "NPM_CONFIG_REGISTRY";
- if let Ok(registry_url) = std::env::var(env_var_name) {
- // ensure there is a trailing slash for the directory
- let registry_url = format!("{}/", registry_url.trim_end_matches('/'));
- match Url::parse(&registry_url) {
- Ok(url) => {
- return url;
- }
- Err(err) => {
- log::debug!("Invalid {} environment variable: {:#}", env_var_name, err,);
+pub fn npm_registry_default_url() -> &'static Url {
+ static NPM_REGISTRY_DEFAULT_URL: Lazy<Url> = Lazy::new(|| {
+ let env_var_name = "NPM_CONFIG_REGISTRY";
+ if let Ok(registry_url) = std::env::var(env_var_name) {
+ // ensure there is a trailing slash for the directory
+ let registry_url = format!("{}/", registry_url.trim_end_matches('/'));
+ match Url::parse(&registry_url) {
+ Ok(url) => {
+ return url;
+ }
+ Err(err) => {
+ log::debug!(
+ "Invalid {} environment variable: {:#}",
+ env_var_name,
+ err,
+ );
+ }
}
}
- }
- Url::parse("https://registry.npmjs.org").unwrap()
-});
+ Url::parse("https://registry.npmjs.org").unwrap()
+ });
-pub fn npm_registry_default_url() -> &'static Url {
&NPM_REGISTRY_DEFAULT_URL
}
@@ -570,10 +574,16 @@ pub fn get_root_cert_store(
/// State provided to the process via an environment variable.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NpmProcessState {
- pub snapshot: deno_npm::resolution::SerializedNpmResolutionSnapshot,
+ pub kind: NpmProcessStateKind,
pub local_node_modules_path: Option<String>,
}
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum NpmProcessStateKind {
+ Snapshot(deno_npm::resolution::SerializedNpmResolutionSnapshot),
+ Byonm,
+}
+
const RESOLUTION_STATE_ENV_VAR_NAME: &str =
"DENO_DONT_USE_INTERNAL_NODE_COMPAT_STATE";
@@ -875,9 +885,11 @@ impl CliOptions {
pub fn resolve_npm_resolution_snapshot(
&self,
) -> Result<Option<ValidSerializedNpmResolutionSnapshot>, AnyError> {
- if let Some(state) = &*NPM_PROCESS_STATE {
+ if let Some(NpmProcessStateKind::Snapshot(snapshot)) =
+ NPM_PROCESS_STATE.as_ref().map(|s| &s.kind)
+ {
// TODO(bartlomieju): remove this clone
- Ok(Some(state.snapshot.clone().into_valid()?))
+ Ok(Some(snapshot.clone().into_valid()?))
} else {
Ok(None)
}
@@ -926,13 +938,6 @@ impl CliOptions {
})
}
- pub fn node_modules_dir_specifier(&self) -> Option<ModuleSpecifier> {
- self
- .maybe_node_modules_folder
- .as_ref()
- .map(|path| ModuleSpecifier::from_directory_path(path).unwrap())
- }
-
pub fn vendor_dir_path(&self) -> Option<&PathBuf> {
self.maybe_vendor_folder.as_ref()
}
@@ -1226,6 +1231,19 @@ impl CliOptions {
.unwrap_or(false)
}
+ pub fn unstable_byonm(&self) -> bool {
+ self.flags.unstable_byonm
+ || NPM_PROCESS_STATE
+ .as_ref()
+ .map(|s| matches!(s.kind, NpmProcessStateKind::Byonm))
+ .unwrap_or(false)
+ || self
+ .maybe_config_file()
+ .as_ref()
+ .map(|c| c.json.unstable.iter().any(|c| c == "byonm"))
+ .unwrap_or(false)
+ }
+
pub fn v8_flags(&self) -> &Vec<String> {
&self.flags.v8_flags
}
diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs
index 1d6a79963..5cc91f50f 100644
--- a/cli/cache/mod.rs
+++ b/cli/cache/mod.rs
@@ -4,6 +4,7 @@ use crate::args::CacheSetting;
use crate::errors::get_error_class_name;
use crate::file_fetcher::FetchOptions;
use crate::file_fetcher::FileFetcher;
+use crate::npm::CliNpmResolver;
use crate::util::fs::atomic_write_file;
use deno_ast::MediaType;
@@ -101,10 +102,10 @@ pub struct FetchCacher {
file_fetcher: Arc<FileFetcher>,
file_header_overrides: HashMap<ModuleSpecifier, HashMap<String, String>>,
global_http_cache: Arc<GlobalHttpCache>,
+ npm_resolver: Arc<dyn CliNpmResolver>,
parsed_source_cache: Arc<ParsedSourceCache>,
permissions: PermissionsContainer,
cache_info_enabled: bool,
- maybe_local_node_modules_url: Option<ModuleSpecifier>,
}
impl FetchCacher {
@@ -113,19 +114,19 @@ impl FetchCacher {
file_fetcher: Arc<FileFetcher>,
file_header_overrides: HashMap<ModuleSpecifier, HashMap<String, String>>,
global_http_cache: Arc<GlobalHttpCache>,
+ npm_resolver: Arc<dyn CliNpmResolver>,
parsed_source_cache: Arc<ParsedSourceCache>,
permissions: PermissionsContainer,
- maybe_local_node_modules_url: Option<ModuleSpecifier>,
) -> Self {
Self {
emit_cache,
file_fetcher,
file_header_overrides,
global_http_cache,
+ npm_resolver,
parsed_source_cache,
permissions,
cache_info_enabled: false,
- maybe_local_node_modules_url,
}
}
@@ -214,20 +215,18 @@ impl Loader for FetchCacher {
) -> LoadFuture {
use deno_graph::source::CacheSetting as LoaderCacheSetting;
- if let Some(node_modules_url) = self.maybe_local_node_modules_url.as_ref() {
+ if specifier.path().contains("/node_modules/") {
// The specifier might be in a completely different symlinked tree than
- // what the resolved node_modules_url is in (ex. `/my-project-1/node_modules`
- // symlinked to `/my-project-2/node_modules`), so first check if the path
- // is in a node_modules dir to avoid needlessly canonicalizing, then compare
+ // what the node_modules url is in (ex. `/my-project-1/node_modules`
+ // symlinked to `/my-project-2/node_modules`), so first we checked if the path
+ // is in a node_modules dir to avoid needlessly canonicalizing, then now compare
// against the canonicalized specifier.
- if specifier.path().contains("/node_modules/") {
- let specifier =
- crate::node::resolve_specifier_into_node_modules(specifier);
- if specifier.as_str().starts_with(node_modules_url.as_str()) {
- return Box::pin(futures::future::ready(Ok(Some(
- LoadResponse::External { specifier },
- ))));
- }
+ let specifier =
+ crate::node::resolve_specifier_into_node_modules(specifier);
+ if self.npm_resolver.in_npm_package(&specifier) {
+ return Box::pin(futures::future::ready(Ok(Some(
+ LoadResponse::External { specifier },
+ ))));
}
}
diff --git a/cli/factory.rs b/cli/factory.rs
index 0c887b720..b5240a85a 100644
--- a/cli/factory.rs
+++ b/cli/factory.rs
@@ -32,6 +32,7 @@ use crate::node::CliCjsCodeAnalyzer;
use crate::node::CliNodeCodeTranslator;
use crate::npm::create_cli_npm_resolver;
use crate::npm::CliNpmResolver;
+use crate::npm::CliNpmResolverByonmCreateOptions;
use crate::npm::CliNpmResolverCreateOptions;
use crate::npm::CliNpmResolverManagedCreateOptions;
use crate::npm::CliNpmResolverManagedPackageJsonInstallerOption;
@@ -300,7 +301,14 @@ impl CliFactory {
.npm_resolver
.get_or_try_init_async(async {
let fs = self.fs();
- create_cli_npm_resolver(
+ create_cli_npm_resolver(if self.options.unstable_byonm() {
+ CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions {
+ fs: fs.clone(),
+ // todo(byonm): actually resolve this properly because the package.json
+ // might be in an ancestor directory
+ root_node_modules_dir: self.options.initial_cwd().join("node_modules"),
+ })
+ } else {
CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions {
snapshot: match self.options.resolve_npm_resolution_snapshot()? {
Some(snapshot) => {
@@ -329,7 +337,7 @@ impl CliFactory {
npm_system_info: self.options.npm_system_info(),
npm_registry_url: crate::args::npm_registry_default_url().to_owned(),
})
- ).await
+ }).await
})
.await
}
@@ -365,24 +373,25 @@ impl CliFactory {
.services
.resolver
.get_or_try_init_async(async {
- Ok(Arc::new(CliGraphResolver::new(
- if self.options.no_npm() {
+ Ok(Arc::new(CliGraphResolver::new(CliGraphResolverOptions {
+ fs: self.fs().clone(),
+ cjs_resolutions: Some(self.cjs_resolutions().clone()),
+ node_resolver: Some(self.node_resolver().await?.clone()),
+ npm_resolver: if self.options.no_npm() {
None
} else {
Some(self.npm_resolver().await?.clone())
},
- self.package_json_deps_provider().clone(),
- CliGraphResolverOptions {
- maybe_jsx_import_source_config: self
- .options
- .to_maybe_jsx_import_source_config()?,
- maybe_import_map: self.maybe_import_map().await?.clone(),
- maybe_vendor_dir: self.options.vendor_dir_path(),
- bare_node_builtins_enabled: self
- .options
- .unstable_bare_node_builtlins(),
- },
- )))
+ package_json_deps_provider: self.package_json_deps_provider().clone(),
+ maybe_jsx_import_source_config: self
+ .options
+ .to_maybe_jsx_import_source_config()?,
+ maybe_import_map: self.maybe_import_map().await?.clone(),
+ maybe_vendor_dir: self.options.vendor_dir_path(),
+ bare_node_builtins_enabled: self
+ .options
+ .unstable_bare_node_builtlins(),
+ })))
})
.await
}
diff --git a/cli/graph_util.rs b/cli/graph_util.rs
index 9a2b805fd..d6b5228cf 100644
--- a/cli/graph_util.rs
+++ b/cli/graph_util.rs
@@ -435,9 +435,9 @@ impl ModuleGraphBuilder {
self.file_fetcher.clone(),
self.options.resolve_file_header_overrides(),
self.global_http_cache.clone(),
+ self.npm_resolver.clone(),
self.parsed_source_cache.clone(),
permissions,
- self.options.node_modules_dir_specifier(),
)
}
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index 6515e7dc0..f8ace060a 100644
--- a/cli/lsp/analysis.rs
+++ b/cli/lsp/analysis.rs
@@ -253,7 +253,7 @@ impl<'a> TsResponseImportMapper<'a> {
let root_folder = self
.npm_resolver
.as_ref()
- .and_then(|r| r.resolve_pkg_folder_from_specifier(specifier).ok())
+ .and_then(|r| r.resolve_package_folder_from_path(specifier).ok())
.flatten()?;
let package_json_path = root_folder.join("package.json");
let package_json_text = std::fs::read_to_string(&package_json_path).ok()?;
diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs
index 57957780b..e29ad785e 100644
--- a/cli/lsp/documents.rs
+++ b/cli/lsp/documents.rs
@@ -37,9 +37,11 @@ use deno_core::ModuleSpecifier;
use deno_graph::source::ResolutionMode;
use deno_graph::GraphImport;
use deno_graph::Resolution;
+use deno_runtime::deno_fs::RealFs;
use deno_runtime::deno_node;
use deno_runtime::deno_node::NodeResolution;
use deno_runtime::deno_node::NodeResolutionMode;
+use deno_runtime::deno_node::NodeResolver;
use deno_runtime::deno_node::PackageJson;
use deno_runtime::permissions::PermissionsContainer;
use deno_semver::npm::NpmPackageReqReference;
@@ -899,6 +901,7 @@ pub struct UpdateDocumentConfigOptions<'a> {
pub maybe_import_map: Option<Arc<import_map::ImportMap>>,
pub maybe_config_file: Option<&'a ConfigFile>,
pub maybe_package_json: Option<&'a PackageJson>,
+ pub node_resolver: Option<Arc<NodeResolver>>,
pub npm_resolver: Option<Arc<dyn CliNpmResolver>>,
}
@@ -955,16 +958,17 @@ impl Documents {
file_system_docs: Default::default(),
resolver_config_hash: 0,
imports: Default::default(),
- resolver: Arc::new(CliGraphResolver::new(
- None,
- Arc::new(PackageJsonDepsProvider::default()),
- CliGraphResolverOptions {
- maybe_jsx_import_source_config: None,
- maybe_import_map: None,
- maybe_vendor_dir: None,
- bare_node_builtins_enabled: false,
- },
- )),
+ resolver: Arc::new(CliGraphResolver::new(CliGraphResolverOptions {
+ fs: Arc::new(RealFs),
+ node_resolver: None,
+ npm_resolver: None,
+ cjs_resolutions: None,
+ package_json_deps_provider: Arc::new(PackageJsonDepsProvider::default()),
+ maybe_jsx_import_source_config: None,
+ maybe_import_map: None,
+ maybe_vendor_dir: None,
+ bare_node_builtins_enabled: false,
+ })),
npm_specifier_reqs: Default::default(),
has_injected_types_node_package: false,
specifier_resolver: Arc::new(SpecifierResolver::new(cache)),
@@ -1329,7 +1333,7 @@ impl Documents {
if let Some(package_json_deps) = &maybe_package_json_deps {
// We need to ensure the hashing is deterministic so explicitly type
// this in order to catch if the type of package_json_deps ever changes
- // from a sorted/deterministic IndexMap to something else.
+ // from a deterministic IndexMap to something else.
let package_json_deps: &IndexMap<_, _> = *package_json_deps;
for (key, value) in package_json_deps {
hasher.write_hashable(key);
@@ -1364,27 +1368,28 @@ impl Documents {
);
let deps_provider =
Arc::new(PackageJsonDepsProvider::new(maybe_package_json_deps));
- self.resolver = Arc::new(CliGraphResolver::new(
- options.npm_resolver,
- deps_provider,
- CliGraphResolverOptions {
- maybe_jsx_import_source_config: maybe_jsx_config,
- maybe_import_map: options.maybe_import_map,
- maybe_vendor_dir: options
- .maybe_config_file
- .and_then(|c| c.vendor_dir_path())
- .as_ref(),
- bare_node_builtins_enabled: options
- .maybe_config_file
- .map(|config| {
- config
- .json
- .unstable
- .contains(&"bare-node-builtins".to_string())
- })
- .unwrap_or(false),
- },
- ));
+ self.resolver = Arc::new(CliGraphResolver::new(CliGraphResolverOptions {
+ fs: Arc::new(RealFs),
+ node_resolver: options.node_resolver,
+ npm_resolver: options.npm_resolver,
+ cjs_resolutions: None, // only used for runtime
+ package_json_deps_provider: deps_provider,
+ maybe_jsx_import_source_config: maybe_jsx_config,
+ maybe_import_map: options.maybe_import_map,
+ maybe_vendor_dir: options
+ .maybe_config_file
+ .and_then(|c| c.vendor_dir_path())
+ .as_ref(),
+ bare_node_builtins_enabled: options
+ .maybe_config_file
+ .map(|config| {
+ config
+ .json
+ .unstable
+ .contains(&"bare-node-builtins".to_string())
+ })
+ .unwrap_or(false),
+ }));
self.imports = Arc::new(
if let Some(Ok(imports)) =
options.maybe_config_file.map(|cf| cf.to_maybe_imports())
@@ -2194,6 +2199,7 @@ console.log(b, "hello deno");
maybe_import_map: Some(Arc::new(import_map)),
maybe_config_file: None,
maybe_package_json: None,
+ node_resolver: None,
npm_resolver: None,
});
@@ -2235,6 +2241,7 @@ console.log(b, "hello deno");
maybe_import_map: Some(Arc::new(import_map)),
maybe_config_file: None,
maybe_package_json: None,
+ node_resolver: None,
npm_resolver: None,
});
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 8afcc7ffc..8b275a650 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -105,6 +105,7 @@ use crate::lsp::tsc::file_text_changes_to_workspace_edit;
use crate::lsp::urls::LspUrlKind;
use crate::npm::create_cli_npm_resolver_for_lsp;
use crate::npm::CliNpmResolver;
+use crate::npm::CliNpmResolverByonmCreateOptions;
use crate::npm::CliNpmResolverCreateOptions;
use crate::npm::CliNpmResolverManagedCreateOptions;
use crate::npm::CliNpmResolverManagedPackageJsonInstallerOption;
@@ -131,6 +132,8 @@ struct LspNpmServices {
config_hash: LspNpmConfigHash,
/// Npm's search api.
search_api: CliNpmSearchApi,
+ /// Node resolver.
+ node_resolver: Option<Arc<NodeResolver>>,
/// Resolver for npm packages.
resolver: Option<Arc<dyn CliNpmResolver>>,
}
@@ -495,6 +498,7 @@ impl Inner {
npm: LspNpmServices {
config_hash: LspNpmConfigHash(0), // this will be updated in initialize
search_api: npm_search_api,
+ node_resolver: None,
resolver: None,
},
performance,
@@ -815,15 +819,19 @@ impl Inner {
return; // no need to do anything
}
- self.npm.resolver = Some(
- create_npm_resolver(
- &deno_dir,
- &self.http_client,
- self.config.maybe_lockfile(),
- self.config.maybe_node_modules_dir_path().cloned(),
- )
- .await,
- );
+ let npm_resolver = create_npm_resolver(
+ &deno_dir,
+ &self.http_client,
+ self.config.maybe_config_file(),
+ self.config.maybe_lockfile(),
+ self.config.maybe_node_modules_dir_path().cloned(),
+ )
+ .await;
+ self.npm.node_resolver = Some(Arc::new(NodeResolver::new(
+ Arc::new(deno_fs::RealFs),
+ npm_resolver.clone().into_npm_resolver(),
+ )));
+ self.npm.resolver = Some(npm_resolver);
// update the hash
self.npm.config_hash = config_hash;
@@ -1059,11 +1067,24 @@ impl Inner {
async fn create_npm_resolver(
deno_dir: &DenoDir,
http_client: &Arc<HttpClient>,
+ maybe_config_file: Option<&ConfigFile>,
maybe_lockfile: Option<&Arc<Mutex<Lockfile>>>,
maybe_node_modules_dir_path: Option<PathBuf>,
) -> Arc<dyn CliNpmResolver> {
- create_cli_npm_resolver_for_lsp(CliNpmResolverCreateOptions::Managed(
- CliNpmResolverManagedCreateOptions {
+ let is_byonm = std::env::var("DENO_UNSTABLE_BYONM").as_deref() == Ok("1")
+ || maybe_config_file
+ .as_ref()
+ .map(|c| c.json.unstable.iter().any(|c| c == "byonm"))
+ .unwrap_or(false);
+ create_cli_npm_resolver_for_lsp(if is_byonm {
+ CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions {
+ fs: Arc::new(deno_fs::RealFs),
+ root_node_modules_dir: std::env::current_dir()
+ .unwrap()
+ .join("node_modules"),
+ })
+ } else {
+ CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions {
http_client: http_client.clone(),
snapshot: match maybe_lockfile {
Some(lockfile) => {
@@ -1090,8 +1111,8 @@ async fn create_npm_resolver(
CliNpmResolverManagedPackageJsonInstallerOption::NoInstall,
npm_registry_url: crate::args::npm_registry_default_url().to_owned(),
npm_system_info: NpmSystemInfo::default(),
- },
- ))
+ })
+ })
.await
}
@@ -1250,6 +1271,7 @@ impl Inner {
maybe_import_map: self.maybe_import_map.clone(),
maybe_config_file: self.config.maybe_config_file(),
maybe_package_json: self.maybe_package_json.as_ref(),
+ node_resolver: self.npm.node_resolver.clone(),
npm_resolver: self.npm.resolver.clone(),
});
diff --git a/cli/module_loader.rs b/cli/module_loader.rs
index 3f5e82d8c..f193c7e15 100644
--- a/cli/module_loader.rs
+++ b/cli/module_loader.rs
@@ -738,7 +738,7 @@ impl CliNodeResolver {
.with_context(|| format!("Could not resolve '{}'.", req_ref))
}
- fn resolve_package_sub_path(
+ pub fn resolve_package_sub_path(
&self,
package_folder: &Path,
sub_path: Option<&str>,
@@ -881,7 +881,7 @@ impl NpmModuleLoader {
}
/// Keeps track of what module specifiers were resolved as CJS.
-#[derive(Default)]
+#[derive(Debug, Default)]
pub struct CjsResolutionStore(Mutex<HashSet<ModuleSpecifier>>);
impl CjsResolutionStore {
diff --git a/cli/npm/byonm.rs b/cli/npm/byonm.rs
new file mode 100644
index 000000000..7aba83915
--- /dev/null
+++ b/cli/npm/byonm.rs
@@ -0,0 +1,274 @@
+// Copyright 2018-2023 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_ast::ModuleSpecifier;
+use deno_core::anyhow::bail;
+use deno_core::error::AnyError;
+use deno_core::serde_json;
+use deno_runtime::deno_fs::FileSystem;
+use deno_runtime::deno_node::NodePermissions;
+use deno_runtime::deno_node::NodeResolutionMode;
+use deno_runtime::deno_node::NpmResolver;
+use deno_runtime::deno_node::PackageJson;
+use deno_semver::package::PackageReq;
+
+use crate::args::package_json::get_local_package_json_version_reqs;
+use crate::args::NpmProcessState;
+use crate::args::NpmProcessStateKind;
+use crate::util::fs::canonicalize_path_maybe_not_exists;
+use crate::util::path::specifier_to_file_path;
+
+use super::common::types_package_name;
+use super::CliNpmResolver;
+use super::InnerCliNpmResolverRef;
+
+pub struct CliNpmResolverByonmCreateOptions {
+ pub fs: Arc<dyn FileSystem>,
+ pub root_node_modules_dir: PathBuf,
+}
+
+pub fn create_byonm_npm_resolver(
+ options: CliNpmResolverByonmCreateOptions,
+) -> Arc<dyn CliNpmResolver> {
+ Arc::new(ByonmCliNpmResolver {
+ fs: options.fs,
+ root_node_modules_dir: options.root_node_modules_dir,
+ })
+}
+
+#[derive(Debug)]
+pub struct ByonmCliNpmResolver {
+ fs: Arc<dyn FileSystem>,
+ root_node_modules_dir: PathBuf,
+}
+
+impl NpmResolver for ByonmCliNpmResolver {
+ fn resolve_package_folder_from_package(
+ &self,
+ name: &str,
+ referrer: &ModuleSpecifier,
+ mode: NodeResolutionMode,
+ ) -> Result<PathBuf, AnyError> {
+ fn inner(
+ fs: &dyn FileSystem,
+ name: &str,
+ package_root_path: &Path,
+ referrer: &ModuleSpecifier,
+ mode: NodeResolutionMode,
+ ) -> Result<PathBuf, AnyError> {
+ let mut current_folder = package_root_path;
+ loop {
+ let node_modules_folder = if current_folder.ends_with("node_modules") {
+ Cow::Borrowed(current_folder)
+ } else {
+ Cow::Owned(current_folder.join("node_modules"))
+ };
+
+ // attempt to resolve the types package first, then fallback to the regular package
+ if mode.is_types() && !name.starts_with("@types/") {
+ let sub_dir =
+ join_package_name(&node_modules_folder, &types_package_name(name));
+ if fs.is_dir_sync(&sub_dir) {
+ return Ok(sub_dir);
+ }
+ }
+
+ let sub_dir = join_package_name(&node_modules_folder, name);
+ if fs.is_dir_sync(&sub_dir) {
+ return Ok(sub_dir);
+ }
+
+ if let Some(parent) = current_folder.parent() {
+ current_folder = parent;
+ } else {
+ break;
+ }
+ }
+
+ bail!(
+ "could not find package '{}' from referrer '{}'.",
+ name,
+ referrer
+ );
+ }
+
+ let package_root_path =
+ self.resolve_package_folder_from_path(referrer)?.unwrap(); // todo(byonm): don't unwrap
+ let path = inner(&*self.fs, name, &package_root_path, referrer, mode)?;
+ Ok(self.fs.realpath_sync(&path)?)
+ }
+
+ fn resolve_package_folder_from_path(
+ &self,
+ specifier: &deno_core::ModuleSpecifier,
+ ) -> Result<Option<PathBuf>, AnyError> {
+ let path = specifier.to_file_path().unwrap(); // todo(byonm): don't unwrap
+ let path = self.fs.realpath_sync(&path)?;
+ if self.in_npm_package(specifier) {
+ let mut path = path.as_path();
+ while let Some(parent) = path.parent() {
+ if parent
+ .file_name()
+ .and_then(|f| f.to_str())
+ .map(|s| s.to_ascii_lowercase())
+ .as_deref()
+ == Some("node_modules")
+ {
+ return Ok(Some(path.to_path_buf()));
+ } else {
+ path = parent;
+ }
+ }
+ } else {
+ // find the folder with a package.json
+ // todo(dsherret): not exactly correct, but good enough for a first pass
+ let mut path = path.as_path();
+ while let Some(parent) = path.parent() {
+ if parent.join("package.json").exists() {
+ return Ok(Some(parent.to_path_buf()));
+ } else {
+ path = parent;
+ }
+ }
+ }
+ Ok(None)
+ }
+
+ fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool {
+ specifier.scheme() == "file"
+ && specifier
+ .path()
+ .to_ascii_lowercase()
+ .contains("/node_modules/")
+ }
+
+ fn ensure_read_permission(
+ &self,
+ permissions: &dyn NodePermissions,
+ path: &Path,
+ ) -> Result<(), AnyError> {
+ if !path
+ .components()
+ .any(|c| c.as_os_str().to_ascii_lowercase() == "node_modules")
+ {
+ permissions.check_read(path)?;
+ }
+ Ok(())
+ }
+}
+
+impl CliNpmResolver for ByonmCliNpmResolver {
+ fn into_npm_resolver(self: Arc<Self>) -> Arc<dyn NpmResolver> {
+ self
+ }
+
+ fn clone_snapshotted(&self) -> Arc<dyn CliNpmResolver> {
+ Arc::new(Self {
+ fs: self.fs.clone(),
+ root_node_modules_dir: self.root_node_modules_dir.clone(),
+ })
+ }
+
+ fn as_inner(&self) -> InnerCliNpmResolverRef {
+ InnerCliNpmResolverRef::Byonm(self)
+ }
+
+ fn root_node_modules_path(&self) -> Option<std::path::PathBuf> {
+ Some(self.root_node_modules_dir.clone())
+ }
+
+ fn resolve_pkg_folder_from_deno_module_req(
+ &self,
+ req: &PackageReq,
+ referrer: &ModuleSpecifier,
+ ) -> Result<PathBuf, AnyError> {
+ fn resolve_from_package_json(
+ req: &PackageReq,
+ fs: &dyn FileSystem,
+ path: PathBuf,
+ ) -> Result<PathBuf, AnyError> {
+ let package_json = PackageJson::load_skip_read_permission(fs, path)?;
+ let deps = get_local_package_json_version_reqs(&package_json);
+ for (key, value) in deps {
+ if let Ok(value) = value {
+ if value.name == req.name
+ && value.version_req.intersects(&req.version_req)
+ {
+ let package_path = package_json
+ .path
+ .parent()
+ .unwrap()
+ .join("node_modules")
+ .join(key);
+ return Ok(canonicalize_path_maybe_not_exists(&package_path)?);
+ }
+ }
+ }
+ bail!(
+ concat!(
+ "Could not find a matching package for 'npm:{}' in '{}'. ",
+ "You must specify this as a package.json dependency when the ",
+ "node_modules folder is not managed by Deno.",
+ ),
+ req,
+ package_json.path.display()
+ );
+ }
+
+ // attempt to resolve the npm specifier from the referrer's package.json,
+ // but otherwise fallback to the project's package.json
+ if let Ok(file_path) = specifier_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 self.fs.exists_sync(&package_json_path) {
+ return resolve_from_package_json(
+ req,
+ self.fs.as_ref(),
+ package_json_path,
+ );
+ }
+ current_path = dir_path;
+ }
+ }
+
+ resolve_from_package_json(
+ req,
+ self.fs.as_ref(),
+ self
+ .root_node_modules_dir
+ .parent()
+ .unwrap()
+ .join("package.json"),
+ )
+ }
+
+ fn get_npm_process_state(&self) -> String {
+ serde_json::to_string(&NpmProcessState {
+ kind: NpmProcessStateKind::Byonm,
+ local_node_modules_path: Some(
+ self.root_node_modules_dir.to_string_lossy().to_string(),
+ ),
+ })
+ .unwrap()
+ }
+
+ fn check_state_hash(&self) -> Option<u64> {
+ // it is very difficult to determine the check state hash for byonm
+ // so we just return None to signify check caching is not supported
+ None
+ }
+}
+
+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
+}
diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs
index b85f1130f..68b5c2134 100644
--- a/cli/npm/managed/mod.rs
+++ b/cli/npm/managed/mod.rs
@@ -27,6 +27,7 @@ use deno_semver::package::PackageReq;
use crate::args::Lockfile;
use crate::args::NpmProcessState;
+use crate::args::NpmProcessStateKind;
use crate::args::PackageJsonDepsProvider;
use crate::cache::FastInsecureHasher;
use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs;
@@ -508,7 +509,18 @@ impl NpmResolver for ManagedCliNpmResolver {
&self,
specifier: &ModuleSpecifier,
) -> Result<Option<PathBuf>, AnyError> {
- self.resolve_pkg_folder_from_specifier(specifier)
+ let Some(path) = self
+ .fs_resolver
+ .resolve_package_folder_from_specifier(specifier)?
+ else {
+ return Ok(None);
+ };
+ log::debug!(
+ "Resolved package folder of {} to {}",
+ specifier,
+ path.display()
+ );
+ Ok(Some(path))
}
fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool {
@@ -568,27 +580,6 @@ impl CliNpmResolver for ManagedCliNpmResolver {
self.fs_resolver.node_modules_path()
}
- /// Resolve the root folder of the package the provided specifier is in.
- ///
- /// This will error when the provided specifier is not in an npm package.
- fn resolve_pkg_folder_from_specifier(
- &self,
- specifier: &ModuleSpecifier,
- ) -> Result<Option<PathBuf>, AnyError> {
- let Some(path) = self
- .fs_resolver
- .resolve_package_folder_from_specifier(specifier)?
- else {
- return Ok(None);
- };
- log::debug!(
- "Resolved package folder of {} to {}",
- specifier,
- path.display()
- );
- Ok(Some(path))
- }
-
fn resolve_pkg_folder_from_deno_module_req(
&self,
req: &PackageReq,
@@ -601,10 +592,12 @@ impl CliNpmResolver for ManagedCliNpmResolver {
/// Gets the state of npm for the process.
fn get_npm_process_state(&self) -> String {
serde_json::to_string(&NpmProcessState {
- snapshot: self
- .resolution
- .serialized_valid_snapshot()
- .into_serialized(),
+ kind: NpmProcessStateKind::Snapshot(
+ self
+ .resolution
+ .serialized_valid_snapshot()
+ .into_serialized(),
+ ),
local_node_modules_path: self
.fs_resolver
.node_modules_path()
diff --git a/cli/npm/managed/resolvers/local.rs b/cli/npm/managed/resolvers/local.rs
index 2d774518a..a4a8550f1 100644
--- a/cli/npm/managed/resolvers/local.rs
+++ b/cli/npm/managed/resolvers/local.rs
@@ -36,7 +36,6 @@ use deno_runtime::deno_core::futures;
use deno_runtime::deno_fs;
use deno_runtime::deno_node::NodePermissions;
use deno_runtime::deno_node::NodeResolutionMode;
-use deno_runtime::deno_node::PackageJson;
use deno_semver::package::PackageNv;
use serde::Deserialize;
use serde::Serialize;
@@ -181,23 +180,8 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver {
} else {
Cow::Owned(current_folder.join("node_modules"))
};
- let sub_dir = join_package_name(&node_modules_folder, name);
- if self.fs.is_dir_sync(&sub_dir) {
- // if doing types resolution, only resolve the package if it specifies a types property
- if mode.is_types() && !name.starts_with("@types/") {
- let package_json = PackageJson::load_skip_read_permission(
- &*self.fs,
- sub_dir.join("package.json"),
- )?;
- if package_json.types.is_some() {
- return Ok(sub_dir);
- }
- } else {
- return Ok(sub_dir);
- }
- }
- // if doing type resolution, check for the existence of a @types package
+ // attempt to resolve the types package first, then fallback to the regular package
if mode.is_types() && !name.starts_with("@types/") {
let sub_dir =
join_package_name(&node_modules_folder, &types_package_name(name));
@@ -206,6 +190,11 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver {
}
}
+ let sub_dir = join_package_name(&node_modules_folder, name);
+ if self.fs.is_dir_sync(&sub_dir) {
+ return Ok(sub_dir);
+ }
+
if current_folder == self.root_node_modules_path {
bail!(
"could not find package '{}' from referrer '{}'.",
diff --git a/cli/npm/managed/tarball.rs b/cli/npm/managed/tarball.rs
index e72b1afc8..17ab7711f 100644
--- a/cli/npm/managed/tarball.rs
+++ b/cli/npm/managed/tarball.rs
@@ -52,15 +52,15 @@ fn verify_tarball_integrity(
let mut hash_ctx = Context::new(algo);
hash_ctx.update(data);
let digest = hash_ctx.finish();
- let tarball_checksum = base64::encode(digest.as_ref()).to_lowercase();
- (tarball_checksum, base64_hash.to_lowercase())
+ let tarball_checksum = base64::encode(digest.as_ref());
+ (tarball_checksum, base64_hash)
}
NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(hex) => {
let mut hash_ctx = Context::new(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY);
hash_ctx.update(data);
let digest = hash_ctx.finish();
- let tarball_checksum = hex::encode(digest.as_ref()).to_lowercase();
- (tarball_checksum, hex.to_lowercase())
+ let tarball_checksum = hex::encode(digest.as_ref());
+ (tarball_checksum, hex)
}
NpmPackageVersionDistInfoIntegrity::UnknownIntegrity(integrity) => {
bail!(
@@ -71,7 +71,7 @@ fn verify_tarball_integrity(
}
};
- if tarball_checksum != expected_checksum {
+ if tarball_checksum != *expected_checksum {
bail!(
"Tarball checksum did not match what was provided by npm registry for {}.\n\nExpected: {}\nActual: {}",
package,
@@ -158,7 +158,7 @@ mod test {
version: Version::parse_from_npm("1.0.0").unwrap(),
};
let actual_checksum =
- "z4phnx7vul3xvchq1m2ab9yg5aulvxxcg/spidns6c5h0ne8xyxysp+dgnkhfuwvy7kxvudbeoglodj6+sfapg==";
+ "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==";
assert_eq!(
verify_tarball_integrity(
&package,
@@ -195,7 +195,7 @@ mod test {
.to_string(),
concat!(
"Tarball checksum did not match what was provided by npm ",
- "registry for package@1.0.0.\n\nExpected: test\nActual: 2jmj7l5rsw0yvb/vlwaykk/ybwk=",
+ "registry for package@1.0.0.\n\nExpected: test\nActual: 2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
),
);
assert_eq!(
diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs
index 81f46419e..761c99dba 100644
--- a/cli/npm/mod.rs
+++ b/cli/npm/mod.rs
@@ -1,5 +1,6 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+mod byonm;
mod cache_dir;
mod common;
mod managed;
@@ -12,17 +13,18 @@ use deno_core::error::AnyError;
use deno_runtime::deno_node::NpmResolver;
use deno_semver::package::PackageReq;
+pub use self::byonm::CliNpmResolverByonmCreateOptions;
pub use self::cache_dir::NpmCacheDir;
pub use self::managed::CliNpmResolverManagedCreateOptions;
pub use self::managed::CliNpmResolverManagedPackageJsonInstallerOption;
pub use self::managed::CliNpmResolverManagedSnapshotOption;
pub use self::managed::ManagedCliNpmResolver;
+use self::byonm::ByonmCliNpmResolver;
+
pub enum CliNpmResolverCreateOptions {
Managed(CliNpmResolverManagedCreateOptions),
- // todo(dsherret): implement this
- #[allow(dead_code)]
- Byonm,
+ Byonm(CliNpmResolverByonmCreateOptions),
}
pub async fn create_cli_npm_resolver_for_lsp(
@@ -33,7 +35,7 @@ pub async fn create_cli_npm_resolver_for_lsp(
Managed(options) => {
managed::create_managed_npm_resolver_for_lsp(options).await
}
- Byonm => todo!(),
+ Byonm(options) => byonm::create_byonm_npm_resolver(options),
}
}
@@ -43,7 +45,7 @@ pub async fn create_cli_npm_resolver(
use CliNpmResolverCreateOptions::*;
match options {
Managed(options) => managed::create_managed_npm_resolver(options).await,
- Byonm => todo!(),
+ Byonm(options) => Ok(byonm::create_byonm_npm_resolver(options)),
}
}
@@ -76,12 +78,6 @@ pub trait CliNpmResolver: NpmResolver {
fn root_node_modules_path(&self) -> Option<PathBuf>;
- /// Resolve the root folder of the package the provided specifier is in.
- fn resolve_pkg_folder_from_specifier(
- &self,
- specifier: &ModuleSpecifier,
- ) -> Result<Option<PathBuf>, AnyError>;
-
fn resolve_pkg_folder_from_deno_module_req(
&self,
req: &PackageReq,
@@ -95,6 +91,3 @@ pub trait CliNpmResolver: NpmResolver {
/// or `None` if the state currently can't be determined.
fn check_state_hash(&self) -> Option<u64>;
}
-
-// todo(#18967): implement this
-pub struct ByonmCliNpmResolver;
diff --git a/cli/resolver.rs b/cli/resolver.rs
index f48e62aae..c4037a75a 100644
--- a/cli/resolver.rs
+++ b/cli/resolver.rs
@@ -13,7 +13,13 @@ use deno_graph::source::ResolveError;
use deno_graph::source::Resolver;
use deno_graph::source::UnknownBuiltInNodeModuleError;
use deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE;
+use deno_runtime::deno_fs::FileSystem;
use deno_runtime::deno_node::is_builtin_node_module;
+use deno_runtime::deno_node::NodeResolution;
+use deno_runtime::deno_node::NodeResolutionMode;
+use deno_runtime::deno_node::NodeResolver;
+use deno_runtime::permissions::PermissionsContainer;
+use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageReq;
use import_map::ImportMap;
use std::path::PathBuf;
@@ -22,6 +28,7 @@ use std::sync::Arc;
use crate::args::package_json::PackageJsonDeps;
use crate::args::JsxImportSourceConfig;
use crate::args::PackageJsonDepsProvider;
+use crate::module_loader::CjsResolutionStore;
use crate::npm::CliNpmResolver;
use crate::npm::InnerCliNpmResolverRef;
use crate::util::sync::AtomicFlag;
@@ -99,16 +106,24 @@ impl MappedSpecifierResolver {
/// import map, JSX settings.
#[derive(Debug)]
pub struct CliGraphResolver {
+ fs: Arc<dyn FileSystem>,
mapped_specifier_resolver: MappedSpecifierResolver,
maybe_default_jsx_import_source: Option<String>,
maybe_jsx_import_source_module: Option<String>,
maybe_vendor_specifier: Option<ModuleSpecifier>,
+ cjs_resolutions: Option<Arc<CjsResolutionStore>>,
+ node_resolver: Option<Arc<NodeResolver>>,
npm_resolver: Option<Arc<dyn CliNpmResolver>>,
found_package_json_dep_flag: Arc<AtomicFlag>,
bare_node_builtins_enabled: bool,
}
pub struct CliGraphResolverOptions<'a> {
+ pub fs: Arc<dyn FileSystem>,
+ pub cjs_resolutions: Option<Arc<CjsResolutionStore>>,
+ pub node_resolver: Option<Arc<NodeResolver>>,
+ pub npm_resolver: Option<Arc<dyn CliNpmResolver>>,
+ pub package_json_deps_provider: Arc<PackageJsonDepsProvider>,
pub maybe_jsx_import_source_config: Option<JsxImportSourceConfig>,
pub maybe_import_map: Option<Arc<ImportMap>>,
pub maybe_vendor_dir: Option<&'a PathBuf>,
@@ -116,15 +131,23 @@ pub struct CliGraphResolverOptions<'a> {
}
impl CliGraphResolver {
- pub fn new(
- npm_resolver: Option<Arc<dyn CliNpmResolver>>,
- package_json_deps_provider: Arc<PackageJsonDepsProvider>,
- options: CliGraphResolverOptions,
- ) -> Self {
+ pub fn new(options: CliGraphResolverOptions) -> Self {
+ let is_byonm = options
+ .npm_resolver
+ .as_ref()
+ .map(|n| n.as_byonm().is_some())
+ .unwrap_or(false);
Self {
+ fs: options.fs,
+ cjs_resolutions: options.cjs_resolutions,
mapped_specifier_resolver: MappedSpecifierResolver::new(
options.maybe_import_map,
- package_json_deps_provider,
+ if is_byonm {
+ // don't resolve from the root package.json deps for byonm
+ Arc::new(PackageJsonDepsProvider::new(None))
+ } else {
+ options.package_json_deps_provider
+ },
),
maybe_default_jsx_import_source: options
.maybe_jsx_import_source_config
@@ -136,7 +159,8 @@ impl CliGraphResolver {
maybe_vendor_specifier: options
.maybe_vendor_dir
.and_then(|v| ModuleSpecifier::from_directory_path(v).ok()),
- npm_resolver,
+ node_resolver: options.node_resolver,
+ npm_resolver: options.npm_resolver,
found_package_json_dep_flag: Default::default(),
bare_node_builtins_enabled: options.bare_node_builtins_enabled,
}
@@ -171,8 +195,15 @@ impl Resolver for CliGraphResolver {
&self,
specifier: &str,
referrer: &ModuleSpecifier,
- _mode: ResolutionMode,
+ mode: ResolutionMode,
) -> Result<ModuleSpecifier, ResolveError> {
+ fn to_node_mode(mode: ResolutionMode) -> NodeResolutionMode {
+ match mode {
+ ResolutionMode::Execution => NodeResolutionMode::Execution,
+ ResolutionMode::Types => NodeResolutionMode::Types,
+ }
+ }
+
let result = match self
.mapped_specifier_resolver
.resolve(specifier, referrer)?
@@ -200,6 +231,80 @@ impl Resolver for CliGraphResolver {
}
}
+ if let Some(resolver) =
+ self.npm_resolver.as_ref().and_then(|r| r.as_byonm())
+ {
+ match &result {
+ Ok(specifier) => {
+ if let Ok(npm_req_ref) =
+ NpmPackageReqReference::from_specifier(specifier)
+ {
+ let package_folder = resolver
+ .resolve_pkg_folder_from_deno_module_req(
+ npm_req_ref.req(),
+ referrer,
+ )?;
+ let node_resolver = self.node_resolver.as_ref().unwrap();
+ let package_json_path = package_folder.join("package.json");
+ if !self.fs.exists_sync(&package_json_path) {
+ return Err(ResolveError::Other(anyhow!(
+ "Could not find '{}'. Maybe run `npm install`?",
+ package_json_path.display()
+ )));
+ }
+ let maybe_resolution = node_resolver
+ .resolve_package_subpath_from_deno_module(
+ &package_folder,
+ npm_req_ref.sub_path(),
+ referrer,
+ to_node_mode(mode),
+ &PermissionsContainer::allow_all(),
+ )?;
+ match maybe_resolution {
+ Some(resolution) => {
+ if let Some(cjs_resolutions) = &self.cjs_resolutions {
+ if let NodeResolution::CommonJs(specifier) = &resolution {
+ // remember that this was a common js resolution
+ cjs_resolutions.insert(specifier.clone());
+ }
+ }
+
+ return Ok(resolution.into_url());
+ }
+ None => {
+ return Err(ResolveError::Other(anyhow!(
+ "Failed resolving package subpath for '{}' in '{}'.",
+ npm_req_ref,
+ package_folder.display()
+ )));
+ }
+ }
+ }
+ }
+ Err(_) => {
+ if referrer.scheme() == "file" {
+ if let Some(node_resolver) = &self.node_resolver {
+ let node_result = node_resolver.resolve(
+ specifier,
+ referrer,
+ to_node_mode(mode),
+ &PermissionsContainer::allow_all(),
+ );
+ if let Ok(Some(resolution)) = node_result {
+ if let Some(cjs_resolutions) = &self.cjs_resolutions {
+ if let NodeResolution::CommonJs(specifier) = &resolution {
+ // remember that this was a common js resolution
+ cjs_resolutions.insert(specifier.clone());
+ }
+ }
+ return Ok(resolution.into_url());
+ }
+ }
+ }
+ }
+ }
+ }
+
result
}
}
diff --git a/cli/tests/integration/cert_tests.rs b/cli/tests/integration/cert_tests.rs
index ffd4b449d..20bf4d80d 100644
--- a/cli/tests/integration/cert_tests.rs
+++ b/cli/tests/integration/cert_tests.rs
@@ -118,7 +118,7 @@ fn cafile_compile() {
context
.new_command()
- .command_name(output_exe)
+ .name(output_exe)
.run()
.assert_matches_text("[WILDCARD]\nHello\n");
}
diff --git a/cli/tests/integration/compile_tests.rs b/cli/tests/integration/compile_tests.rs
index 657d17d7d..3ca57a35f 100644
--- a/cli/tests/integration/compile_tests.rs
+++ b/cli/tests/integration/compile_tests.rs
@@ -29,7 +29,7 @@ fn compile_basic() {
.run();
output.assert_exit_code(0);
output.skip_output_check();
- let output = context.new_command().command_name(&exe).run();
+ let output = context.new_command().name(&exe).run();
output.assert_matches_text("Welcome to Deno!\n");
}
@@ -42,7 +42,7 @@ fn compile_basic() {
.new_command()
// it should fail creating this, but still work
.env("DENO_DIR", readonly_sub_dir)
- .command_name(exe)
+ .name(exe)
.run();
output.assert_matches_text("Welcome to Deno!\n");
}
@@ -688,11 +688,7 @@ fn workers_not_in_module_map() {
output.assert_exit_code(0);
output.skip_output_check();
- let output = context
- .new_command()
- .command_name(exe)
- .env("NO_COLOR", "")
- .run();
+ let output = context.new_command().name(exe).env("NO_COLOR", "").run();
output.assert_exit_code(1);
output.assert_matches_text(concat!(
"error: Uncaught (in worker \"\") Module not found: [WILDCARD]",
@@ -825,7 +821,7 @@ fn compile_npm_specifiers() {
output.assert_exit_code(0);
output.skip_output_check();
- let output = context.new_command().command_name(&binary_path).run();
+ let output = context.new_command().name(&binary_path).run();
output.assert_matches_text(
r#"Node esm importing node cjs
===========================
@@ -881,7 +877,7 @@ testing[WILDCARD]this
output.assert_exit_code(0);
output.skip_output_check();
- let output = context.new_command().command_name(binary_path).run();
+ let output = context.new_command().name(binary_path).run();
output.assert_matches_text("2\n");
}
@@ -1050,7 +1046,7 @@ fn run_npm_bin_compile_test(opts: RunNpmBinCompileOptions) {
};
let output = context
.new_command()
- .command_name(binary_path)
+ .name(binary_path)
.args_vec(opts.run_args)
.run();
output.assert_matches_file(opts.output_file);
@@ -1114,6 +1110,6 @@ fn compile_node_modules_symlink_outside() {
// run
let binary_path =
project_dir.join(if cfg!(windows) { "bin.exe" } else { "bin" });
- let output = context.new_command().command_name(binary_path).run();
+ let output = context.new_command().name(binary_path).run();
output.assert_matches_file("compile/node_modules_symlink_outside/main.out");
}
diff --git a/cli/tests/integration/npm_tests.rs b/cli/tests/integration/npm_tests.rs
index ddbc79cec..c3fe2949c 100644
--- a/cli/tests/integration/npm_tests.rs
+++ b/cli/tests/integration/npm_tests.rs
@@ -2219,3 +2219,240 @@ itest!(require_resolve_url_paths {
cwd: Some("npm/require_resolve_url/"),
copy_temp_dir: Some("npm/require_resolve_url/"),
});
+
+#[test]
+pub fn byonm_cjs_esm_packages() {
+ let test_context = TestContextBuilder::for_npm()
+ .env("DENO_UNSTABLE_BYONM", "1")
+ .use_temp_cwd()
+ .build();
+ let dir = test_context.temp_dir();
+ let run_npm = |args: &str| {
+ test_context
+ .new_command()
+ .name("npm")
+ .args(args)
+ .run()
+ .skip_output_check();
+ };
+
+ run_npm("init -y");
+ run_npm("install @denotest/esm-basic @denotest/cjs-default-export @denotest/dual-cjs-esm chalk@4 chai@4.3");
+
+ dir.write(
+ "main.ts",
+ r#"
+import { getValue, setValue } from "@denotest/esm-basic";
+
+setValue(2);
+console.log(getValue());
+
+import cjsDefault from "@denotest/cjs-default-export";
+console.log(cjsDefault.default());
+console.log(cjsDefault.named());
+
+import { getKind } from "@denotest/dual-cjs-esm";
+console.log(getKind());
+
+
+"#,
+ );
+ let output = test_context.new_command().args("run --check main.ts").run();
+ output
+ .assert_matches_text("Check file:///[WILDCARD]/main.ts\n2\n1\n2\nesm\n");
+
+ // should not have created the .deno directory
+ assert!(!dir.path().join("node_modules/.deno").exists());
+
+ // try chai
+ dir.write(
+ "chai.ts",
+ r#"import { expect } from "chai";
+
+ const timeout = setTimeout(() => {}, 0);
+ expect(timeout).to.be.a("number");
+ clearTimeout(timeout);"#,
+ );
+ test_context.new_command().args("run chai.ts").run();
+
+ // try chalk cjs
+ dir.write(
+ "chalk.ts",
+ "import chalk from 'chalk'; console.log(chalk.green('chalk cjs loads'));",
+ );
+ let output = test_context
+ .new_command()
+ .args("run --allow-read chalk.ts")
+ .run();
+ output.assert_matches_text("chalk cjs loads\n");
+
+ // try using an npm specifier for chalk that matches the version we installed
+ dir.write(
+ "chalk.ts",
+ "import chalk from 'npm:chalk@4'; console.log(chalk.green('chalk cjs loads'));",
+ );
+ let output = test_context
+ .new_command()
+ .args("run --allow-read chalk.ts")
+ .run();
+ output.assert_matches_text("chalk cjs loads\n");
+
+ // try with one that doesn't match the package.json
+ dir.write(
+ "chalk.ts",
+ "import chalk from 'npm:chalk@5'; console.log(chalk.green('chalk cjs loads'));",
+ );
+ let output = test_context
+ .new_command()
+ .args("run --allow-read chalk.ts")
+ .run();
+ output.assert_matches_text(
+ r#"error: Could not find a matching package for 'npm:chalk@5' in '[WILDCARD]package.json'. You must specify this as a package.json dependency when the node_modules folder is not managed by Deno.
+ at file:///[WILDCARD]chalk.ts:1:19
+"#);
+ output.assert_exit_code(1);
+}
+
+#[test]
+pub fn byonm_package_npm_specifier_not_installed_and_invalid_subpath() {
+ let test_context = TestContextBuilder::for_npm()
+ .env("DENO_UNSTABLE_BYONM", "1")
+ .use_temp_cwd()
+ .build();
+ let dir = test_context.temp_dir();
+ dir.path().join("package.json").write_json(&json!({
+ "dependencies": {
+ "chalk": "4",
+ "@denotest/conditional-exports-strict": "1"
+ }
+ }));
+ dir.write(
+ "main.ts",
+ "import chalk from 'npm:chalk'; console.log(chalk.green('hi'));",
+ );
+
+ // no npm install has been run, so this should give an informative error
+ let output = test_context.new_command().args("run main.ts").run();
+ output.assert_matches_text(
+ r#"error: Could not find '[WILDCARD]package.json'. Maybe run `npm install`?
+ at file:///[WILDCARD]/main.ts:1:19
+"#,
+ );
+ output.assert_exit_code(1);
+
+ // now test for an invalid sub path after doing an npm install
+ dir.write(
+ "main.ts",
+ "import 'npm:@denotest/conditional-exports-strict/test';",
+ );
+
+ test_context
+ .new_command()
+ .name("npm")
+ .args("install")
+ .run()
+ .skip_output_check();
+
+ let output = test_context.new_command().args("run main.ts").run();
+ output.assert_matches_text(
+ r#"error: Failed resolving package subpath './test' for '[WILDCARD]package.json'
+ at file:///[WILDCARD]/main.ts:1:8
+"#,
+ );
+ output.assert_exit_code(1);
+}
+
+#[test]
+pub fn byonm_npm_workspaces() {
+ let test_context = TestContextBuilder::for_npm().use_temp_cwd().build();
+ let dir = test_context.temp_dir();
+ dir.write(
+ "deno.json",
+ r#"{
+ "unstable": [
+ "byonm"
+ ]
+ }"#,
+ );
+
+ dir.write(
+ "package.json",
+ r#"{
+ "name": "my-workspace",
+ "workspaces": [
+ "project-a",
+ "project-b"
+ ]
+}
+"#,
+ );
+
+ let project_a_dir = dir.path().join("project-a");
+ project_a_dir.create_dir_all();
+ project_a_dir.join("package.json").write_json(&json!({
+ "name": "project-a",
+ "version": "1.0.0",
+ "main": "./index.js",
+ "type": "module",
+ "dependencies": {
+ "chai": "^4.2",
+ "project-b": "^1"
+ }
+ }));
+ project_a_dir.join("index.js").write(
+ r#"
+import { expect } from "chai";
+
+const timeout = setTimeout(() => {}, 0);
+expect(timeout).to.be.a("number");
+clearTimeout(timeout);
+
+export function add(a, b) {
+ return a + b;
+}
+"#,
+ );
+ project_a_dir
+ .join("index.d.ts")
+ .write("export function add(a: number, b: number): number;");
+
+ let project_b_dir = dir.path().join("project-b");
+ project_b_dir.create_dir_all();
+ project_b_dir.join("package.json").write_json(&json!({
+ "name": "project-b",
+ "version": "1.0.0",
+ "type": "module",
+ "dependencies": {
+ "@denotest/esm-basic": "^1.0",
+ }
+ }));
+ project_b_dir.join("main.ts").write(
+ r#"
+import { getValue, setValue } from "@denotest/esm-basic";
+
+setValue(5);
+console.log(getValue());
+
+import { add } from "project-a";
+console.log(add(1, 2));
+"#,
+ );
+
+ test_context
+ .new_command()
+ .name("npm")
+ .args("install")
+ .run()
+ .skip_output_check();
+
+ let output = test_context
+ .new_command()
+ .args("run ./project-b/main.ts")
+ .run();
+ output.assert_matches_text("5\n3\n");
+ let output = test_context
+ .new_command()
+ .args("check ./project-b/main.ts")
+ .run();
+ output.assert_matches_text("Check file:///[WILDCARD]/project-b/main.ts\n");
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/cjs/index.cjs b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/cjs/index.cjs
new file mode 100644
index 000000000..16895e48c
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/cjs/index.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ hello: "from cjs"
+}; \ No newline at end of file
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/bar.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/bar.js
new file mode 100644
index 000000000..1474f5d29
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/bar.js
@@ -0,0 +1,3 @@
+export default {
+ hello: "from esm client bar",
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/foo.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/foo.js
new file mode 100644
index 000000000..bb5284b15
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/foo.js
@@ -0,0 +1,3 @@
+export default {
+ hello: "from esm client foo",
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/index.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/index.js
new file mode 100644
index 000000000..dc1ec197d
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/index.js
@@ -0,0 +1,3 @@
+export default {
+ hello: "from esm client",
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/m.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/m.js
new file mode 100644
index 000000000..fec6807ac
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/client/m.js
@@ -0,0 +1,3 @@
+export default {
+ hello: "from esm client m",
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/index.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/index.js
new file mode 100644
index 000000000..38dae7d93
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/esm/index.js
@@ -0,0 +1,3 @@
+export default {
+ hello: "from esm",
+} \ No newline at end of file
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/foo.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/foo.js
new file mode 100644
index 000000000..6060c8a67
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/foo.js
@@ -0,0 +1,3 @@
+export default {
+ hello: "from foo",
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/package.json
new file mode 100644
index 000000000..3576e48f8
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports-strict/1.0.0/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@denotest/conditional-exports-strict",
+ "version": "1.0.0",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./types/src/index.d.ts",
+ "require": "./cjs/index.cjs",
+ "import": "./esm/index.js"
+ },
+ "./client": {
+ "types": "./types/src/client/index.d.ts",
+ "import": "./esm/client/index.js"
+ }
+ }
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/bar.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/bar.js
index 12352639d..1474f5d29 100644
--- a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/bar.js
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/bar.js
@@ -1,3 +1,3 @@
export default {
hello: "from esm client bar",
-} \ No newline at end of file
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/foo.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/foo.js
index 1ab5baf1b..bb5284b15 100644
--- a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/foo.js
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/foo.js
@@ -1,3 +1,3 @@
export default {
hello: "from esm client foo",
-} \ No newline at end of file
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/index.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/index.js
index 86f246be4..dc1ec197d 100644
--- a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/index.js
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/index.js
@@ -1,3 +1,3 @@
export default {
hello: "from esm client",
-} \ No newline at end of file
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/m.js b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/m.js
index 40e769031..fec6807ac 100644
--- a/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/m.js
+++ b/cli/tests/testdata/npm/registry/@denotest/conditional-exports/1.0.0/esm/client/m.js
@@ -1,3 +1,3 @@
export default {
hello: "from esm client m",
-} \ No newline at end of file
+}
diff --git a/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.cts b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.cts
new file mode 100644
index 000000000..f969ba996
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.cts
@@ -0,0 +1 @@
+export function getKind(): string;
diff --git a/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.mts b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.mts
new file mode 100644
index 000000000..f969ba996
--- /dev/null
+++ b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/main.d.mts
@@ -0,0 +1 @@
+export function getKind(): string;
diff --git a/cli/tools/vendor/test.rs b/cli/tools/vendor/test.rs
index 810daf4fb..73a4324cf 100644
--- a/cli/tools/vendor/test.rs
+++ b/cli/tools/vendor/test.rs
@@ -19,6 +19,7 @@ use deno_graph::source::LoadResponse;
use deno_graph::source::Loader;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
+use deno_runtime::deno_fs::RealFs;
use import_map::ImportMap;
use crate::args::JsxImportSourceConfig;
@@ -293,16 +294,17 @@ fn build_resolver(
maybe_jsx_import_source_config: Option<JsxImportSourceConfig>,
original_import_map: Option<ImportMap>,
) -> CliGraphResolver {
- CliGraphResolver::new(
- None,
- Default::default(),
- CliGraphResolverOptions {
- maybe_jsx_import_source_config,
- maybe_import_map: original_import_map.map(Arc::new),
- maybe_vendor_dir: None,
- bare_node_builtins_enabled: false,
- },
- )
+ CliGraphResolver::new(CliGraphResolverOptions {
+ fs: Arc::new(RealFs),
+ node_resolver: None,
+ npm_resolver: None,
+ cjs_resolutions: None,
+ package_json_deps_provider: Default::default(),
+ maybe_jsx_import_source_config,
+ maybe_import_map: original_import_map.map(Arc::new),
+ maybe_vendor_dir: None,
+ bare_node_builtins_enabled: false,
+ })
}
async fn build_test_graph(