mirror of
https://github.com/denoland/deno.git
synced 2024-12-21 06:44:45 -05:00
32e260d55a
Fixes https://github.com/denoland/deno/issues/26989 --------- Co-authored-by: Nathan Whitaker <nathan@deno.com>
1314 lines
38 KiB
Rust
1314 lines
38 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use crate::args::config_to_deno_graph_workspace_member;
|
|
use crate::args::jsr_url;
|
|
use crate::args::CliLockfile;
|
|
use crate::args::CliOptions;
|
|
use crate::args::DENO_DISABLE_PEDANTIC_NODE_WARNINGS;
|
|
use crate::cache;
|
|
use crate::cache::FetchCacher;
|
|
use crate::cache::GlobalHttpCache;
|
|
use crate::cache::ModuleInfoCache;
|
|
use crate::cache::ParsedSourceCache;
|
|
use crate::colors;
|
|
use crate::errors::get_error_class_name;
|
|
use crate::file_fetcher::FileFetcher;
|
|
use crate::npm::CliNpmResolver;
|
|
use crate::resolver::CjsTracker;
|
|
use crate::resolver::CliResolver;
|
|
use crate::resolver::CliSloppyImportsResolver;
|
|
use crate::resolver::SloppyImportsCachedFs;
|
|
use crate::tools::check;
|
|
use crate::tools::check::TypeChecker;
|
|
use crate::util::file_watcher::WatcherCommunicator;
|
|
use crate::util::fs::canonicalize_path;
|
|
use deno_config::deno_json::JsxImportSourceConfig;
|
|
use deno_config::workspace::JsrPackageConfig;
|
|
use deno_core::anyhow::bail;
|
|
use deno_graph::source::LoaderChecksum;
|
|
use deno_graph::source::ResolutionKind;
|
|
use deno_graph::FillFromLockfileOptions;
|
|
use deno_graph::JsrLoadError;
|
|
use deno_graph::ModuleLoadError;
|
|
use deno_graph::WorkspaceFastCheckOption;
|
|
|
|
use deno_core::error::custom_error;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::parking_lot::Mutex;
|
|
use deno_core::ModuleSpecifier;
|
|
use deno_graph::source::Loader;
|
|
use deno_graph::source::ResolveError;
|
|
use deno_graph::GraphKind;
|
|
use deno_graph::ModuleError;
|
|
use deno_graph::ModuleGraph;
|
|
use deno_graph::ModuleGraphError;
|
|
use deno_graph::ResolutionError;
|
|
use deno_graph::SpecifierError;
|
|
use deno_path_util::url_to_file_path;
|
|
use deno_resolver::sloppy_imports::SloppyImportsResolutionKind;
|
|
use deno_runtime::deno_fs::FileSystem;
|
|
use deno_runtime::deno_node;
|
|
use deno_runtime::deno_permissions::PermissionsContainer;
|
|
use deno_semver::jsr::JsrDepPackageReq;
|
|
use deno_semver::package::PackageNv;
|
|
use import_map::ImportMapError;
|
|
use node_resolver::InNpmPackageChecker;
|
|
use std::collections::HashSet;
|
|
use std::error::Error;
|
|
use std::ops::Deref;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Clone)]
|
|
pub struct GraphValidOptions {
|
|
pub check_js: bool,
|
|
pub kind: GraphKind,
|
|
/// Whether to exit the process for integrity check errors such as
|
|
/// lockfile checksum mismatches and JSR integrity failures.
|
|
/// Otherwise, surfaces integrity errors as errors.
|
|
pub exit_integrity_errors: bool,
|
|
}
|
|
|
|
/// Check if `roots` and their deps are available. Returns `Ok(())` if
|
|
/// so. Returns `Err(_)` if there is a known module graph or resolution
|
|
/// error statically reachable from `roots`.
|
|
///
|
|
/// It is preferable to use this over using deno_graph's API directly
|
|
/// because it will have enhanced error message information specifically
|
|
/// for the CLI.
|
|
pub fn graph_valid(
|
|
graph: &ModuleGraph,
|
|
fs: &Arc<dyn FileSystem>,
|
|
roots: &[ModuleSpecifier],
|
|
options: GraphValidOptions,
|
|
) -> Result<(), AnyError> {
|
|
if options.exit_integrity_errors {
|
|
graph_exit_integrity_errors(graph);
|
|
}
|
|
|
|
let mut errors = graph_walk_errors(
|
|
graph,
|
|
fs,
|
|
roots,
|
|
GraphWalkErrorsOptions {
|
|
check_js: options.check_js,
|
|
kind: options.kind,
|
|
},
|
|
);
|
|
if let Some(error) = errors.next() {
|
|
Err(error)
|
|
} else {
|
|
// finally surface the npm resolution result
|
|
if let Err(err) = &graph.npm_dep_graph_result {
|
|
return Err(custom_error(
|
|
get_error_class_name(err),
|
|
format_deno_graph_error(err.as_ref().deref()),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct GraphWalkErrorsOptions {
|
|
pub check_js: bool,
|
|
pub kind: GraphKind,
|
|
}
|
|
|
|
/// Walks the errors found in the module graph that should be surfaced to users
|
|
/// and enhances them with CLI information.
|
|
pub fn graph_walk_errors<'a>(
|
|
graph: &'a ModuleGraph,
|
|
fs: &'a Arc<dyn FileSystem>,
|
|
roots: &'a [ModuleSpecifier],
|
|
options: GraphWalkErrorsOptions,
|
|
) -> impl Iterator<Item = AnyError> + 'a {
|
|
graph
|
|
.walk(
|
|
roots.iter(),
|
|
deno_graph::WalkOptions {
|
|
check_js: options.check_js,
|
|
kind: options.kind,
|
|
follow_dynamic: false,
|
|
prefer_fast_check_graph: false,
|
|
},
|
|
)
|
|
.errors()
|
|
.flat_map(|error| {
|
|
let is_root = match &error {
|
|
ModuleGraphError::ResolutionError(_)
|
|
| ModuleGraphError::TypesResolutionError(_) => false,
|
|
ModuleGraphError::ModuleError(error) => {
|
|
roots.contains(error.specifier())
|
|
}
|
|
};
|
|
let mut message = match &error {
|
|
ModuleGraphError::ResolutionError(resolution_error) => {
|
|
enhanced_resolution_error_message(resolution_error)
|
|
}
|
|
ModuleGraphError::TypesResolutionError(resolution_error) => {
|
|
format!(
|
|
"Failed resolving types. {}",
|
|
enhanced_resolution_error_message(resolution_error)
|
|
)
|
|
}
|
|
ModuleGraphError::ModuleError(error) => {
|
|
enhanced_integrity_error_message(error)
|
|
.or_else(|| enhanced_sloppy_imports_error_message(fs, error))
|
|
.unwrap_or_else(|| format_deno_graph_error(error))
|
|
}
|
|
};
|
|
|
|
if let Some(range) = error.maybe_range() {
|
|
if !is_root && !range.specifier.as_str().contains("/$deno$eval") {
|
|
message.push_str("\n at ");
|
|
message.push_str(&format_range_with_colors(range));
|
|
}
|
|
}
|
|
|
|
if graph.graph_kind() == GraphKind::TypesOnly
|
|
&& matches!(
|
|
error,
|
|
ModuleGraphError::ModuleError(ModuleError::UnsupportedMediaType(..))
|
|
)
|
|
{
|
|
log::debug!("Ignoring: {}", message);
|
|
return None;
|
|
}
|
|
|
|
Some(custom_error(get_error_class_name(&error.into()), message))
|
|
})
|
|
}
|
|
|
|
pub fn graph_exit_integrity_errors(graph: &ModuleGraph) {
|
|
for error in graph.module_errors() {
|
|
exit_for_integrity_error(error);
|
|
}
|
|
}
|
|
|
|
fn exit_for_integrity_error(err: &ModuleError) {
|
|
if let Some(err_message) = enhanced_integrity_error_message(err) {
|
|
log::error!("{} {}", colors::red("error:"), err_message);
|
|
deno_runtime::exit(10);
|
|
}
|
|
}
|
|
|
|
pub struct CreateGraphOptions<'a> {
|
|
pub graph_kind: GraphKind,
|
|
pub roots: Vec<ModuleSpecifier>,
|
|
pub is_dynamic: bool,
|
|
/// Specify `None` to use the default CLI loader.
|
|
pub loader: Option<&'a mut dyn Loader>,
|
|
}
|
|
|
|
pub struct ModuleGraphCreator {
|
|
options: Arc<CliOptions>,
|
|
npm_resolver: Arc<dyn CliNpmResolver>,
|
|
module_graph_builder: Arc<ModuleGraphBuilder>,
|
|
type_checker: Arc<TypeChecker>,
|
|
}
|
|
|
|
impl ModuleGraphCreator {
|
|
pub fn new(
|
|
options: Arc<CliOptions>,
|
|
npm_resolver: Arc<dyn CliNpmResolver>,
|
|
module_graph_builder: Arc<ModuleGraphBuilder>,
|
|
type_checker: Arc<TypeChecker>,
|
|
) -> Self {
|
|
Self {
|
|
options,
|
|
npm_resolver,
|
|
module_graph_builder,
|
|
type_checker,
|
|
}
|
|
}
|
|
|
|
pub async fn create_graph(
|
|
&self,
|
|
graph_kind: GraphKind,
|
|
roots: Vec<ModuleSpecifier>,
|
|
) -> Result<deno_graph::ModuleGraph, AnyError> {
|
|
let mut cache = self.module_graph_builder.create_graph_loader();
|
|
self
|
|
.create_graph_with_loader(graph_kind, roots, &mut cache)
|
|
.await
|
|
}
|
|
|
|
pub async fn create_graph_with_loader(
|
|
&self,
|
|
graph_kind: GraphKind,
|
|
roots: Vec<ModuleSpecifier>,
|
|
loader: &mut dyn Loader,
|
|
) -> Result<ModuleGraph, AnyError> {
|
|
self
|
|
.create_graph_with_options(CreateGraphOptions {
|
|
is_dynamic: false,
|
|
graph_kind,
|
|
roots,
|
|
loader: Some(loader),
|
|
})
|
|
.await
|
|
}
|
|
|
|
pub async fn create_and_validate_publish_graph(
|
|
&self,
|
|
package_configs: &[JsrPackageConfig],
|
|
build_fast_check_graph: bool,
|
|
) -> Result<ModuleGraph, AnyError> {
|
|
struct PublishLoader(FetchCacher);
|
|
impl Loader for PublishLoader {
|
|
fn load(
|
|
&self,
|
|
specifier: &deno_ast::ModuleSpecifier,
|
|
options: deno_graph::source::LoadOptions,
|
|
) -> deno_graph::source::LoadFuture {
|
|
if specifier.scheme() == "bun" {
|
|
return Box::pin(std::future::ready(Ok(Some(
|
|
deno_graph::source::LoadResponse::External {
|
|
specifier: specifier.clone(),
|
|
},
|
|
))));
|
|
}
|
|
self.0.load(specifier, options)
|
|
}
|
|
}
|
|
fn graph_has_external_remote(graph: &ModuleGraph) -> bool {
|
|
// Earlier on, we marked external non-JSR modules as external.
|
|
// If the graph contains any of those, it would cause type checking
|
|
// to crash, so since publishing is going to fail anyway, skip type
|
|
// checking.
|
|
graph.modules().any(|module| match module {
|
|
deno_graph::Module::External(external_module) => {
|
|
matches!(external_module.specifier.scheme(), "http" | "https")
|
|
}
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
let mut roots = Vec::new();
|
|
for package_config in package_configs {
|
|
roots.extend(package_config.config_file.resolve_export_value_urls()?);
|
|
}
|
|
|
|
let loader = self.module_graph_builder.create_graph_loader();
|
|
let mut publish_loader = PublishLoader(loader);
|
|
let mut graph = self
|
|
.create_graph_with_options(CreateGraphOptions {
|
|
is_dynamic: false,
|
|
graph_kind: deno_graph::GraphKind::All,
|
|
roots,
|
|
loader: Some(&mut publish_loader),
|
|
})
|
|
.await?;
|
|
self.graph_valid(&graph)?;
|
|
if self.options.type_check_mode().is_true()
|
|
&& !graph_has_external_remote(&graph)
|
|
{
|
|
self.type_check_graph(graph.clone()).await?;
|
|
}
|
|
|
|
if build_fast_check_graph {
|
|
let fast_check_workspace_members = package_configs
|
|
.iter()
|
|
.map(|p| config_to_deno_graph_workspace_member(&p.config_file))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
self.module_graph_builder.build_fast_check_graph(
|
|
&mut graph,
|
|
BuildFastCheckGraphOptions {
|
|
workspace_fast_check: WorkspaceFastCheckOption::Enabled(
|
|
&fast_check_workspace_members,
|
|
),
|
|
},
|
|
)?;
|
|
}
|
|
|
|
Ok(graph)
|
|
}
|
|
|
|
pub async fn create_graph_with_options(
|
|
&self,
|
|
options: CreateGraphOptions<'_>,
|
|
) -> Result<ModuleGraph, AnyError> {
|
|
let mut graph = ModuleGraph::new(options.graph_kind);
|
|
|
|
self
|
|
.module_graph_builder
|
|
.build_graph_with_npm_resolution(&mut graph, options)
|
|
.await?;
|
|
|
|
if let Some(npm_resolver) = self.npm_resolver.as_managed() {
|
|
if graph.has_node_specifier && self.options.type_check_mode().is_true() {
|
|
npm_resolver.inject_synthetic_types_node_package().await?;
|
|
}
|
|
}
|
|
|
|
Ok(graph)
|
|
}
|
|
|
|
pub async fn create_graph_and_maybe_check(
|
|
&self,
|
|
roots: Vec<ModuleSpecifier>,
|
|
) -> Result<Arc<deno_graph::ModuleGraph>, AnyError> {
|
|
let graph_kind = self.options.type_check_mode().as_graph_kind();
|
|
|
|
let graph = self
|
|
.create_graph_with_options(CreateGraphOptions {
|
|
is_dynamic: false,
|
|
graph_kind,
|
|
roots,
|
|
loader: None,
|
|
})
|
|
.await?;
|
|
|
|
self.graph_valid(&graph)?;
|
|
|
|
if self.options.type_check_mode().is_true() {
|
|
// provide the graph to the type checker, then get it back after it's done
|
|
let graph = self.type_check_graph(graph).await?;
|
|
Ok(graph)
|
|
} else {
|
|
Ok(Arc::new(graph))
|
|
}
|
|
}
|
|
|
|
pub fn graph_valid(&self, graph: &ModuleGraph) -> Result<(), AnyError> {
|
|
self.module_graph_builder.graph_valid(graph)
|
|
}
|
|
|
|
async fn type_check_graph(
|
|
&self,
|
|
graph: ModuleGraph,
|
|
) -> Result<Arc<ModuleGraph>, AnyError> {
|
|
self
|
|
.type_checker
|
|
.check(
|
|
graph,
|
|
check::CheckOptions {
|
|
build_fast_check_graph: true,
|
|
lib: self.options.ts_type_lib_window(),
|
|
log_ignored_options: true,
|
|
reload: self.options.reload_flag(),
|
|
type_check_mode: self.options.type_check_mode(),
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
|
|
pub struct BuildFastCheckGraphOptions<'a> {
|
|
/// Whether to do fast check on workspace members. This
|
|
/// is mostly only useful when publishing.
|
|
pub workspace_fast_check: deno_graph::WorkspaceFastCheckOption<'a>,
|
|
}
|
|
|
|
pub struct ModuleGraphBuilder {
|
|
caches: Arc<cache::Caches>,
|
|
cjs_tracker: Arc<CjsTracker>,
|
|
cli_options: Arc<CliOptions>,
|
|
file_fetcher: Arc<FileFetcher>,
|
|
fs: Arc<dyn FileSystem>,
|
|
global_http_cache: Arc<GlobalHttpCache>,
|
|
in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
|
|
lockfile: Option<Arc<CliLockfile>>,
|
|
maybe_file_watcher_reporter: Option<FileWatcherReporter>,
|
|
module_info_cache: Arc<ModuleInfoCache>,
|
|
npm_resolver: Arc<dyn CliNpmResolver>,
|
|
parsed_source_cache: Arc<ParsedSourceCache>,
|
|
resolver: Arc<CliResolver>,
|
|
root_permissions_container: PermissionsContainer,
|
|
}
|
|
|
|
impl ModuleGraphBuilder {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new(
|
|
caches: Arc<cache::Caches>,
|
|
cjs_tracker: Arc<CjsTracker>,
|
|
cli_options: Arc<CliOptions>,
|
|
file_fetcher: Arc<FileFetcher>,
|
|
fs: Arc<dyn FileSystem>,
|
|
global_http_cache: Arc<GlobalHttpCache>,
|
|
in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
|
|
lockfile: Option<Arc<CliLockfile>>,
|
|
maybe_file_watcher_reporter: Option<FileWatcherReporter>,
|
|
module_info_cache: Arc<ModuleInfoCache>,
|
|
npm_resolver: Arc<dyn CliNpmResolver>,
|
|
parsed_source_cache: Arc<ParsedSourceCache>,
|
|
resolver: Arc<CliResolver>,
|
|
root_permissions_container: PermissionsContainer,
|
|
) -> Self {
|
|
Self {
|
|
caches,
|
|
cjs_tracker,
|
|
cli_options,
|
|
file_fetcher,
|
|
fs,
|
|
global_http_cache,
|
|
in_npm_pkg_checker,
|
|
lockfile,
|
|
maybe_file_watcher_reporter,
|
|
module_info_cache,
|
|
npm_resolver,
|
|
parsed_source_cache,
|
|
resolver,
|
|
root_permissions_container,
|
|
}
|
|
}
|
|
|
|
pub async fn build_graph_with_npm_resolution<'a>(
|
|
&self,
|
|
graph: &mut ModuleGraph,
|
|
options: CreateGraphOptions<'a>,
|
|
) -> Result<(), AnyError> {
|
|
enum MutLoaderRef<'a> {
|
|
Borrowed(&'a mut dyn Loader),
|
|
Owned(cache::FetchCacher),
|
|
}
|
|
|
|
impl<'a> MutLoaderRef<'a> {
|
|
pub fn as_mut_loader(&mut self) -> &mut dyn Loader {
|
|
match self {
|
|
Self::Borrowed(loader) => *loader,
|
|
Self::Owned(loader) => loader,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LockfileLocker<'a>(&'a CliLockfile);
|
|
|
|
impl<'a> deno_graph::source::Locker for LockfileLocker<'a> {
|
|
fn get_remote_checksum(
|
|
&self,
|
|
specifier: &deno_ast::ModuleSpecifier,
|
|
) -> Option<LoaderChecksum> {
|
|
self
|
|
.0
|
|
.lock()
|
|
.remote()
|
|
.get(specifier.as_str())
|
|
.map(|s| LoaderChecksum::new(s.clone()))
|
|
}
|
|
|
|
fn has_remote_checksum(
|
|
&self,
|
|
specifier: &deno_ast::ModuleSpecifier,
|
|
) -> bool {
|
|
self.0.lock().remote().contains_key(specifier.as_str())
|
|
}
|
|
|
|
fn set_remote_checksum(
|
|
&mut self,
|
|
specifier: &deno_ast::ModuleSpecifier,
|
|
checksum: LoaderChecksum,
|
|
) {
|
|
self
|
|
.0
|
|
.lock()
|
|
.insert_remote(specifier.to_string(), checksum.into_string())
|
|
}
|
|
|
|
fn get_pkg_manifest_checksum(
|
|
&self,
|
|
package_nv: &PackageNv,
|
|
) -> Option<LoaderChecksum> {
|
|
self
|
|
.0
|
|
.lock()
|
|
.content
|
|
.packages
|
|
.jsr
|
|
.get(package_nv)
|
|
.map(|s| LoaderChecksum::new(s.integrity.clone()))
|
|
}
|
|
|
|
fn set_pkg_manifest_checksum(
|
|
&mut self,
|
|
package_nv: &PackageNv,
|
|
checksum: LoaderChecksum,
|
|
) {
|
|
// a value would only exist in here if two workers raced
|
|
// to insert the same package manifest checksum
|
|
self
|
|
.0
|
|
.lock()
|
|
.insert_package(package_nv.clone(), checksum.into_string());
|
|
}
|
|
}
|
|
|
|
let maybe_imports = if options.graph_kind.include_types() {
|
|
self.cli_options.to_compiler_option_types()?
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let analyzer = self.module_info_cache.as_module_analyzer();
|
|
let mut loader = match options.loader {
|
|
Some(loader) => MutLoaderRef::Borrowed(loader),
|
|
None => MutLoaderRef::Owned(self.create_graph_loader()),
|
|
};
|
|
let cli_resolver = &self.resolver;
|
|
let graph_resolver = self.create_graph_resolver()?;
|
|
let graph_npm_resolver = cli_resolver.create_graph_npm_resolver();
|
|
let maybe_file_watcher_reporter = self
|
|
.maybe_file_watcher_reporter
|
|
.as_ref()
|
|
.map(|r| r.as_reporter());
|
|
let mut locker = self
|
|
.lockfile
|
|
.as_ref()
|
|
.map(|lockfile| LockfileLocker(lockfile));
|
|
self
|
|
.build_graph_with_npm_resolution_and_build_options(
|
|
graph,
|
|
options.roots,
|
|
loader.as_mut_loader(),
|
|
deno_graph::BuildOptions {
|
|
imports: maybe_imports,
|
|
is_dynamic: options.is_dynamic,
|
|
passthrough_jsr_specifiers: false,
|
|
executor: Default::default(),
|
|
file_system: &DenoGraphFsAdapter(self.fs.as_ref()),
|
|
jsr_url_provider: &CliJsrUrlProvider,
|
|
npm_resolver: Some(&graph_npm_resolver),
|
|
module_analyzer: &analyzer,
|
|
reporter: maybe_file_watcher_reporter,
|
|
resolver: Some(&graph_resolver),
|
|
locker: locker.as_mut().map(|l| l as _),
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn build_graph_with_npm_resolution_and_build_options<'a>(
|
|
&self,
|
|
graph: &mut ModuleGraph,
|
|
roots: Vec<ModuleSpecifier>,
|
|
loader: &'a mut dyn deno_graph::source::Loader,
|
|
options: deno_graph::BuildOptions<'a>,
|
|
) -> Result<(), AnyError> {
|
|
// ensure an "npm install" is done if the user has explicitly
|
|
// opted into using a node_modules directory
|
|
if self
|
|
.cli_options
|
|
.node_modules_dir()?
|
|
.map(|m| m.uses_node_modules_dir())
|
|
.unwrap_or(false)
|
|
{
|
|
if let Some(npm_resolver) = self.npm_resolver.as_managed() {
|
|
npm_resolver.ensure_top_level_package_json_install().await?;
|
|
}
|
|
}
|
|
|
|
// fill the graph with the information from the lockfile
|
|
let is_first_execution = graph.roots.is_empty();
|
|
if is_first_execution {
|
|
// populate the information from the lockfile
|
|
if let Some(lockfile) = &self.lockfile {
|
|
let lockfile = lockfile.lock();
|
|
graph.fill_from_lockfile(FillFromLockfileOptions {
|
|
redirects: lockfile
|
|
.content
|
|
.redirects
|
|
.iter()
|
|
.map(|(from, to)| (from.as_str(), to.as_str())),
|
|
package_specifiers: lockfile
|
|
.content
|
|
.packages
|
|
.specifiers
|
|
.iter()
|
|
.map(|(dep, id)| (dep, id.as_str())),
|
|
});
|
|
}
|
|
}
|
|
|
|
let initial_redirects_len = graph.redirects.len();
|
|
let initial_package_deps_len = graph.packages.package_deps_sum();
|
|
let initial_package_mappings_len = graph.packages.mappings().len();
|
|
|
|
if roots.iter().any(|r| r.scheme() == "npm")
|
|
&& self.npm_resolver.as_byonm().is_some()
|
|
{
|
|
bail!("Resolving npm specifier entrypoints this way is currently not supported with \"nodeModules\": \"manual\". In the meantime, try with --node-modules-dir=auto instead");
|
|
}
|
|
|
|
graph.build(roots, loader, options).await;
|
|
|
|
let has_redirects_changed = graph.redirects.len() != initial_redirects_len;
|
|
let has_jsr_package_deps_changed =
|
|
graph.packages.package_deps_sum() != initial_package_deps_len;
|
|
let has_jsr_package_mappings_changed =
|
|
graph.packages.mappings().len() != initial_package_mappings_len;
|
|
|
|
if has_redirects_changed
|
|
|| has_jsr_package_deps_changed
|
|
|| has_jsr_package_mappings_changed
|
|
{
|
|
if let Some(lockfile) = &self.lockfile {
|
|
let mut lockfile = lockfile.lock();
|
|
// https redirects
|
|
if has_redirects_changed {
|
|
let graph_redirects = graph.redirects.iter().filter(|(from, _)| {
|
|
!matches!(from.scheme(), "npm" | "file" | "deno")
|
|
});
|
|
for (from, to) in graph_redirects {
|
|
lockfile.insert_redirect(from.to_string(), to.to_string());
|
|
}
|
|
}
|
|
// jsr package mappings
|
|
if has_jsr_package_mappings_changed {
|
|
for (from, to) in graph.packages.mappings() {
|
|
lockfile.insert_package_specifier(
|
|
JsrDepPackageReq::jsr(from.clone()),
|
|
to.version.to_string(),
|
|
);
|
|
}
|
|
}
|
|
// jsr packages
|
|
if has_jsr_package_deps_changed {
|
|
for (nv, deps) in graph.packages.packages_with_deps() {
|
|
lockfile.add_package_deps(nv, deps.cloned());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn build_fast_check_graph(
|
|
&self,
|
|
graph: &mut ModuleGraph,
|
|
options: BuildFastCheckGraphOptions,
|
|
) -> Result<(), AnyError> {
|
|
if !graph.graph_kind().include_types() {
|
|
return Ok(());
|
|
}
|
|
|
|
log::debug!("Building fast check graph");
|
|
let fast_check_cache = if matches!(
|
|
options.workspace_fast_check,
|
|
deno_graph::WorkspaceFastCheckOption::Disabled
|
|
) {
|
|
Some(cache::FastCheckCache::new(self.caches.fast_check_db()))
|
|
} else {
|
|
None
|
|
};
|
|
let parser = self.parsed_source_cache.as_capturing_parser();
|
|
let cli_resolver = &self.resolver;
|
|
let graph_resolver = self.create_graph_resolver()?;
|
|
let graph_npm_resolver = cli_resolver.create_graph_npm_resolver();
|
|
|
|
graph.build_fast_check_type_graph(
|
|
deno_graph::BuildFastCheckTypeGraphOptions {
|
|
es_parser: Some(&parser),
|
|
fast_check_cache: fast_check_cache.as_ref().map(|c| c as _),
|
|
fast_check_dts: false,
|
|
jsr_url_provider: &CliJsrUrlProvider,
|
|
resolver: Some(&graph_resolver),
|
|
npm_resolver: Some(&graph_npm_resolver),
|
|
workspace_fast_check: options.workspace_fast_check,
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Creates the default loader used for creating a graph.
|
|
pub fn create_graph_loader(&self) -> cache::FetchCacher {
|
|
self.create_fetch_cacher(self.root_permissions_container.clone())
|
|
}
|
|
|
|
pub fn create_fetch_cacher(
|
|
&self,
|
|
permissions: PermissionsContainer,
|
|
) -> cache::FetchCacher {
|
|
cache::FetchCacher::new(
|
|
self.file_fetcher.clone(),
|
|
self.fs.clone(),
|
|
self.global_http_cache.clone(),
|
|
self.in_npm_pkg_checker.clone(),
|
|
self.module_info_cache.clone(),
|
|
cache::FetchCacherOptions {
|
|
file_header_overrides: self.cli_options.resolve_file_header_overrides(),
|
|
permissions,
|
|
is_deno_publish: matches!(
|
|
self.cli_options.sub_command(),
|
|
crate::args::DenoSubcommand::Publish { .. }
|
|
),
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Check if `roots` and their deps are available. Returns `Ok(())` if
|
|
/// so. Returns `Err(_)` if there is a known module graph or resolution
|
|
/// error statically reachable from `roots` and not a dynamic import.
|
|
pub fn graph_valid(&self, graph: &ModuleGraph) -> Result<(), AnyError> {
|
|
self.graph_roots_valid(
|
|
graph,
|
|
&graph.roots.iter().cloned().collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
|
|
pub fn graph_roots_valid(
|
|
&self,
|
|
graph: &ModuleGraph,
|
|
roots: &[ModuleSpecifier],
|
|
) -> Result<(), AnyError> {
|
|
graph_valid(
|
|
graph,
|
|
&self.fs,
|
|
roots,
|
|
GraphValidOptions {
|
|
kind: if self.cli_options.type_check_mode().is_true() {
|
|
GraphKind::All
|
|
} else {
|
|
GraphKind::CodeOnly
|
|
},
|
|
check_js: self.cli_options.check_js(),
|
|
exit_integrity_errors: true,
|
|
},
|
|
)
|
|
}
|
|
|
|
fn create_graph_resolver(&self) -> Result<CliGraphResolver, AnyError> {
|
|
let jsx_import_source_config = self
|
|
.cli_options
|
|
.workspace()
|
|
.to_maybe_jsx_import_source_config()?;
|
|
Ok(CliGraphResolver {
|
|
cjs_tracker: &self.cjs_tracker,
|
|
resolver: &self.resolver,
|
|
jsx_import_source_config,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Adds more explanatory information to a resolution error.
|
|
pub fn enhanced_resolution_error_message(error: &ResolutionError) -> String {
|
|
let mut message = format_deno_graph_error(error);
|
|
|
|
let maybe_hint = if let Some(specifier) =
|
|
get_resolution_error_bare_node_specifier(error)
|
|
{
|
|
if !*DENO_DISABLE_PEDANTIC_NODE_WARNINGS {
|
|
Some(format!("If you want to use a built-in Node module, add a \"node:\" prefix (ex. \"node:{specifier}\")."))
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
get_import_prefix_missing_error(error).map(|specifier| {
|
|
format!(
|
|
"If you want to use a JSR or npm package, try running `deno add jsr:{}` or `deno add npm:{}`",
|
|
specifier, specifier
|
|
)
|
|
})
|
|
};
|
|
|
|
if let Some(hint) = maybe_hint {
|
|
message.push_str(&format!("\n {} {}", colors::cyan("hint:"), hint));
|
|
}
|
|
|
|
message
|
|
}
|
|
|
|
fn enhanced_sloppy_imports_error_message(
|
|
fs: &Arc<dyn FileSystem>,
|
|
error: &ModuleError,
|
|
) -> Option<String> {
|
|
match error {
|
|
ModuleError::LoadingErr(specifier, _, ModuleLoadError::Loader(_)) // ex. "Is a directory" error
|
|
| ModuleError::Missing(specifier, _) => {
|
|
let additional_message = CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(fs.clone()))
|
|
.resolve(specifier, SloppyImportsResolutionKind::Execution)?
|
|
.as_suggestion_message();
|
|
Some(format!(
|
|
"{} {} or run with --unstable-sloppy-imports",
|
|
error,
|
|
additional_message,
|
|
))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn enhanced_integrity_error_message(err: &ModuleError) -> Option<String> {
|
|
match err {
|
|
ModuleError::LoadingErr(
|
|
specifier,
|
|
_,
|
|
ModuleLoadError::Jsr(JsrLoadError::ContentChecksumIntegrity(
|
|
checksum_err,
|
|
)),
|
|
) => {
|
|
Some(format!(
|
|
concat!(
|
|
"Integrity check failed in package. The package may have been tampered with.\n\n",
|
|
" Specifier: {}\n",
|
|
" Actual: {}\n",
|
|
" Expected: {}\n\n",
|
|
"If you modified your global cache, run again with the --reload flag to restore ",
|
|
"its state. If you want to modify dependencies locally run again with the ",
|
|
"--vendor flag or specify `\"vendor\": true` in a deno.json then modify the contents ",
|
|
"of the vendor/ folder."
|
|
),
|
|
specifier,
|
|
checksum_err.actual,
|
|
checksum_err.expected,
|
|
))
|
|
}
|
|
ModuleError::LoadingErr(
|
|
_specifier,
|
|
_,
|
|
ModuleLoadError::Jsr(
|
|
JsrLoadError::PackageVersionManifestChecksumIntegrity(
|
|
package_nv,
|
|
checksum_err,
|
|
),
|
|
),
|
|
) => {
|
|
Some(format!(
|
|
concat!(
|
|
"Integrity check failed for package. The source code is invalid, as it does not match the expected hash in the lock file.\n\n",
|
|
" Package: {}\n",
|
|
" Actual: {}\n",
|
|
" Expected: {}\n\n",
|
|
"This could be caused by:\n",
|
|
" * the lock file may be corrupt\n",
|
|
" * the source itself may be corrupt\n\n",
|
|
"Investigate the lockfile; delete it to regenerate the lockfile or --reload to reload the source code from the server."
|
|
),
|
|
package_nv,
|
|
checksum_err.actual,
|
|
checksum_err.expected,
|
|
))
|
|
}
|
|
ModuleError::LoadingErr(
|
|
specifier,
|
|
_,
|
|
ModuleLoadError::HttpsChecksumIntegrity(checksum_err),
|
|
) => {
|
|
Some(format!(
|
|
concat!(
|
|
"Integrity check failed for remote specifier. The source code is invalid, as it does not match the expected hash in the lock file.\n\n",
|
|
" Specifier: {}\n",
|
|
" Actual: {}\n",
|
|
" Expected: {}\n\n",
|
|
"This could be caused by:\n",
|
|
" * the lock file may be corrupt\n",
|
|
" * the source itself may be corrupt\n\n",
|
|
"Investigate the lockfile; delete it to regenerate the lockfile or --reload to reload the source code from the server."
|
|
),
|
|
specifier,
|
|
checksum_err.actual,
|
|
checksum_err.expected,
|
|
))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_resolution_error_bare_node_specifier(
|
|
error: &ResolutionError,
|
|
) -> Option<&str> {
|
|
get_resolution_error_bare_specifier(error)
|
|
.filter(|specifier| deno_node::is_builtin_node_module(specifier))
|
|
}
|
|
|
|
fn get_resolution_error_bare_specifier(
|
|
error: &ResolutionError,
|
|
) -> Option<&str> {
|
|
if let ResolutionError::InvalidSpecifier {
|
|
error: SpecifierError::ImportPrefixMissing { specifier, .. },
|
|
..
|
|
} = error
|
|
{
|
|
Some(specifier.as_str())
|
|
} else if let ResolutionError::ResolverError { error, .. } = error {
|
|
if let ResolveError::Other(error) = (*error).as_ref() {
|
|
if let Some(ImportMapError::UnmappedBareSpecifier(specifier, _)) =
|
|
error.downcast_ref::<ImportMapError>()
|
|
{
|
|
Some(specifier.as_str())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_import_prefix_missing_error(error: &ResolutionError) -> Option<&str> {
|
|
let mut maybe_specifier = None;
|
|
if let ResolutionError::InvalidSpecifier {
|
|
error: SpecifierError::ImportPrefixMissing { specifier, .. },
|
|
range,
|
|
} = error
|
|
{
|
|
if range.specifier.scheme() == "file" {
|
|
maybe_specifier = Some(specifier);
|
|
}
|
|
} else if let ResolutionError::ResolverError { error, range, .. } = error {
|
|
if range.specifier.scheme() == "file" {
|
|
match error.as_ref() {
|
|
ResolveError::Specifier(specifier_error) => {
|
|
if let SpecifierError::ImportPrefixMissing { specifier, .. } =
|
|
specifier_error
|
|
{
|
|
maybe_specifier = Some(specifier);
|
|
}
|
|
}
|
|
ResolveError::Other(other_error) => {
|
|
if let Some(SpecifierError::ImportPrefixMissing {
|
|
specifier, ..
|
|
}) = other_error.downcast_ref::<SpecifierError>()
|
|
{
|
|
maybe_specifier = Some(specifier);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE(bartlomieju): For now, return None if a specifier contains a dot or a space. This is because
|
|
// suggesting to `deno add bad-module.ts` makes no sense and is worse than not providing
|
|
// a suggestion at all. This should be improved further in the future
|
|
if let Some(specifier) = maybe_specifier {
|
|
if specifier.contains('.') || specifier.contains(' ') {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
maybe_specifier.map(|s| s.as_str())
|
|
}
|
|
|
|
/// Gets if any of the specified root's "file:" dependents are in the
|
|
/// provided changed set.
|
|
pub fn has_graph_root_local_dependent_changed(
|
|
graph: &ModuleGraph,
|
|
root: &ModuleSpecifier,
|
|
canonicalized_changed_paths: &HashSet<PathBuf>,
|
|
) -> bool {
|
|
let mut dependent_specifiers = graph.walk(
|
|
std::iter::once(root),
|
|
deno_graph::WalkOptions {
|
|
follow_dynamic: true,
|
|
kind: GraphKind::All,
|
|
prefer_fast_check_graph: true,
|
|
check_js: true,
|
|
},
|
|
);
|
|
while let Some((s, _)) = dependent_specifiers.next() {
|
|
if let Ok(path) = url_to_file_path(s) {
|
|
if let Ok(path) = canonicalize_path(&path) {
|
|
if canonicalized_changed_paths.contains(&path) {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
// skip walking this remote module's dependencies
|
|
dependent_specifiers.skip_previous_dependencies();
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct FileWatcherReporter {
|
|
watcher_communicator: Arc<WatcherCommunicator>,
|
|
file_paths: Arc<Mutex<Vec<PathBuf>>>,
|
|
}
|
|
|
|
impl FileWatcherReporter {
|
|
pub fn new(watcher_communicator: Arc<WatcherCommunicator>) -> Self {
|
|
Self {
|
|
watcher_communicator,
|
|
file_paths: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn as_reporter(&self) -> &dyn deno_graph::source::Reporter {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl deno_graph::source::Reporter for FileWatcherReporter {
|
|
fn on_load(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
modules_done: usize,
|
|
modules_total: usize,
|
|
) {
|
|
let mut file_paths = self.file_paths.lock();
|
|
if specifier.scheme() == "file" {
|
|
// Don't trust that the path is a valid path at this point:
|
|
// https://github.com/denoland/deno/issues/26209.
|
|
if let Ok(file_path) = specifier.to_file_path() {
|
|
file_paths.push(file_path);
|
|
}
|
|
}
|
|
|
|
if modules_done == modules_total {
|
|
self
|
|
.watcher_communicator
|
|
.watch_paths(file_paths.drain(..).collect())
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct DenoGraphFsAdapter<'a>(
|
|
pub &'a dyn deno_runtime::deno_fs::FileSystem,
|
|
);
|
|
|
|
impl<'a> deno_graph::source::FileSystem for DenoGraphFsAdapter<'a> {
|
|
fn read_dir(
|
|
&self,
|
|
dir_url: &deno_graph::ModuleSpecifier,
|
|
) -> Vec<deno_graph::source::DirEntry> {
|
|
use deno_core::anyhow;
|
|
use deno_graph::source::DirEntry;
|
|
use deno_graph::source::DirEntryKind;
|
|
|
|
let dir_path = match dir_url.to_file_path() {
|
|
Ok(path) => path,
|
|
// ignore, treat as non-analyzable
|
|
Err(()) => return vec![],
|
|
};
|
|
let entries = match self.0.read_dir_sync(&dir_path) {
|
|
Ok(dir) => dir,
|
|
Err(err)
|
|
if matches!(
|
|
err.kind(),
|
|
std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::NotFound
|
|
) =>
|
|
{
|
|
return vec![];
|
|
}
|
|
Err(err) => {
|
|
return vec![DirEntry {
|
|
kind: DirEntryKind::Error(
|
|
anyhow::Error::from(err)
|
|
.context("Failed to read directory.".to_string()),
|
|
),
|
|
url: dir_url.clone(),
|
|
}];
|
|
}
|
|
};
|
|
let mut dir_entries = Vec::with_capacity(entries.len());
|
|
for entry in entries {
|
|
let entry_path = dir_path.join(&entry.name);
|
|
dir_entries.push(if entry.is_directory {
|
|
DirEntry {
|
|
kind: DirEntryKind::Dir,
|
|
url: ModuleSpecifier::from_directory_path(&entry_path).unwrap(),
|
|
}
|
|
} else if entry.is_file {
|
|
DirEntry {
|
|
kind: DirEntryKind::File,
|
|
url: ModuleSpecifier::from_file_path(&entry_path).unwrap(),
|
|
}
|
|
} else if entry.is_symlink {
|
|
DirEntry {
|
|
kind: DirEntryKind::Symlink,
|
|
url: ModuleSpecifier::from_file_path(&entry_path).unwrap(),
|
|
}
|
|
} else {
|
|
continue;
|
|
});
|
|
}
|
|
|
|
dir_entries
|
|
}
|
|
}
|
|
|
|
pub fn format_range_with_colors(referrer: &deno_graph::Range) -> String {
|
|
format!(
|
|
"{}:{}:{}",
|
|
colors::cyan(referrer.specifier.as_str()),
|
|
colors::yellow(&(referrer.range.start.line + 1).to_string()),
|
|
colors::yellow(&(referrer.range.start.character + 1).to_string())
|
|
)
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub struct CliJsrUrlProvider;
|
|
|
|
impl deno_graph::source::JsrUrlProvider for CliJsrUrlProvider {
|
|
fn url(&self) -> &'static ModuleSpecifier {
|
|
jsr_url()
|
|
}
|
|
}
|
|
|
|
// todo(dsherret): We should change ModuleError to use thiserror so that
|
|
// we don't need to do this.
|
|
fn format_deno_graph_error(err: &dyn Error) -> String {
|
|
use std::fmt::Write;
|
|
|
|
let mut message = format!("{}", err);
|
|
let mut maybe_source = err.source();
|
|
|
|
if maybe_source.is_some() {
|
|
let mut past_message = message.clone();
|
|
let mut count = 0;
|
|
let mut display_count = 0;
|
|
while let Some(source) = maybe_source {
|
|
let current_message = format!("{}", source);
|
|
maybe_source = source.source();
|
|
|
|
// sometimes an error might be repeated due to
|
|
// being boxed multiple times in another AnyError
|
|
if current_message != past_message {
|
|
write!(message, "\n {}: ", display_count,).unwrap();
|
|
for (i, line) in current_message.lines().enumerate() {
|
|
if i > 0 {
|
|
write!(message, "\n {}", line).unwrap();
|
|
} else {
|
|
write!(message, "{}", line).unwrap();
|
|
}
|
|
}
|
|
display_count += 1;
|
|
}
|
|
|
|
if count > 8 {
|
|
write!(message, "\n {}: ...", count).unwrap();
|
|
break;
|
|
}
|
|
|
|
past_message = current_message;
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
message
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CliGraphResolver<'a> {
|
|
cjs_tracker: &'a CjsTracker,
|
|
resolver: &'a CliResolver,
|
|
jsx_import_source_config: Option<JsxImportSourceConfig>,
|
|
}
|
|
|
|
impl<'a> deno_graph::source::Resolver for CliGraphResolver<'a> {
|
|
fn default_jsx_import_source(&self) -> Option<String> {
|
|
self
|
|
.jsx_import_source_config
|
|
.as_ref()
|
|
.and_then(|c| c.default_specifier.clone())
|
|
}
|
|
|
|
fn default_jsx_import_source_types(&self) -> Option<String> {
|
|
self
|
|
.jsx_import_source_config
|
|
.as_ref()
|
|
.and_then(|c| c.default_types_specifier.clone())
|
|
}
|
|
|
|
fn jsx_import_source_module(&self) -> &str {
|
|
self
|
|
.jsx_import_source_config
|
|
.as_ref()
|
|
.map(|c| c.module.as_str())
|
|
.unwrap_or(deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE)
|
|
}
|
|
|
|
fn resolve(
|
|
&self,
|
|
raw_specifier: &str,
|
|
referrer_range: &deno_graph::Range,
|
|
resolution_kind: ResolutionKind,
|
|
) -> Result<ModuleSpecifier, ResolveError> {
|
|
self.resolver.resolve(
|
|
raw_specifier,
|
|
&referrer_range.specifier,
|
|
referrer_range.range.start,
|
|
referrer_range
|
|
.resolution_mode
|
|
.map(to_node_resolution_mode)
|
|
.unwrap_or_else(|| {
|
|
self
|
|
.cjs_tracker
|
|
.get_referrer_kind(&referrer_range.specifier)
|
|
}),
|
|
to_node_resolution_kind(resolution_kind),
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn to_node_resolution_kind(
|
|
kind: ResolutionKind,
|
|
) -> node_resolver::NodeResolutionKind {
|
|
match kind {
|
|
ResolutionKind::Execution => node_resolver::NodeResolutionKind::Execution,
|
|
ResolutionKind::Types => node_resolver::NodeResolutionKind::Types,
|
|
}
|
|
}
|
|
|
|
pub fn to_node_resolution_mode(
|
|
mode: deno_graph::source::ResolutionMode,
|
|
) -> node_resolver::ResolutionMode {
|
|
match mode {
|
|
deno_graph::source::ResolutionMode::Import => {
|
|
node_resolver::ResolutionMode::Import
|
|
}
|
|
deno_graph::source::ResolutionMode::Require => {
|
|
node_resolver::ResolutionMode::Require
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::sync::Arc;
|
|
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_graph::source::ResolveError;
|
|
use deno_graph::PositionRange;
|
|
use deno_graph::Range;
|
|
use deno_graph::ResolutionError;
|
|
use deno_graph::SpecifierError;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn import_map_node_resolution_error() {
|
|
let cases = vec![("fs", Some("fs")), ("other", None)];
|
|
for (input, output) in cases {
|
|
let import_map = import_map::ImportMap::new(
|
|
ModuleSpecifier::parse("file:///deno.json").unwrap(),
|
|
);
|
|
let specifier = ModuleSpecifier::parse("file:///file.ts").unwrap();
|
|
let err = import_map.resolve(input, &specifier).err().unwrap();
|
|
let err = ResolutionError::ResolverError {
|
|
error: Arc::new(ResolveError::Other(err.into())),
|
|
specifier: input.to_string(),
|
|
range: Range {
|
|
specifier,
|
|
resolution_mode: None,
|
|
range: PositionRange::zeroed(),
|
|
},
|
|
};
|
|
assert_eq!(get_resolution_error_bare_node_specifier(&err), output);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bare_specifier_node_resolution_error() {
|
|
let cases = vec![("process", Some("process")), ("other", None)];
|
|
for (input, output) in cases {
|
|
let specifier = ModuleSpecifier::parse("file:///file.ts").unwrap();
|
|
let err = ResolutionError::InvalidSpecifier {
|
|
range: Range {
|
|
specifier,
|
|
resolution_mode: None,
|
|
range: PositionRange::zeroed(),
|
|
},
|
|
error: SpecifierError::ImportPrefixMissing {
|
|
specifier: input.to_string(),
|
|
referrer: None,
|
|
},
|
|
};
|
|
assert_eq!(get_resolution_error_bare_node_specifier(&err), output,);
|
|
}
|
|
}
|
|
}
|