deno/libs/resolver/factory.rs
David Sherret 63e4ee54c9
Some checks are pending
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build libs (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions
fix: support package.json imports in a Deno workspace (#30198)
2025-07-25 08:21:09 -04:00

1212 lines
39 KiB
Rust

// Copyright 2018-2025 the Deno authors. MIT license.
use std::borrow::Cow;
use std::path::Path;
use std::path::PathBuf;
use anyhow::bail;
use boxed_error::Boxed;
use deno_cache_dir::DenoDirResolutionError;
use deno_cache_dir::GlobalHttpCacheRc;
use deno_cache_dir::GlobalOrLocalHttpCache;
use deno_cache_dir::LocalHttpCache;
use deno_cache_dir::npm::NpmCacheDir;
use deno_config::deno_json::NodeModulesDirMode;
use deno_config::workspace::FolderConfigs;
use deno_config::workspace::VendorEnablement;
use deno_config::workspace::Workspace;
use deno_config::workspace::WorkspaceDirectory;
use deno_config::workspace::WorkspaceDirectoryEmptyOptions;
use deno_config::workspace::WorkspaceDiscoverError;
use deno_config::workspace::WorkspaceDiscoverOptions;
use deno_config::workspace::WorkspaceDiscoverStart;
pub use deno_npm::NpmSystemInfo;
use deno_path_util::fs::canonicalize_path_maybe_not_exists;
use futures::future::FutureExt;
use node_resolver::DenoIsBuiltInNodeModuleChecker;
use node_resolver::NodeResolver;
use node_resolver::NodeResolverOptions;
use node_resolver::NodeResolverRc;
use node_resolver::PackageJsonResolver;
use node_resolver::PackageJsonResolverRc;
use node_resolver::analyze::CjsModuleExportAnalyzerRc;
use node_resolver::analyze::NodeCodeTranslator;
use node_resolver::analyze::NodeCodeTranslatorRc;
use node_resolver::cache::NodeResolutionSys;
use thiserror::Error;
use url::Url;
use crate::DefaultRawDenoResolverRc;
use crate::DenoResolverOptions;
use crate::NodeAndNpmResolvers;
use crate::NpmCacheDirRc;
use crate::RawDenoResolver;
use crate::WorkspaceResolverRc;
use crate::cache::DenoDir;
use crate::cache::DenoDirOptions;
use crate::cache::DenoDirProvider;
use crate::cache::DenoDirProviderRc;
use crate::cache::DenoDirSys;
use crate::cache::EmitCache;
use crate::cache::EmitCacheRc;
use crate::cjs::CjsTracker;
use crate::cjs::CjsTrackerRc;
use crate::cjs::IsCjsResolutionMode;
use crate::cjs::analyzer::DenoCjsCodeAnalyzer;
use crate::cjs::analyzer::NodeAnalysisCacheRc;
use crate::cjs::analyzer::NullNodeAnalysisCache;
use crate::collections::FolderScopedWithUnscopedMap;
use crate::deno_json::CompilerOptionsOverrides;
use crate::deno_json::CompilerOptionsResolver;
use crate::deno_json::CompilerOptionsResolverRc;
use crate::import_map::WorkspaceExternalImportMapLoader;
use crate::import_map::WorkspaceExternalImportMapLoaderRc;
use crate::loader::DenoNpmModuleLoaderRc;
use crate::loader::NpmModuleLoader;
use crate::lockfile::LockfileLock;
use crate::lockfile::LockfileLockRc;
use crate::npm::ByonmNpmResolverCreateOptions;
use crate::npm::CreateInNpmPkgCheckerOptions;
use crate::npm::DenoInNpmPackageChecker;
use crate::npm::NpmReqResolver;
use crate::npm::NpmReqResolverOptions;
use crate::npm::NpmReqResolverRc;
use crate::npm::NpmResolver;
use crate::npm::NpmResolverCreateOptions;
use crate::npm::managed::ManagedInNpmPkgCheckerCreateOptions;
use crate::npm::managed::ManagedNpmResolverCreateOptions;
use crate::npm::managed::NpmResolutionCellRc;
use crate::npmrc::NpmRcDiscoverError;
use crate::npmrc::ResolvedNpmRcRc;
use crate::npmrc::discover_npmrc_from_workspace;
use crate::sync::MaybeSend;
use crate::sync::MaybeSync;
use crate::sync::new_rc;
use crate::workspace::FsCacheOptions;
use crate::workspace::PackageJsonDepResolution;
use crate::workspace::SloppyImportsOptions;
use crate::workspace::WorkspaceNpmLinkPackages;
use crate::workspace::WorkspaceNpmLinkPackagesRc;
use crate::workspace::WorkspaceResolver;
// todo(https://github.com/rust-lang/rust/issues/109737): remove once_cell after get_or_try_init is stabilized
#[cfg(feature = "sync")]
type Deferred<T> = once_cell::sync::OnceCell<T>;
#[cfg(not(feature = "sync"))]
type Deferred<T> = once_cell::unsync::OnceCell<T>;
#[allow(clippy::disallowed_types)]
type UrlRc = crate::sync::MaybeArc<Url>;
#[allow(clippy::disallowed_types)]
pub type WorkspaceDirectoryRc = crate::sync::MaybeArc<WorkspaceDirectory>;
#[allow(clippy::disallowed_types)]
pub type WorkspaceRc = crate::sync::MaybeArc<Workspace>;
pub type DenoCjsModuleExportAnalyzerRc<TSys> = CjsModuleExportAnalyzerRc<
DenoCjsCodeAnalyzer<TSys>,
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
NpmResolver<TSys>,
TSys,
>;
pub type DenoNodeCodeTranslatorRc<TSys> = NodeCodeTranslatorRc<
DenoCjsCodeAnalyzer<TSys>,
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
NpmResolver<TSys>,
TSys,
>;
#[derive(Debug, Boxed)]
pub struct HttpCacheCreateError(pub Box<HttpCacheCreateErrorKind>);
#[derive(Debug, Error)]
pub enum HttpCacheCreateErrorKind {
#[error(transparent)]
DenoDirResolution(#[from] DenoDirResolutionError),
#[error(transparent)]
WorkspaceDiscover(#[from] WorkspaceDiscoverError),
}
#[derive(Debug, Boxed)]
pub struct NpmCacheDirCreateError(pub Box<NpmCacheDirCreateErrorKind>);
#[derive(Debug, Error)]
pub enum NpmCacheDirCreateErrorKind {
#[error(transparent)]
DenoDirResolution(#[from] DenoDirResolutionError),
#[error(transparent)]
NpmRcCreate(#[from] NpmRcCreateError),
}
#[derive(Debug, Boxed)]
pub struct NpmRcCreateError(pub Box<NpmRcCreateErrorKind>);
#[derive(Debug, Error)]
pub enum NpmRcCreateErrorKind {
#[error(transparent)]
WorkspaceDiscover(#[from] WorkspaceDiscoverError),
#[error(transparent)]
NpmRcDiscover(#[from] NpmRcDiscoverError),
}
#[derive(Debug, Default)]
pub enum ConfigDiscoveryOption {
#[default]
DiscoverCwd,
Discover {
start_paths: Vec<PathBuf>,
},
Path(PathBuf),
Disabled,
}
/// Resolves the JSR regsitry URL to use for the given system.
pub fn resolve_jsr_url(sys: &impl sys_traits::EnvVar) -> Url {
let env_var_name = "JSR_URL";
if let Ok(registry_url) = sys.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://jsr.io/").unwrap()
}
#[async_trait::async_trait(?Send)]
pub trait SpecifiedImportMapProvider:
std::fmt::Debug + MaybeSend + MaybeSync
{
async fn get(
&self,
) -> Result<Option<crate::workspace::SpecifiedImportMap>, anyhow::Error>;
}
#[derive(Debug)]
pub struct NpmProcessStateOptions {
pub node_modules_dir: Option<Cow<'static, str>>,
pub is_byonm: bool,
}
#[derive(Debug, Default)]
pub struct WorkspaceFactoryOptions {
pub additional_config_file_names: &'static [&'static str],
pub config_discovery: ConfigDiscoveryOption,
pub is_package_manager_subcommand: bool,
pub frozen_lockfile: Option<bool>,
pub lock_arg: Option<PathBuf>,
/// Whether to skip writing to the lockfile.
pub lockfile_skip_write: bool,
pub maybe_custom_deno_dir_root: Option<PathBuf>,
pub node_modules_dir: Option<NodeModulesDirMode>,
pub no_lock: bool,
pub no_npm: bool,
/// The process sate if using ext/node and the current process was "forked".
/// This value is found at `deno_lib::args::NPM_PROCESS_STATE`
/// but in most scenarios this can probably just be `None`.
pub npm_process_state: Option<NpmProcessStateOptions>,
pub vendor: Option<bool>,
}
#[allow(clippy::disallowed_types)]
pub type WorkspaceFactoryRc<TSys> =
crate::sync::MaybeArc<WorkspaceFactory<TSys>>;
#[sys_traits::auto_impl]
pub trait WorkspaceFactorySys:
DenoDirSys
+ crate::lockfile::LockfileSys
+ crate::npm::NpmResolverSys
+ deno_cache_dir::GlobalHttpCacheSys
+ deno_cache_dir::LocalHttpCacheSys
+ crate::loader::NpmModuleLoaderSys
{
}
pub struct WorkspaceFactory<TSys: WorkspaceFactorySys> {
sys: TSys,
deno_dir_provider: Deferred<DenoDirProviderRc<TSys>>,
emit_cache: Deferred<EmitCacheRc<TSys>>,
global_http_cache: Deferred<GlobalHttpCacheRc<TSys>>,
http_cache: Deferred<GlobalOrLocalHttpCache<TSys>>,
jsr_url: Deferred<Url>,
lockfile: async_once_cell::OnceCell<Option<LockfileLockRc<TSys>>>,
node_modules_dir_path: Deferred<Option<PathBuf>>,
npm_cache_dir: Deferred<NpmCacheDirRc>,
npmrc: Deferred<(ResolvedNpmRcRc, Option<PathBuf>)>,
node_modules_dir_mode: Deferred<NodeModulesDirMode>,
workspace_directory: Deferred<WorkspaceDirectoryRc>,
workspace_directory_provider: Deferred<WorkspaceDirectoryProviderRc>,
workspace_external_import_map_loader:
Deferred<WorkspaceExternalImportMapLoaderRc<TSys>>,
workspace_npm_link_packages: Deferred<WorkspaceNpmLinkPackagesRc>,
initial_cwd: PathBuf,
options: WorkspaceFactoryOptions,
}
impl<TSys: WorkspaceFactorySys> WorkspaceFactory<TSys> {
pub fn new(
sys: TSys,
initial_cwd: PathBuf,
options: WorkspaceFactoryOptions,
) -> Self {
Self {
sys,
deno_dir_provider: Default::default(),
emit_cache: Default::default(),
global_http_cache: Default::default(),
http_cache: Default::default(),
jsr_url: Default::default(),
lockfile: Default::default(),
node_modules_dir_path: Default::default(),
npm_cache_dir: Default::default(),
npmrc: Default::default(),
node_modules_dir_mode: Default::default(),
workspace_directory: Default::default(),
workspace_directory_provider: Default::default(),
workspace_external_import_map_loader: Default::default(),
workspace_npm_link_packages: Default::default(),
initial_cwd,
options,
}
}
pub fn set_workspace_directory(
&mut self,
workspace_directory: WorkspaceDirectoryRc,
) {
self.workspace_directory = Deferred::from(workspace_directory);
}
pub fn deno_dir_provider(&self) -> &DenoDirProviderRc<TSys> {
self.deno_dir_provider.get_or_init(|| {
new_rc(DenoDirProvider::new(
self.sys.clone(),
DenoDirOptions {
maybe_custom_root: self.options.maybe_custom_deno_dir_root.clone(),
},
))
})
}
pub fn emit_cache(&self) -> Result<&EmitCacheRc<TSys>, anyhow::Error> {
self.emit_cache.get_or_try_init(|| {
Ok(new_rc(EmitCache::new(
&self.sys,
self.deno_dir()?.gen_cache.clone(),
#[cfg(feature = "deno_ast")]
Cow::Borrowed(deno_ast::VERSION),
#[cfg(not(feature = "deno_ast"))]
Cow::Borrowed(env!("CARGO_PKG_VERSION")),
)))
})
}
pub fn jsr_url(&self) -> &Url {
self.jsr_url.get_or_init(|| resolve_jsr_url(&self.sys))
}
pub fn initial_cwd(&self) -> &PathBuf {
&self.initial_cwd
}
pub fn no_npm(&self) -> bool {
self.options.no_npm
}
pub fn node_modules_dir_mode(
&self,
) -> Result<NodeModulesDirMode, anyhow::Error> {
self
.node_modules_dir_mode
.get_or_try_init(|| {
let raw_resolve = || -> Result<_, anyhow::Error> {
if let Some(process_state) = &self.options.npm_process_state {
if process_state.is_byonm {
return Ok(NodeModulesDirMode::Manual);
}
if process_state.node_modules_dir.is_some() {
return Ok(NodeModulesDirMode::Auto);
} else {
return Ok(NodeModulesDirMode::None);
}
}
if let Some(flag) = self.options.node_modules_dir {
return Ok(flag);
}
let workspace = &self.workspace_directory()?.workspace;
if let Some(mode) = workspace.node_modules_dir()? {
return Ok(mode);
}
let workspace = &self.workspace_directory()?.workspace;
if let Some(pkg_json) = workspace.root_pkg_json() {
if let Ok(deno_dir) = self.deno_dir() {
let deno_dir = &deno_dir.root;
// `deno_dir` can be symlink in macOS or on the CI
if let Ok(deno_dir) =
canonicalize_path_maybe_not_exists(&self.sys, deno_dir)
{
if pkg_json.path.starts_with(deno_dir) {
// if the package.json is in deno_dir, then do not use node_modules
// next to it as local node_modules dir
return Ok(NodeModulesDirMode::None);
}
}
}
Ok(NodeModulesDirMode::Manual)
} else if workspace.vendor_dir_path().is_some() {
Ok(NodeModulesDirMode::Auto)
} else {
// use the global cache
Ok(NodeModulesDirMode::None)
}
};
let mode = raw_resolve()?;
if mode == NodeModulesDirMode::Manual
&& self.options.is_package_manager_subcommand
{
// force using the managed resolver for package management
// sub commands so that it sets up the node_modules directory
Ok(NodeModulesDirMode::Auto)
} else {
Ok(mode)
}
})
.copied()
}
/// Resolves the path to use for a local node_modules folder.
pub fn node_modules_dir_path(&self) -> Result<Option<&Path>, anyhow::Error> {
fn resolve_from_root(root_folder: &FolderConfigs, cwd: &Path) -> PathBuf {
root_folder
.deno_json
.as_ref()
.map(|c| Cow::Owned(c.dir_path()))
.or_else(|| {
root_folder
.pkg_json
.as_ref()
.map(|c| Cow::Borrowed(c.dir_path()))
})
.unwrap_or(Cow::Borrowed(cwd))
.join("node_modules")
}
self
.node_modules_dir_path
.get_or_try_init(|| {
if let Some(process_state) = &self.options.npm_process_state {
return Ok(
process_state
.node_modules_dir
.as_ref()
.map(|p| PathBuf::from(p.as_ref())),
);
}
let mode = self.node_modules_dir_mode()?;
let workspace = &self.workspace_directory()?.workspace;
let root_folder = workspace.root_folder_configs();
if !mode.uses_node_modules_dir() {
return Ok(None);
}
let node_modules_dir =
resolve_from_root(root_folder, &self.initial_cwd);
Ok(Some(canonicalize_path_maybe_not_exists(
&self.sys,
&node_modules_dir,
)?))
})
.map(|p| p.as_deref())
}
pub fn deno_dir(&self) -> Result<&DenoDir<TSys>, DenoDirResolutionError> {
self.deno_dir_provider().get_or_create()
}
pub fn global_http_cache(
&self,
) -> Result<&GlobalHttpCacheRc<TSys>, DenoDirResolutionError> {
self.global_http_cache.get_or_try_init(|| {
let global_cache_dir = self.deno_dir()?.remote_folder_path();
let global_http_cache = new_rc(deno_cache_dir::GlobalHttpCache::new(
self.sys.clone(),
global_cache_dir,
));
Ok(global_http_cache)
})
}
pub fn http_cache(
&self,
) -> Result<&deno_cache_dir::GlobalOrLocalHttpCache<TSys>, HttpCacheCreateError>
{
self.http_cache.get_or_try_init(|| {
let global_cache = self.global_http_cache()?.clone();
match self.workspace_directory()?.workspace.vendor_dir_path() {
Some(local_path) => {
let local_cache = LocalHttpCache::new(
local_path.clone(),
global_cache,
deno_cache_dir::GlobalToLocalCopy::Allow,
self.jsr_url().clone(),
);
Ok(new_rc(local_cache).into())
}
None => Ok(global_cache.into()),
}
})
}
pub async fn maybe_lockfile(
&self,
npm_package_info_provider: &dyn deno_lockfile::NpmPackageInfoProvider,
) -> Result<Option<&LockfileLockRc<TSys>>, anyhow::Error> {
self
.lockfile
.get_or_try_init(async move {
let workspace_directory = self.workspace_directory()?;
let maybe_external_import_map =
self.workspace_external_import_map_loader()?.get_or_load()?;
let maybe_lock_file = LockfileLock::discover(
self.sys().clone(),
crate::lockfile::LockfileFlags {
no_lock: self.options.no_lock,
frozen_lockfile: self.options.frozen_lockfile,
lock: self.options.lock_arg.as_ref().map(|path| {
#[cfg(not(target_arch = "wasm32"))]
debug_assert!(path.is_absolute());
path.clone()
}),
skip_write: self.options.lockfile_skip_write,
no_config: matches!(
self.options.config_discovery,
ConfigDiscoveryOption::Disabled
),
no_npm: self.options.no_npm,
},
&workspace_directory.workspace,
maybe_external_import_map.as_ref().map(|v| &v.value),
npm_package_info_provider,
)
.await?
.map(crate::sync::new_rc);
Ok(maybe_lock_file)
})
.await
.map(|c| c.as_ref())
}
pub fn npm_cache_dir(
&self,
) -> Result<&NpmCacheDirRc, NpmCacheDirCreateError> {
self.npm_cache_dir.get_or_try_init(|| {
let npm_cache_dir = self.deno_dir()?.npm_folder_path();
Ok(new_rc(NpmCacheDir::new(
&self.sys,
npm_cache_dir,
self.npmrc()?.get_all_known_registries_urls(),
)))
})
}
pub fn npmrc(&self) -> Result<&ResolvedNpmRcRc, NpmRcCreateError> {
self.npmrc_with_path().map(|(npmrc, _)| npmrc)
}
pub fn npmrc_with_path(
&self,
) -> Result<&(ResolvedNpmRcRc, Option<PathBuf>), NpmRcCreateError> {
self.npmrc.get_or_try_init(|| {
let (npmrc, path) = discover_npmrc_from_workspace(
&self.sys,
&self.workspace_directory()?.workspace,
)?;
Ok((new_rc(npmrc), path))
})
}
pub fn sys(&self) -> &TSys {
&self.sys
}
pub fn workspace_directory(
&self,
) -> Result<&WorkspaceDirectoryRc, WorkspaceDiscoverError> {
self.workspace_directory.get_or_try_init(|| {
let maybe_vendor_override = self.options.vendor.map(|v| match v {
true => VendorEnablement::Enable {
cwd: &self.initial_cwd,
},
false => VendorEnablement::Disable,
});
let resolve_workspace_discover_options = || {
let discover_pkg_json = !self.options.no_npm
&& !self.has_flag_env_var("DENO_NO_PACKAGE_JSON");
if !discover_pkg_json {
log::debug!("package.json auto-discovery is disabled");
}
WorkspaceDiscoverOptions {
deno_json_cache: None,
pkg_json_cache: Some(&node_resolver::PackageJsonThreadLocalCache),
workspace_cache: None,
additional_config_file_names: self
.options
.additional_config_file_names,
discover_pkg_json,
maybe_vendor_override,
}
};
let resolve_empty_options = || WorkspaceDirectoryEmptyOptions {
root_dir: new_rc(
deno_path_util::url_from_directory_path(&self.initial_cwd).unwrap(),
),
use_vendor_dir: maybe_vendor_override
.unwrap_or(VendorEnablement::Disable),
};
let dir = match &self.options.config_discovery {
ConfigDiscoveryOption::DiscoverCwd => WorkspaceDirectory::discover(
&self.sys,
WorkspaceDiscoverStart::Paths(&[self.initial_cwd.clone()]),
&resolve_workspace_discover_options(),
)?,
ConfigDiscoveryOption::Discover { start_paths } => {
WorkspaceDirectory::discover(
&self.sys,
WorkspaceDiscoverStart::Paths(start_paths),
&resolve_workspace_discover_options(),
)?
}
ConfigDiscoveryOption::Path(path) => {
#[cfg(not(target_arch = "wasm32"))]
debug_assert!(path.is_absolute());
WorkspaceDirectory::discover(
&self.sys,
WorkspaceDiscoverStart::ConfigFile(path),
&resolve_workspace_discover_options(),
)?
}
ConfigDiscoveryOption::Disabled => {
WorkspaceDirectory::empty(resolve_empty_options())
}
};
Ok(new_rc(dir))
})
}
pub fn workspace_directory_provider(
&self,
) -> Result<&WorkspaceDirectoryProviderRc, WorkspaceDiscoverError> {
self.workspace_directory_provider.get_or_try_init(|| {
Ok(new_rc(WorkspaceDirectoryProvider::from_initial_dir(
self.workspace_directory()?,
)))
})
}
pub fn workspace_external_import_map_loader(
&self,
) -> Result<&WorkspaceExternalImportMapLoaderRc<TSys>, WorkspaceDiscoverError>
{
self
.workspace_external_import_map_loader
.get_or_try_init(|| {
Ok(new_rc(WorkspaceExternalImportMapLoader::new(
self.sys().clone(),
self.workspace_directory()?.workspace.clone(),
)))
})
}
pub fn workspace_npm_link_packages(
&self,
) -> Result<&WorkspaceNpmLinkPackagesRc, anyhow::Error> {
self
.workspace_npm_link_packages
.get_or_try_init(|| {
let workspace_dir = self.workspace_directory()?;
let npm_packages = new_rc(WorkspaceNpmLinkPackages::from_workspace(
workspace_dir.workspace.as_ref(),
));
if !npm_packages.0.is_empty() && !matches!(self.node_modules_dir_mode()?, NodeModulesDirMode::Auto | NodeModulesDirMode::Manual) {
bail!("Linking npm packages requires using a node_modules directory. Ensure you have a package.json or set the \"nodeModulesDir\" option to \"auto\" or \"manual\" in your workspace root deno.json.")
} else {
Ok(npm_packages)
}
})
}
fn has_flag_env_var(&self, name: &str) -> bool {
let value = self.sys.env_var_os(name);
match value {
Some(value) => value == "1",
None => false,
}
}
}
#[derive(Default)]
pub struct ResolverFactoryOptions {
pub compiler_options_overrides: CompilerOptionsOverrides,
pub is_cjs_resolution_mode: IsCjsResolutionMode,
pub node_analysis_cache: Option<NodeAnalysisCacheRc>,
pub node_code_translator_mode: node_resolver::analyze::NodeCodeTranslatorMode,
pub node_resolver_options: NodeResolverOptions,
pub node_resolution_cache: Option<node_resolver::NodeResolutionCacheRc>,
pub npm_system_info: NpmSystemInfo,
pub package_json_cache: Option<node_resolver::PackageJsonCacheRc>,
pub package_json_dep_resolution: Option<PackageJsonDepResolution>,
pub specified_import_map: Option<Box<dyn SpecifiedImportMapProvider>>,
/// Whether to resolve bare node builtins (ex. "path" as "node:path").
pub bare_node_builtins: bool,
pub unstable_sloppy_imports: bool,
#[cfg(feature = "graph")]
pub on_mapped_resolution_diagnostic:
Option<crate::graph::OnMappedResolutionDiagnosticFn>,
}
pub struct ResolverFactory<TSys: WorkspaceFactorySys> {
options: ResolverFactoryOptions,
sys: NodeResolutionSys<TSys>,
cjs_module_export_analyzer: Deferred<DenoCjsModuleExportAnalyzerRc<TSys>>,
cjs_tracker: Deferred<CjsTrackerRc<DenoInNpmPackageChecker, TSys>>,
compiler_options_resolver: Deferred<CompilerOptionsResolverRc>,
#[cfg(feature = "graph")]
deno_resolver:
async_once_cell::OnceCell<crate::graph::DefaultDenoResolverRc<TSys>>,
#[cfg(feature = "deno_ast")]
emitter: Deferred<crate::emit::EmitterRc<DenoInNpmPackageChecker, TSys>>,
#[cfg(feature = "graph")]
found_package_json_dep_flag: crate::graph::FoundPackageJsonDepFlagRc,
in_npm_package_checker: Deferred<DenoInNpmPackageChecker>,
node_code_translator: Deferred<DenoNodeCodeTranslatorRc<TSys>>,
npm_module_loader: Deferred<DenoNpmModuleLoaderRc<TSys>>,
node_resolver: Deferred<
NodeResolverRc<
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
NpmResolver<TSys>,
TSys,
>,
>,
npm_req_resolver: Deferred<
NpmReqResolverRc<
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
NpmResolver<TSys>,
TSys,
>,
>,
npm_resolver: Deferred<NpmResolver<TSys>>,
npm_resolution: NpmResolutionCellRc,
#[cfg(feature = "deno_ast")]
parsed_source_cache: crate::cache::ParsedSourceCacheRc,
pkg_json_resolver: Deferred<PackageJsonResolverRc<TSys>>,
#[cfg(all(feature = "graph", feature = "deno_ast"))]
module_loader: Deferred<crate::loader::ModuleLoaderRc<TSys>>,
raw_deno_resolver: async_once_cell::OnceCell<DefaultRawDenoResolverRc<TSys>>,
workspace_factory: WorkspaceFactoryRc<TSys>,
workspace_resolver: async_once_cell::OnceCell<WorkspaceResolverRc<TSys>>,
}
impl<TSys: WorkspaceFactorySys> ResolverFactory<TSys> {
pub fn new(
workspace_factory: WorkspaceFactoryRc<TSys>,
options: ResolverFactoryOptions,
) -> Self {
Self {
sys: NodeResolutionSys::new(
workspace_factory.sys.clone(),
options.node_resolution_cache.clone(),
),
cjs_module_export_analyzer: Default::default(),
cjs_tracker: Default::default(),
compiler_options_resolver: Default::default(),
raw_deno_resolver: Default::default(),
#[cfg(feature = "graph")]
deno_resolver: Default::default(),
#[cfg(feature = "deno_ast")]
emitter: Default::default(),
#[cfg(feature = "graph")]
found_package_json_dep_flag: Default::default(),
in_npm_package_checker: Default::default(),
node_code_translator: Default::default(),
node_resolver: Default::default(),
npm_module_loader: Default::default(),
npm_req_resolver: Default::default(),
npm_resolution: Default::default(),
npm_resolver: Default::default(),
#[cfg(feature = "deno_ast")]
parsed_source_cache: Default::default(),
pkg_json_resolver: Default::default(),
#[cfg(all(feature = "graph", feature = "deno_ast"))]
module_loader: Default::default(),
workspace_factory,
workspace_resolver: Default::default(),
options,
}
}
pub async fn raw_deno_resolver(
&self,
) -> Result<&DefaultRawDenoResolverRc<TSys>, anyhow::Error> {
self
.raw_deno_resolver
.get_or_try_init(
async {
Ok(new_rc(RawDenoResolver::new(DenoResolverOptions {
in_npm_pkg_checker: self.in_npm_package_checker()?.clone(),
node_and_req_resolver: if self.workspace_factory.no_npm() {
None
} else {
Some(NodeAndNpmResolvers {
node_resolver: self.node_resolver()?.clone(),
npm_resolver: self.npm_resolver()?.clone(),
npm_req_resolver: self.npm_req_resolver()?.clone(),
})
},
bare_node_builtins: self.bare_node_builtins()?,
is_byonm: self.use_byonm()?,
maybe_vendor_dir: self
.workspace_factory
.workspace_directory()?
.workspace
.vendor_dir_path(),
workspace_resolver: self.workspace_resolver().await?.clone(),
})))
}
// boxed to prevent the futures getting big and exploding the stack
.boxed_local(),
)
.await
}
pub fn cjs_module_export_analyzer(
&self,
) -> Result<&DenoCjsModuleExportAnalyzerRc<TSys>, anyhow::Error> {
self.cjs_module_export_analyzer.get_or_try_init(|| {
let code_analyzer = DenoCjsCodeAnalyzer::new(
self
.options
.node_analysis_cache
.clone()
.unwrap_or_else(|| new_rc(NullNodeAnalysisCache)),
self.cjs_tracker()?.clone(),
#[cfg(feature = "deno_ast")]
new_rc(crate::cjs::analyzer::DenoAstModuleExportAnalyzer::new(
self.parsed_source_cache().clone(),
)),
#[cfg(not(feature = "deno_ast"))]
new_rc(crate::cjs::analyzer::NotImplementedModuleExportAnalyzer),
self.workspace_factory.sys().clone(),
);
Ok(new_rc(
node_resolver::analyze::CjsModuleExportAnalyzer::new(
code_analyzer,
self.in_npm_package_checker()?.clone(),
self.node_resolver()?.clone(),
self.npm_resolver()?.clone(),
self.pkg_json_resolver().clone(),
self.workspace_factory.sys().clone(),
),
))
})
}
pub fn cjs_tracker(
&self,
) -> Result<&CjsTrackerRc<DenoInNpmPackageChecker, TSys>, anyhow::Error> {
self.cjs_tracker.get_or_try_init(|| {
Ok(new_rc(CjsTracker::new(
self.in_npm_package_checker()?.clone(),
self.pkg_json_resolver().clone(),
self.options.is_cjs_resolution_mode,
)))
})
}
pub fn compiler_options_resolver(
&self,
) -> Result<&CompilerOptionsResolverRc, anyhow::Error> {
self.compiler_options_resolver.get_or_try_init(|| {
Ok(new_rc(CompilerOptionsResolver::new(
&self.sys,
self.workspace_factory.workspace_directory_provider()?,
self.node_resolver()?,
&self.workspace_factory.options.config_discovery,
&self.options.compiler_options_overrides,
)))
})
}
#[cfg(feature = "deno_ast")]
pub fn emitter(
&self,
) -> Result<
&crate::emit::EmitterRc<DenoInNpmPackageChecker, TSys>,
anyhow::Error,
> {
self.emitter.get_or_try_init(|| {
Ok(new_rc(crate::emit::Emitter::new(
self.cjs_tracker()?.clone(),
self.workspace_factory.emit_cache()?.clone(),
self.parsed_source_cache().clone(),
self.compiler_options_resolver()?.clone(),
)))
})
}
#[cfg(feature = "graph")]
pub fn found_package_json_dep_flag(
&self,
) -> &crate::graph::FoundPackageJsonDepFlagRc {
&self.found_package_json_dep_flag
}
#[cfg(feature = "graph")]
pub async fn deno_resolver(
&self,
) -> Result<&crate::graph::DefaultDenoResolverRc<TSys>, anyhow::Error> {
self
.deno_resolver
.get_or_try_init(async {
Ok(new_rc(crate::graph::DenoResolver::new(
self.raw_deno_resolver().await?.clone(),
self.workspace_factory.sys.clone(),
self.found_package_json_dep_flag.clone(),
self.options.on_mapped_resolution_diagnostic.clone(),
)))
})
.await
}
pub fn in_npm_package_checker(
&self,
) -> Result<&DenoInNpmPackageChecker, anyhow::Error> {
self.in_npm_package_checker.get_or_try_init(|| {
let options = match self.use_byonm()? {
true => CreateInNpmPkgCheckerOptions::Byonm,
false => CreateInNpmPkgCheckerOptions::Managed(
ManagedInNpmPkgCheckerCreateOptions {
root_cache_dir_url: self
.workspace_factory
.npm_cache_dir()?
.root_dir_url(),
maybe_node_modules_path: self
.workspace_factory
.node_modules_dir_path()?,
},
),
};
Ok(DenoInNpmPackageChecker::new(options))
})
}
pub fn node_code_translator(
&self,
) -> Result<&DenoNodeCodeTranslatorRc<TSys>, anyhow::Error> {
self.node_code_translator.get_or_try_init(|| {
Ok(new_rc(NodeCodeTranslator::new(
self.cjs_module_export_analyzer()?.clone(),
self.options.node_code_translator_mode,
)))
})
}
pub fn node_resolver(
&self,
) -> Result<
&NodeResolverRc<
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
NpmResolver<TSys>,
TSys,
>,
anyhow::Error,
> {
self.node_resolver.get_or_try_init(|| {
Ok(new_rc(NodeResolver::new(
self.in_npm_package_checker()?.clone(),
DenoIsBuiltInNodeModuleChecker,
self.npm_resolver()?.clone(),
self.pkg_json_resolver().clone(),
self.sys.clone(),
self.options.node_resolver_options.clone(),
)))
})
}
pub fn npm_module_loader(
&self,
) -> Result<&DenoNpmModuleLoaderRc<TSys>, anyhow::Error> {
self.npm_module_loader.get_or_try_init(|| {
Ok(new_rc(NpmModuleLoader::new(
self.cjs_tracker()?.clone(),
self.node_code_translator()?.clone(),
self.workspace_factory.sys.clone(),
)))
})
}
pub fn npm_resolution(&self) -> &NpmResolutionCellRc {
&self.npm_resolution
}
pub fn npm_req_resolver(
&self,
) -> Result<
&NpmReqResolverRc<
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
NpmResolver<TSys>,
TSys,
>,
anyhow::Error,
> {
self.npm_req_resolver.get_or_try_init(|| {
Ok(new_rc(NpmReqResolver::new(NpmReqResolverOptions {
in_npm_pkg_checker: self.in_npm_package_checker()?.clone(),
node_resolver: self.node_resolver()?.clone(),
npm_resolver: self.npm_resolver()?.clone(),
sys: self.workspace_factory.sys.clone(),
})))
})
}
pub fn npm_resolver(&self) -> Result<&NpmResolver<TSys>, anyhow::Error> {
self.npm_resolver.get_or_try_init(|| {
Ok(NpmResolver::<TSys>::new::<TSys>(if self.use_byonm()? {
NpmResolverCreateOptions::Byonm(ByonmNpmResolverCreateOptions {
sys: self.sys.clone(),
pkg_json_resolver: self.pkg_json_resolver().clone(),
root_node_modules_dir: Some(
match self.workspace_factory.node_modules_dir_path()? {
Some(node_modules_path) => node_modules_path.to_path_buf(),
// path needs to be canonicalized for node resolution
// (node_modules_dir_path above is already canonicalized)
None => canonicalize_path_maybe_not_exists(
&self.workspace_factory.sys,
self.workspace_factory.initial_cwd(),
)?
.join("node_modules"),
},
),
})
} else {
NpmResolverCreateOptions::Managed(ManagedNpmResolverCreateOptions {
sys: self.workspace_factory.sys.clone(),
npm_resolution: self.npm_resolution().clone(),
npm_cache_dir: self.workspace_factory.npm_cache_dir()?.clone(),
maybe_node_modules_path: self
.workspace_factory
.node_modules_dir_path()?
.map(|p| p.to_path_buf()),
npm_system_info: self.options.npm_system_info.clone(),
npmrc: self.workspace_factory.npmrc()?.clone(),
})
}))
})
}
#[cfg(feature = "deno_ast")]
pub fn parsed_source_cache(&self) -> &crate::cache::ParsedSourceCacheRc {
&self.parsed_source_cache
}
pub fn pkg_json_resolver(&self) -> &PackageJsonResolverRc<TSys> {
self.pkg_json_resolver.get_or_init(|| {
new_rc(PackageJsonResolver::new(
self.workspace_factory.sys.clone(),
self.options.package_json_cache.clone(),
))
})
}
#[cfg(all(feature = "graph", feature = "deno_ast"))]
pub fn module_loader(
&self,
) -> Result<&crate::loader::ModuleLoaderRc<TSys>, anyhow::Error> {
self.module_loader.get_or_try_init(|| {
let cjs_tracker = self.cjs_tracker()?;
Ok(new_rc(crate::loader::ModuleLoader::new(
cjs_tracker.clone(),
self.emitter()?.clone(),
self.in_npm_package_checker()?.clone(),
self.node_code_translator()?.clone(),
self.npm_module_loader()?.clone(),
self.parsed_source_cache.clone(),
self.workspace_factory.sys.clone(),
)))
})
}
pub fn workspace_factory(&self) -> &WorkspaceFactoryRc<TSys> {
&self.workspace_factory
}
pub async fn workspace_resolver(
&self,
) -> Result<&WorkspaceResolverRc<TSys>, anyhow::Error> {
self
.workspace_resolver
.get_or_try_init(
async {
let directory = self.workspace_factory.workspace_directory()?;
let workspace = &directory.workspace;
let specified_import_map = match &self.options.specified_import_map {
Some(import_map) => import_map.get().await?,
None => None,
};
let options = crate::workspace::CreateResolverOptions {
pkg_json_dep_resolution: match self
.options
.package_json_dep_resolution
{
Some(value) => value,
None => {
match self.workspace_factory.node_modules_dir_mode()? {
NodeModulesDirMode::Manual => {
PackageJsonDepResolution::Disabled
}
NodeModulesDirMode::Auto | NodeModulesDirMode::None => {
// todo(dsherret): should this be disabled for auto?
PackageJsonDepResolution::Enabled
}
}
}
},
specified_import_map,
sloppy_imports_options: if self.options.unstable_sloppy_imports
|| workspace.has_unstable("sloppy-imports")
{
SloppyImportsOptions::Enabled
} else {
SloppyImportsOptions::Disabled
},
fs_cache_options: FsCacheOptions::Enabled,
};
let resolver = WorkspaceResolver::from_workspace(
workspace,
self.workspace_factory.sys.clone(),
options,
)?;
if !resolver.diagnostics().is_empty() {
// todo(dsherret): do not log this in this crate... that should be
// a CLI responsibility
log::warn!(
"Resolver diagnostics:\n{}",
resolver
.diagnostics()
.iter()
.map(|d| format!(" - {d}"))
.collect::<Vec<_>>()
.join("\n")
);
}
Ok(new_rc(resolver))
}
// boxed to prevent the futures getting big and exploding the stack
.boxed_local(),
)
.await
}
pub fn bare_node_builtins(&self) -> Result<bool, anyhow::Error> {
Ok(
self.options.bare_node_builtins
|| self
.workspace_factory
.workspace_directory()?
.workspace
.has_unstable("bare-node-builtins"),
)
}
pub fn npm_system_info(&self) -> &NpmSystemInfo {
&self.options.npm_system_info
}
pub fn use_byonm(&self) -> Result<bool, anyhow::Error> {
Ok(
self.workspace_factory.node_modules_dir_mode()?
== NodeModulesDirMode::Manual,
)
}
}
#[derive(Debug)]
pub struct WorkspaceDirectoryProvider {
pub workspace: WorkspaceRc,
dirs: FolderScopedWithUnscopedMap<Deferred<WorkspaceDirectoryRc>>,
}
impl WorkspaceDirectoryProvider {
pub fn from_initial_dir(dir: &WorkspaceDirectoryRc) -> Self {
let workspace = dir.workspace.clone();
let mut dirs = FolderScopedWithUnscopedMap::new(Deferred::default());
for dir_url in workspace.config_folders().keys() {
if dir_url == workspace.root_dir() {
continue;
} else if dir_url == dir.dir_url() {
dirs.insert(dir_url.clone(), Deferred::from(dir.clone()));
} else {
dirs.insert(dir_url.clone(), Deferred::default());
}
}
Self { workspace, dirs }
}
pub fn for_specifier(&self, specifier: &Url) -> &WorkspaceDirectoryRc {
let (dir_url, dir) = self.dirs.entry_for_specifier(specifier);
dir.get_or_init(|| {
new_rc(
self
.workspace
.resolve_member_dir(dir_url.unwrap_or(self.workspace.root_dir())),
)
})
}
pub fn root(&self) -> &WorkspaceDirectoryRc {
self.dirs.unscoped.get_or_init(|| {
new_rc(self.workspace.resolve_member_dir(self.workspace.root_dir()))
})
}
pub fn entries(
&self,
) -> impl Iterator<Item = (Option<&UrlRc>, &WorkspaceDirectoryRc)> {
self.dirs.entries().map(|(s, d)| {
(
s,
d.get_or_init(|| {
new_rc(
self
.workspace
.resolve_member_dir(s.unwrap_or(self.workspace.root_dir())),
)
}),
)
})
}
}
#[allow(clippy::disallowed_types)]
pub type WorkspaceDirectoryProviderRc =
crate::sync::MaybeArc<WorkspaceDirectoryProvider>;