summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2024-09-28 19:17:48 -0400
committerGitHub <noreply@github.com>2024-09-28 19:17:48 -0400
commit5faf769ac61b627d14710cdf487de7cd4eb3f9d3 (patch)
tree2cc4ab975522b3c8845ab3040c010fd998a769a6
parent3138478f66823348eb745c7f0c2d34eed378a3f0 (diff)
refactor: extract out sloppy imports resolution from CLI crate (#25920)
This is slow progress towards creating a `deno_resolver` crate. Waiting on: * https://github.com/denoland/deno/pull/25918 * https://github.com/denoland/deno/pull/25916
-rw-r--r--Cargo.lock12
-rw-r--r--Cargo.toml10
-rw-r--r--cli/Cargo.toml6
-rw-r--r--cli/factory.rs18
-rw-r--r--cli/graph_util.rs9
-rw-r--r--cli/lsp/config.rs13
-rw-r--r--cli/lsp/diagnostics.rs11
-rw-r--r--cli/resolver.rs525
-rw-r--r--cli/tools/lint/rules/mod.rs6
-rw-r--r--cli/tools/lint/rules/no_sloppy_imports.rs19
-rw-r--r--cli/tools/registry/mod.rs7
-rw-r--r--cli/tools/registry/unfurl.rs15
-rw-r--r--resolvers/deno/Cargo.toml24
-rw-r--r--resolvers/deno/README.md3
-rw-r--r--resolvers/deno/lib.rs3
-rw-r--r--resolvers/deno/sloppy_imports.rs511
-rw-r--r--resolvers/node/Cargo.toml (renamed from ext/node_resolver/Cargo.toml)0
-rw-r--r--resolvers/node/README.md (renamed from ext/node_resolver/README.md)0
-rw-r--r--resolvers/node/analyze.rs (renamed from ext/node_resolver/analyze.rs)0
-rw-r--r--resolvers/node/clippy.toml (renamed from ext/node_resolver/clippy.toml)0
-rw-r--r--resolvers/node/env.rs (renamed from ext/node_resolver/env.rs)0
-rw-r--r--resolvers/node/errors.rs (renamed from ext/node_resolver/errors.rs)0
-rw-r--r--resolvers/node/lib.rs (renamed from ext/node_resolver/lib.rs)0
-rw-r--r--resolvers/node/npm.rs (renamed from ext/node_resolver/npm.rs)0
-rw-r--r--resolvers/node/package_json.rs (renamed from ext/node_resolver/package_json.rs)0
-rw-r--r--resolvers/node/path.rs (renamed from ext/node_resolver/path.rs)0
-rw-r--r--resolvers/node/resolution.rs (renamed from ext/node_resolver/resolution.rs)0
-rw-r--r--resolvers/node/sync.rs (renamed from ext/node_resolver/sync.rs)0
28 files changed, 665 insertions, 527 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1372ac191..bdde5d541 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1150,7 +1150,6 @@ version = "2.0.0-rc.7"
dependencies = [
"anstream",
"async-trait",
- "base32",
"base64 0.21.7",
"bincode",
"bytes",
@@ -1175,6 +1174,7 @@ dependencies = [
"deno_npm",
"deno_package_json",
"deno_path_util",
+ "deno_resolver",
"deno_runtime",
"deno_semver",
"deno_task_shell",
@@ -1950,6 +1950,16 @@ dependencies = [
]
[[package]]
+name = "deno_resolver"
+version = "0.0.1"
+dependencies = [
+ "deno_media_type",
+ "deno_path_util",
+ "test_server",
+ "url",
+]
+
+[[package]]
name = "deno_runtime"
version = "0.177.0"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index 4b27f85b8..2f53c8999 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,13 +21,14 @@ members = [
"ext/napi",
"ext/net",
"ext/node",
- "ext/node_resolver",
"ext/url",
"ext/web",
"ext/webgpu",
"ext/webidl",
"ext/websocket",
"ext/webstorage",
+ "resolvers/deno",
+ "resolvers/node",
"runtime",
"runtime/permissions",
"tests",
@@ -50,6 +51,7 @@ deno_core = { version = "0.311.0" }
deno_bench_util = { version = "0.162.0", path = "./bench_util" }
deno_lockfile = "=0.23.1"
deno_media_type = { version = "0.1.4", features = ["module_specifier"] }
+deno_npm = "=0.25.2"
deno_path_util = "=0.1.1"
deno_permissions = { version = "0.28.0", path = "./runtime/permissions" }
deno_runtime = { version = "0.177.0", path = "./runtime" }
@@ -86,7 +88,10 @@ deno_webgpu = { version = "0.135.0", path = "./ext/webgpu" }
deno_webidl = { version = "0.168.0", path = "./ext/webidl" }
deno_websocket = { version = "0.173.0", path = "./ext/websocket" }
deno_webstorage = { version = "0.163.0", path = "./ext/webstorage" }
-node_resolver = { version = "0.7.0", path = "./ext/node_resolver" }
+
+# resolvers
+deno_resolver = { version = "0.0.1", path = "./resolvers/deno" }
+node_resolver = { version = "0.7.0", path = "./resolvers/node" }
aes = "=0.8.3"
anyhow = "1.0.57"
@@ -102,6 +107,7 @@ cbc = { version = "=0.1.2", features = ["alloc"] }
# Instead use util::time::utc_now()
chrono = { version = "0.4", default-features = false, features = ["std", "serde"] }
console_static_text = "=0.8.1"
+dashmap = "5.5.3"
data-encoding = "2.3.3"
data-url = "=0.3.0"
deno_cache_dir = "=0.12.0"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index ddcf7119f..32e0651b2 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -71,9 +71,10 @@ deno_doc = { version = "0.150.0", features = ["html", "syntect"] }
deno_graph = { version = "=0.82.3" }
deno_lint = { version = "=0.67.0", features = ["docs"] }
deno_lockfile.workspace = true
-deno_npm = "=0.25.2"
+deno_npm.workspace = true
deno_package_json.workspace = true
deno_path_util.workspace = true
+deno_resolver.workspace = true
deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] }
deno_semver.workspace = true
deno_task_shell = "=0.17.0"
@@ -85,7 +86,6 @@ node_resolver.workspace = true
anstream = "0.6.14"
async-trait.workspace = true
-base32.workspace = true
base64.workspace = true
bincode = "=1.3.3"
bytes.workspace = true
@@ -96,7 +96,7 @@ clap_complete = "=4.5.24"
clap_complete_fig = "=4.5.2"
color-print = "0.3.5"
console_static_text.workspace = true
-dashmap = "5.5.3"
+dashmap.workspace = true
data-encoding.workspace = true
dissimilar = "=1.0.4"
dotenvy = "0.15.7"
diff --git a/cli/factory.rs b/cli/factory.rs
index 8aea635d2..5e525ee32 100644
--- a/cli/factory.rs
+++ b/cli/factory.rs
@@ -41,8 +41,9 @@ use crate::resolver::CjsResolutionStore;
use crate::resolver::CliGraphResolver;
use crate::resolver::CliGraphResolverOptions;
use crate::resolver::CliNodeResolver;
+use crate::resolver::CliSloppyImportsResolver;
use crate::resolver::NpmModuleLoader;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::SloppyImportsCachedFs;
use crate::standalone::DenoCompileBinaryWriter;
use crate::tools::check::TypeChecker;
use crate::tools::coverage::CoverageCollector;
@@ -186,7 +187,7 @@ struct CliFactoryServices {
npm_resolver: Deferred<Arc<dyn CliNpmResolver>>,
permission_desc_parser: Deferred<Arc<RuntimePermissionDescriptorParser>>,
root_permissions_container: Deferred<PermissionsContainer>,
- sloppy_imports_resolver: Deferred<Option<Arc<SloppyImportsResolver>>>,
+ sloppy_imports_resolver: Deferred<Option<Arc<CliSloppyImportsResolver>>>,
text_only_progress_bar: Deferred<ProgressBar>,
type_checker: Deferred<Arc<TypeChecker>>,
cjs_resolutions: Deferred<Arc<CjsResolutionStore>>,
@@ -404,17 +405,16 @@ impl CliFactory {
pub fn sloppy_imports_resolver(
&self,
- ) -> Result<Option<&Arc<SloppyImportsResolver>>, AnyError> {
+ ) -> Result<Option<&Arc<CliSloppyImportsResolver>>, AnyError> {
self
.services
.sloppy_imports_resolver
.get_or_try_init(|| {
- Ok(
- self
- .cli_options()?
- .unstable_sloppy_imports()
- .then(|| Arc::new(SloppyImportsResolver::new(self.fs().clone()))),
- )
+ Ok(self.cli_options()?.unstable_sloppy_imports().then(|| {
+ Arc::new(CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(
+ self.fs().clone(),
+ )))
+ }))
})
.map(|maybe| maybe.as_ref())
}
diff --git a/cli/graph_util.rs b/cli/graph_util.rs
index 7d03d3c0b..f7194ac11 100644
--- a/cli/graph_util.rs
+++ b/cli/graph_util.rs
@@ -14,7 +14,8 @@ use crate::errors::get_error_class_name;
use crate::file_fetcher::FileFetcher;
use crate::npm::CliNpmResolver;
use crate::resolver::CliGraphResolver;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
+use crate::resolver::SloppyImportsCachedFs;
use crate::tools::check;
use crate::tools::check::TypeChecker;
use crate::util::file_watcher::WatcherCommunicator;
@@ -31,7 +32,6 @@ use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::ModuleSpecifier;
use deno_graph::source::Loader;
-use deno_graph::source::ResolutionMode;
use deno_graph::source::ResolveError;
use deno_graph::GraphKind;
use deno_graph::ModuleError;
@@ -40,6 +40,7 @@ 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::SloppyImportsResolutionMode;
use deno_runtime::deno_fs::FileSystem;
use deno_runtime::deno_node;
use deno_runtime::deno_permissions::PermissionsContainer;
@@ -765,8 +766,8 @@ fn enhanced_sloppy_imports_error_message(
match error {
ModuleError::LoadingErr(specifier, _, ModuleLoadError::Loader(_)) // ex. "Is a directory" error
| ModuleError::Missing(specifier, _) => {
- let additional_message = SloppyImportsResolver::new(fs.clone())
- .resolve(specifier, ResolutionMode::Execution)?
+ let additional_message = CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(fs.clone()))
+ .resolve(specifier, SloppyImportsResolutionMode::Execution)?
.as_suggestion_message();
Some(format!(
"{} {} or run with --unstable-sloppy-imports",
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index dcb6120a4..c54de3a23 100644
--- a/cli/lsp/config.rs
+++ b/cli/lsp/config.rs
@@ -59,7 +59,8 @@ use crate::args::LintOptions;
use crate::cache::FastInsecureHasher;
use crate::file_fetcher::FileFetcher;
use crate::lsp::logging::lsp_warn;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
+use crate::resolver::SloppyImportsCachedFs;
use crate::tools::lint::CliLinter;
use crate::tools::lint::CliLinterOptions;
use crate::tools::lint::LintRuleProvider;
@@ -1181,7 +1182,7 @@ pub struct ConfigData {
pub lockfile: Option<Arc<CliLockfile>>,
pub npmrc: Option<Arc<ResolvedNpmRc>>,
pub resolver: Arc<WorkspaceResolver>,
- pub sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ pub sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
pub import_map_from_settings: Option<ModuleSpecifier>,
watched_files: HashMap<ModuleSpecifier, ConfigWatchedFileType>,
}
@@ -1584,9 +1585,11 @@ impl ConfigData {
.is_ok()
|| member_dir.workspace.has_unstable("sloppy-imports");
let sloppy_imports_resolver = unstable_sloppy_imports.then(|| {
- Arc::new(SloppyImportsResolver::new_without_stat_cache(Arc::new(
- deno_runtime::deno_fs::RealFs,
- )))
+ Arc::new(CliSloppyImportsResolver::new(
+ SloppyImportsCachedFs::new_without_stat_cache(Arc::new(
+ deno_runtime::deno_fs::RealFs,
+ )),
+ ))
});
let resolver = Arc::new(resolver);
let lint_rule_provider = LintRuleProvider::new(
diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs
index 1aebaf56f..e57681f3f 100644
--- a/cli/lsp/diagnostics.rs
+++ b/cli/lsp/diagnostics.rs
@@ -19,8 +19,8 @@ use super::urls::LspUrlMap;
use crate::graph_util;
use crate::graph_util::enhanced_resolution_error_message;
use crate::lsp::lsp_custom::DiagnosticBatchNotificationParams;
-use crate::resolver::SloppyImportsResolution;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
+use crate::resolver::SloppyImportsCachedFs;
use crate::tools::lint::CliLinter;
use crate::tools::lint::CliLinterOptions;
use crate::tools::lint::LintRuleProvider;
@@ -40,11 +40,12 @@ use deno_core::unsync::spawn_blocking;
use deno_core::unsync::JoinHandle;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
-use deno_graph::source::ResolutionMode;
use deno_graph::source::ResolveError;
use deno_graph::Resolution;
use deno_graph::ResolutionError;
use deno_graph::SpecifierError;
+use deno_resolver::sloppy_imports::SloppyImportsResolution;
+use deno_resolver::sloppy_imports::SloppyImportsResolutionMode;
use deno_runtime::deno_fs;
use deno_runtime::deno_node;
use deno_runtime::tokio_util::create_basic_runtime;
@@ -1263,7 +1264,9 @@ impl DenoDiagnostic {
Self::NotInstalledJsr(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("JSR package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))),
Self::NotInstalledNpm(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("NPM package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))),
Self::NoLocal(specifier) => {
- let maybe_sloppy_resolution = SloppyImportsResolver::new(Arc::new(deno_fs::RealFs)).resolve(specifier, ResolutionMode::Execution);
+ let maybe_sloppy_resolution = CliSloppyImportsResolver::new(
+ SloppyImportsCachedFs::new(Arc::new(deno_fs::RealFs))
+ ).resolve(specifier, SloppyImportsResolutionMode::Execution);
let data = maybe_sloppy_resolution.as_ref().map(|res| {
json!({
"specifier": specifier,
diff --git a/cli/resolver.rs b/cli/resolver.rs
index cf4cd8b74..d6e14c39d 100644
--- a/cli/resolver.rs
+++ b/cli/resolver.rs
@@ -22,7 +22,8 @@ use deno_graph::NpmLoadError;
use deno_graph::NpmResolvePkgReqsResult;
use deno_npm::resolution::NpmResolutionError;
use deno_package_json::PackageJsonDepValue;
-use deno_path_util::url_to_file_path;
+use deno_resolver::sloppy_imports::SloppyImportsResolutionMode;
+use deno_resolver::sloppy_imports::SloppyImportsResolver;
use deno_runtime::colors;
use deno_runtime::deno_fs;
use deno_runtime::deno_fs::FileSystem;
@@ -421,13 +422,16 @@ impl CjsResolutionStore {
}
}
+pub type CliSloppyImportsResolver =
+ SloppyImportsResolver<SloppyImportsCachedFs>;
+
/// A resolver that takes care of resolution, taking into account loaded
/// import map, JSX settings.
#[derive(Debug)]
pub struct CliGraphResolver {
node_resolver: Option<Arc<CliNodeResolver>>,
npm_resolver: Option<Arc<dyn CliNpmResolver>>,
- sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
workspace_resolver: Arc<WorkspaceResolver>,
maybe_default_jsx_import_source: Option<String>,
maybe_default_jsx_import_source_types: Option<String>,
@@ -441,7 +445,7 @@ pub struct CliGraphResolver {
pub struct CliGraphResolverOptions<'a> {
pub node_resolver: Option<Arc<CliNodeResolver>>,
pub npm_resolver: Option<Arc<dyn CliNpmResolver>>,
- pub sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ pub sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
pub workspace_resolver: Arc<WorkspaceResolver>,
pub bare_node_builtins_enabled: bool,
pub maybe_jsx_import_source_config: Option<JsxImportSourceConfig>,
@@ -565,7 +569,15 @@ impl Resolver for CliGraphResolver {
if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver {
Ok(
sloppy_imports_resolver
- .resolve(&specifier, mode)
+ .resolve(
+ &specifier,
+ match mode {
+ ResolutionMode::Execution => {
+ SloppyImportsResolutionMode::Execution
+ }
+ ResolutionMode::Types => SloppyImportsResolutionMode::Types,
+ },
+ )
.map(|s| s.into_specifier())
.unwrap_or(specifier),
)
@@ -847,96 +859,18 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> {
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum SloppyImportsFsEntry {
- File,
- Dir,
-}
-
-impl SloppyImportsFsEntry {
- pub fn from_fs_stat(
- stat: &deno_runtime::deno_io::fs::FsStat,
- ) -> Option<SloppyImportsFsEntry> {
- if stat.is_file {
- Some(SloppyImportsFsEntry::File)
- } else if stat.is_directory {
- Some(SloppyImportsFsEntry::Dir)
- } else {
- None
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum SloppyImportsResolution {
- /// Ex. `./file.js` to `./file.ts`
- JsToTs(ModuleSpecifier),
- /// Ex. `./file` to `./file.ts`
- NoExtension(ModuleSpecifier),
- /// Ex. `./dir` to `./dir/index.ts`
- Directory(ModuleSpecifier),
-}
-
-impl SloppyImportsResolution {
- pub fn as_specifier(&self) -> &ModuleSpecifier {
- match self {
- Self::JsToTs(specifier) => specifier,
- Self::NoExtension(specifier) => specifier,
- Self::Directory(specifier) => specifier,
- }
- }
-
- pub fn into_specifier(self) -> ModuleSpecifier {
- match self {
- Self::JsToTs(specifier) => specifier,
- Self::NoExtension(specifier) => specifier,
- Self::Directory(specifier) => specifier,
- }
- }
-
- pub fn as_suggestion_message(&self) -> String {
- format!("Maybe {}", self.as_base_message())
- }
-
- pub fn as_quick_fix_message(&self) -> String {
- let message = self.as_base_message();
- let mut chars = message.chars();
- format!(
- "{}{}.",
- chars.next().unwrap().to_uppercase(),
- chars.as_str()
- )
- }
-
- fn as_base_message(&self) -> String {
- match self {
- SloppyImportsResolution::JsToTs(specifier) => {
- let media_type = MediaType::from_specifier(specifier);
- format!("change the extension to '{}'", media_type.as_ts_extension())
- }
- SloppyImportsResolution::NoExtension(specifier) => {
- let media_type = MediaType::from_specifier(specifier);
- format!("add a '{}' extension", media_type.as_ts_extension())
- }
- SloppyImportsResolution::Directory(specifier) => {
- let file_name = specifier
- .path()
- .rsplit_once('/')
- .map(|(_, file_name)| file_name)
- .unwrap_or(specifier.path());
- format!("specify path to '{}' file in directory instead", file_name)
- }
- }
- }
-}
-
#[derive(Debug)]
-pub struct SloppyImportsResolver {
- fs: Arc<dyn FileSystem>,
- cache: Option<DashMap<PathBuf, Option<SloppyImportsFsEntry>>>,
+pub struct SloppyImportsCachedFs {
+ fs: Arc<dyn deno_fs::FileSystem>,
+ cache: Option<
+ DashMap<
+ PathBuf,
+ Option<deno_resolver::sloppy_imports::SloppyImportsFsEntry>,
+ >,
+ >,
}
-impl SloppyImportsResolver {
+impl SloppyImportsCachedFs {
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
Self {
fs,
@@ -947,409 +881,34 @@ impl SloppyImportsResolver {
pub fn new_without_stat_cache(fs: Arc<dyn FileSystem>) -> Self {
Self { fs, cache: None }
}
+}
- pub fn resolve(
+impl deno_resolver::sloppy_imports::SloppyImportResolverFs
+ for SloppyImportsCachedFs
+{
+ fn stat_sync(
&self,
- specifier: &ModuleSpecifier,
- mode: ResolutionMode,
- ) -> Option<SloppyImportsResolution> {
- fn path_without_ext(
- path: &Path,
- media_type: MediaType,
- ) -> Option<Cow<str>> {
- let old_path_str = path.to_string_lossy();
- match media_type {
- MediaType::Unknown => Some(old_path_str),
- _ => old_path_str
- .strip_suffix(media_type.as_ts_extension())
- .map(|s| Cow::Owned(s.to_string())),
- }
- }
-
- fn media_types_to_paths(
- path_no_ext: &str,
- original_media_type: MediaType,
- probe_media_type_types: Vec<MediaType>,
- reason: SloppyImportsResolutionReason,
- ) -> Vec<(PathBuf, SloppyImportsResolutionReason)> {
- probe_media_type_types
- .into_iter()
- .filter(|media_type| *media_type != original_media_type)
- .map(|media_type| {
- (
- PathBuf::from(format!(
- "{}{}",
- path_no_ext,
- media_type.as_ts_extension()
- )),
- reason,
- )
- })
- .collect::<Vec<_>>()
- }
-
- if specifier.scheme() != "file" {
- return None;
- }
-
- let path = url_to_file_path(specifier).ok()?;
-
- #[derive(Clone, Copy)]
- enum SloppyImportsResolutionReason {
- JsToTs,
- NoExtension,
- Directory,
- }
-
- let probe_paths: Vec<(PathBuf, SloppyImportsResolutionReason)> =
- match self.stat_sync(&path) {
- Some(SloppyImportsFsEntry::File) => {
- if mode.is_types() {
- let media_type = MediaType::from_specifier(specifier);
- // attempt to resolve the .d.ts file before the .js file
- let probe_media_type_types = match media_type {
- MediaType::JavaScript => {
- vec![(MediaType::Dts), MediaType::JavaScript]
- }
- MediaType::Mjs => {
- vec![MediaType::Dmts, MediaType::Dts, MediaType::Mjs]
- }
- MediaType::Cjs => {
- vec![MediaType::Dcts, MediaType::Dts, MediaType::Cjs]
- }
- _ => return None,
- };
- let path_no_ext = path_without_ext(&path, media_type)?;
- media_types_to_paths(
- &path_no_ext,
- media_type,
- probe_media_type_types,
- SloppyImportsResolutionReason::JsToTs,
- )
- } else {
- return None;
- }
- }
- entry @ None | entry @ Some(SloppyImportsFsEntry::Dir) => {
- let media_type = MediaType::from_specifier(specifier);
- let probe_media_type_types = match media_type {
- MediaType::JavaScript => (
- if mode.is_types() {
- vec![MediaType::TypeScript, MediaType::Tsx, MediaType::Dts]
- } else {
- vec![MediaType::TypeScript, MediaType::Tsx]
- },
- SloppyImportsResolutionReason::JsToTs,
- ),
- MediaType::Jsx => {
- (vec![MediaType::Tsx], SloppyImportsResolutionReason::JsToTs)
- }
- MediaType::Mjs => (
- if mode.is_types() {
- vec![MediaType::Mts, MediaType::Dmts, MediaType::Dts]
- } else {
- vec![MediaType::Mts]
- },
- SloppyImportsResolutionReason::JsToTs,
- ),
- MediaType::Cjs => (
- if mode.is_types() {
- vec![MediaType::Cts, MediaType::Dcts, MediaType::Dts]
- } else {
- vec![MediaType::Cts]
- },
- SloppyImportsResolutionReason::JsToTs,
- ),
- MediaType::TypeScript
- | MediaType::Mts
- | MediaType::Cts
- | MediaType::Dts
- | MediaType::Dmts
- | MediaType::Dcts
- | MediaType::Tsx
- | MediaType::Json
- | MediaType::Wasm
- | MediaType::TsBuildInfo
- | MediaType::SourceMap => {
- return None;
- }
- MediaType::Unknown => (
- if mode.is_types() {
- vec![
- MediaType::TypeScript,
- MediaType::Tsx,
- MediaType::Mts,
- MediaType::Dts,
- MediaType::Dmts,
- MediaType::Dcts,
- MediaType::JavaScript,
- MediaType::Jsx,
- MediaType::Mjs,
- ]
- } else {
- vec![
- MediaType::TypeScript,
- MediaType::JavaScript,
- MediaType::Tsx,
- MediaType::Jsx,
- MediaType::Mts,
- MediaType::Mjs,
- ]
- },
- SloppyImportsResolutionReason::NoExtension,
- ),
- };
- let mut probe_paths = match path_without_ext(&path, media_type) {
- Some(path_no_ext) => media_types_to_paths(
- &path_no_ext,
- media_type,
- probe_media_type_types.0,
- probe_media_type_types.1,
- ),
- None => vec![],
- };
-
- if matches!(entry, Some(SloppyImportsFsEntry::Dir)) {
- // try to resolve at the index file
- if mode.is_types() {
- probe_paths.push((
- path.join("index.ts"),
- SloppyImportsResolutionReason::Directory,
- ));
-
- probe_paths.push((
- path.join("index.mts"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.d.ts"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.d.mts"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.js"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.mjs"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.tsx"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.jsx"),
- SloppyImportsResolutionReason::Directory,
- ));
- } else {
- probe_paths.push((
- path.join("index.ts"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.mts"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.tsx"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.js"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.mjs"),
- SloppyImportsResolutionReason::Directory,
- ));
- probe_paths.push((
- path.join("index.jsx"),
- SloppyImportsResolutionReason::Directory,
- ));
- }
- }
- if probe_paths.is_empty() {
- return None;
- }
- probe_paths
- }
- };
-
- for (probe_path, reason) in probe_paths {
- if self.stat_sync(&probe_path) == Some(SloppyImportsFsEntry::File) {
- if let Ok(specifier) = ModuleSpecifier::from_file_path(probe_path) {
- match reason {
- SloppyImportsResolutionReason::JsToTs => {
- return Some(SloppyImportsResolution::JsToTs(specifier));
- }
- SloppyImportsResolutionReason::NoExtension => {
- return Some(SloppyImportsResolution::NoExtension(specifier));
- }
- SloppyImportsResolutionReason::Directory => {
- return Some(SloppyImportsResolution::Directory(specifier));
- }
- }
- }
- }
- }
-
- None
- }
-
- fn stat_sync(&self, path: &Path) -> Option<SloppyImportsFsEntry> {
+ path: &Path,
+ ) -> Option<deno_resolver::sloppy_imports::SloppyImportsFsEntry> {
if let Some(cache) = &self.cache {
if let Some(entry) = cache.get(path) {
return *entry;
}
}
- let entry = self
- .fs
- .stat_sync(path)
- .ok()
- .and_then(|stat| SloppyImportsFsEntry::from_fs_stat(&stat));
+ let entry = self.fs.stat_sync(path).ok().and_then(|stat| {
+ if stat.is_file {
+ Some(deno_resolver::sloppy_imports::SloppyImportsFsEntry::File)
+ } else if stat.is_directory {
+ Some(deno_resolver::sloppy_imports::SloppyImportsFsEntry::Dir)
+ } else {
+ None
+ }
+ });
+
if let Some(cache) = &self.cache {
cache.insert(path.to_owned(), entry);
}
entry
}
}
-
-#[cfg(test)]
-mod test {
- use test_util::TestContext;
-
- use super::*;
-
- #[test]
- fn test_unstable_sloppy_imports() {
- fn resolve(specifier: &ModuleSpecifier) -> Option<SloppyImportsResolution> {
- resolve_with_mode(specifier, ResolutionMode::Execution)
- }
-
- fn resolve_types(
- specifier: &ModuleSpecifier,
- ) -> Option<SloppyImportsResolution> {
- resolve_with_mode(specifier, ResolutionMode::Types)
- }
-
- fn resolve_with_mode(
- specifier: &ModuleSpecifier,
- mode: ResolutionMode,
- ) -> Option<SloppyImportsResolution> {
- SloppyImportsResolver::new(Arc::new(deno_fs::RealFs))
- .resolve(specifier, mode)
- }
-
- let context = TestContext::default();
- let temp_dir = context.temp_dir().path();
-
- // scenarios like resolving ./example.js to ./example.ts
- for (ext_from, ext_to) in [("js", "ts"), ("js", "tsx"), ("mjs", "mts")] {
- let ts_file = temp_dir.join(format!("file.{}", ext_to));
- ts_file.write("");
- assert_eq!(resolve(&ts_file.url_file()), None);
- assert_eq!(
- resolve(
- &temp_dir
- .url_dir()
- .join(&format!("file.{}", ext_from))
- .unwrap()
- ),
- Some(SloppyImportsResolution::JsToTs(ts_file.url_file())),
- );
- ts_file.remove_file();
- }
-
- // no extension scenarios
- for ext in ["js", "ts", "js", "tsx", "jsx", "mjs", "mts"] {
- let file = temp_dir.join(format!("file.{}", ext));
- file.write("");
- assert_eq!(
- resolve(
- &temp_dir
- .url_dir()
- .join("file") // no ext
- .unwrap()
- ),
- Some(SloppyImportsResolution::NoExtension(file.url_file()))
- );
- file.remove_file();
- }
-
- // .ts and .js exists, .js specified (goes to specified)
- {
- let ts_file = temp_dir.join("file.ts");
- ts_file.write("");
- let js_file = temp_dir.join("file.js");
- js_file.write("");
- assert_eq!(resolve(&js_file.url_file()), None);
- }
-
- // only js exists, .js specified
- {
- let js_only_file = temp_dir.join("js_only.js");
- js_only_file.write("");
- assert_eq!(resolve(&js_only_file.url_file()), None);
- assert_eq!(resolve_types(&js_only_file.url_file()), None);
- }
-
- // resolving a directory to an index file
- {
- let routes_dir = temp_dir.join("routes");
- routes_dir.create_dir_all();
- let index_file = routes_dir.join("index.ts");
- index_file.write("");
- assert_eq!(
- resolve(&routes_dir.url_file()),
- Some(SloppyImportsResolution::Directory(index_file.url_file())),
- );
- }
-
- // both a directory and a file with specifier is present
- {
- let api_dir = temp_dir.join("api");
- api_dir.create_dir_all();
- let bar_file = api_dir.join("bar.ts");
- bar_file.write("");
- let api_file = temp_dir.join("api.ts");
- api_file.write("");
- assert_eq!(
- resolve(&api_dir.url_file()),
- Some(SloppyImportsResolution::NoExtension(api_file.url_file())),
- );
- }
- }
-
- #[test]
- fn test_sloppy_import_resolution_suggestion_message() {
- // directory
- assert_eq!(
- SloppyImportsResolution::Directory(
- ModuleSpecifier::parse("file:///dir/index.js").unwrap()
- )
- .as_suggestion_message(),
- "Maybe specify path to 'index.js' file in directory instead"
- );
- // no ext
- assert_eq!(
- SloppyImportsResolution::NoExtension(
- ModuleSpecifier::parse("file:///dir/index.mjs").unwrap()
- )
- .as_suggestion_message(),
- "Maybe add a '.mjs' extension"
- );
- // js to ts
- assert_eq!(
- SloppyImportsResolution::JsToTs(
- ModuleSpecifier::parse("file:///dir/index.mts").unwrap()
- )
- .as_suggestion_message(),
- "Maybe change the extension to '.mts'"
- );
- }
-}
diff --git a/cli/tools/lint/rules/mod.rs b/cli/tools/lint/rules/mod.rs
index 2669ffda1..dd723ad15 100644
--- a/cli/tools/lint/rules/mod.rs
+++ b/cli/tools/lint/rules/mod.rs
@@ -14,7 +14,7 @@ use deno_graph::ModuleGraph;
use deno_lint::diagnostic::LintDiagnostic;
use deno_lint::rules::LintRule;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
mod no_sloppy_imports;
mod no_slow_types;
@@ -144,13 +144,13 @@ impl ConfiguredRules {
}
pub struct LintRuleProvider {
- sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
workspace_resolver: Option<Arc<WorkspaceResolver>>,
}
impl LintRuleProvider {
pub fn new(
- sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
workspace_resolver: Option<Arc<WorkspaceResolver>>,
) -> Self {
Self {
diff --git a/cli/tools/lint/rules/no_sloppy_imports.rs b/cli/tools/lint/rules/no_sloppy_imports.rs
index 4180be5be..2f6087588 100644
--- a/cli/tools/lint/rules/no_sloppy_imports.rs
+++ b/cli/tools/lint/rules/no_sloppy_imports.rs
@@ -16,24 +16,25 @@ use deno_lint::diagnostic::LintDiagnosticRange;
use deno_lint::diagnostic::LintFix;
use deno_lint::diagnostic::LintFixChange;
use deno_lint::rules::LintRule;
+use deno_resolver::sloppy_imports::SloppyImportsResolution;
+use deno_resolver::sloppy_imports::SloppyImportsResolutionMode;
use text_lines::LineAndColumnIndex;
use crate::graph_util::CliJsrUrlProvider;
-use crate::resolver::SloppyImportsResolution;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
use super::ExtendedLintRule;
#[derive(Debug)]
pub struct NoSloppyImportsRule {
- sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
// None for making printing out the lint rules easy
workspace_resolver: Option<Arc<WorkspaceResolver>>,
}
impl NoSloppyImportsRule {
pub fn new(
- sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ sloppy_imports_resolver: Option<Arc<CliSloppyImportsResolver>>,
workspace_resolver: Option<Arc<WorkspaceResolver>>,
) -> Self {
NoSloppyImportsRule {
@@ -172,7 +173,7 @@ impl LintRule for NoSloppyImportsRule {
#[derive(Debug)]
struct SloppyImportCaptureResolver<'a> {
workspace_resolver: &'a WorkspaceResolver,
- sloppy_imports_resolver: &'a SloppyImportsResolver,
+ sloppy_imports_resolver: &'a CliSloppyImportsResolver,
captures: RefCell<HashMap<Range, SloppyImportsResolution>>,
}
@@ -194,7 +195,13 @@ impl<'a> deno_graph::source::Resolver for SloppyImportCaptureResolver<'a> {
}
| deno_config::workspace::MappedResolution::ImportMap {
specifier, ..
- } => match self.sloppy_imports_resolver.resolve(&specifier, mode) {
+ } => match self.sloppy_imports_resolver.resolve(
+ &specifier,
+ match mode {
+ ResolutionMode::Execution => SloppyImportsResolutionMode::Execution,
+ ResolutionMode::Types => SloppyImportsResolutionMode::Types,
+ },
+ ) {
Some(res) => {
self
.captures
diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs
index 941514b04..4098d62e3 100644
--- a/cli/tools/registry/mod.rs
+++ b/cli/tools/registry/mod.rs
@@ -43,7 +43,8 @@ use crate::cache::ParsedSourceCache;
use crate::factory::CliFactory;
use crate::graph_util::ModuleGraphCreator;
use crate::http_util::HttpClient;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
+use crate::resolver::SloppyImportsCachedFs;
use crate::tools::check::CheckOptions;
use crate::tools::lint::collect_no_slow_type_diagnostics;
use crate::tools::registry::diagnostics::PublishDiagnostic;
@@ -108,7 +109,9 @@ pub async fn publish(
}
let specifier_unfurler = Arc::new(SpecifierUnfurler::new(
if cli_options.unstable_sloppy_imports() {
- Some(SloppyImportsResolver::new(cli_factory.fs().clone()))
+ Some(CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(
+ cli_factory.fs().clone(),
+ )))
} else {
None
},
diff --git a/cli/tools/registry/unfurl.rs b/cli/tools/registry/unfurl.rs
index 0f5b9fdd3..5ec726a64 100644
--- a/cli/tools/registry/unfurl.rs
+++ b/cli/tools/registry/unfurl.rs
@@ -12,9 +12,10 @@ use deno_graph::DynamicTemplatePart;
use deno_graph::ParserModuleAnalyzer;
use deno_graph::TypeScriptReference;
use deno_package_json::PackageJsonDepValue;
+use deno_resolver::sloppy_imports::SloppyImportsResolutionMode;
use deno_runtime::deno_node::is_builtin_node_module;
-use crate::resolver::SloppyImportsResolver;
+use crate::resolver::CliSloppyImportsResolver;
#[derive(Debug, Clone)]
pub enum SpecifierUnfurlerDiagnostic {
@@ -42,14 +43,14 @@ impl SpecifierUnfurlerDiagnostic {
}
pub struct SpecifierUnfurler {
- sloppy_imports_resolver: Option<SloppyImportsResolver>,
+ sloppy_imports_resolver: Option<CliSloppyImportsResolver>,
workspace_resolver: WorkspaceResolver,
bare_node_builtins: bool,
}
impl SpecifierUnfurler {
pub fn new(
- sloppy_imports_resolver: Option<SloppyImportsResolver>,
+ sloppy_imports_resolver: Option<CliSloppyImportsResolver>,
workspace_resolver: WorkspaceResolver,
bare_node_builtins: bool,
) -> Self {
@@ -179,7 +180,7 @@ impl SpecifierUnfurler {
let resolved =
if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver {
sloppy_imports_resolver
- .resolve(&resolved, deno_graph::source::ResolutionMode::Execution)
+ .resolve(&resolved, SloppyImportsResolutionMode::Execution)
.map(|res| res.into_specifier())
.unwrap_or(resolved)
} else {
@@ -388,6 +389,8 @@ fn to_range(
mod tests {
use std::sync::Arc;
+ use crate::resolver::SloppyImportsCachedFs;
+
use super::*;
use deno_ast::MediaType;
use deno_ast::ModuleSpecifier;
@@ -455,7 +458,9 @@ mod tests {
);
let fs = Arc::new(RealFs);
let unfurler = SpecifierUnfurler::new(
- Some(SloppyImportsResolver::new(fs)),
+ Some(CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(
+ fs,
+ ))),
workspace_resolver,
true,
);
diff --git a/resolvers/deno/Cargo.toml b/resolvers/deno/Cargo.toml
new file mode 100644
index 000000000..23c43810a
--- /dev/null
+++ b/resolvers/deno/Cargo.toml
@@ -0,0 +1,24 @@
+# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+[package]
+name = "deno_resolver"
+version = "0.0.1"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+readme = "README.md"
+repository.workspace = true
+description = "Deno resolution algorithm"
+
+[lib]
+path = "lib.rs"
+
+[features]
+
+[dependencies]
+deno_media_type.workspace = true
+deno_path_util.workspace = true
+url.workspace = true
+
+[dev-dependencies]
+test_util.workspace = true
diff --git a/resolvers/deno/README.md b/resolvers/deno/README.md
new file mode 100644
index 000000000..f51619a31
--- /dev/null
+++ b/resolvers/deno/README.md
@@ -0,0 +1,3 @@
+# deno_resolver
+
+Deno resolution algorithm.
diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs
new file mode 100644
index 000000000..7d7796d77
--- /dev/null
+++ b/resolvers/deno/lib.rs
@@ -0,0 +1,3 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+pub mod sloppy_imports;
diff --git a/resolvers/deno/sloppy_imports.rs b/resolvers/deno/sloppy_imports.rs
new file mode 100644
index 000000000..e4d0898e5
--- /dev/null
+++ b/resolvers/deno/sloppy_imports.rs
@@ -0,0 +1,511 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use std::borrow::Cow;
+use std::path::Path;
+use std::path::PathBuf;
+
+use deno_media_type::MediaType;
+use deno_path_util::url_to_file_path;
+use url::Url;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SloppyImportsFsEntry {
+ File,
+ Dir,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum SloppyImportsResolution {
+ /// Ex. `./file.js` to `./file.ts`
+ JsToTs(Url),
+ /// Ex. `./file` to `./file.ts`
+ NoExtension(Url),
+ /// Ex. `./dir` to `./dir/index.ts`
+ Directory(Url),
+}
+
+impl SloppyImportsResolution {
+ pub fn as_specifier(&self) -> &Url {
+ match self {
+ Self::JsToTs(specifier) => specifier,
+ Self::NoExtension(specifier) => specifier,
+ Self::Directory(specifier) => specifier,
+ }
+ }
+
+ pub fn into_specifier(self) -> Url {
+ match self {
+ Self::JsToTs(specifier) => specifier,
+ Self::NoExtension(specifier) => specifier,
+ Self::Directory(specifier) => specifier,
+ }
+ }
+
+ pub fn as_suggestion_message(&self) -> String {
+ format!("Maybe {}", self.as_base_message())
+ }
+
+ pub fn as_quick_fix_message(&self) -> String {
+ let message = self.as_base_message();
+ let mut chars = message.chars();
+ format!(
+ "{}{}.",
+ chars.next().unwrap().to_uppercase(),
+ chars.as_str()
+ )
+ }
+
+ fn as_base_message(&self) -> String {
+ match self {
+ SloppyImportsResolution::JsToTs(specifier) => {
+ let media_type = MediaType::from_specifier(specifier);
+ format!("change the extension to '{}'", media_type.as_ts_extension())
+ }
+ SloppyImportsResolution::NoExtension(specifier) => {
+ let media_type = MediaType::from_specifier(specifier);
+ format!("add a '{}' extension", media_type.as_ts_extension())
+ }
+ SloppyImportsResolution::Directory(specifier) => {
+ let file_name = specifier
+ .path()
+ .rsplit_once('/')
+ .map(|(_, file_name)| file_name)
+ .unwrap_or(specifier.path());
+ format!("specify path to '{}' file in directory instead", file_name)
+ }
+ }
+ }
+}
+
+/// The kind of resolution currently being done.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SloppyImportsResolutionMode {
+ /// Resolving for code that will be executed.
+ Execution,
+ /// Resolving for code that will be used for type information.
+ Types,
+}
+
+impl SloppyImportsResolutionMode {
+ pub fn is_types(&self) -> bool {
+ *self == SloppyImportsResolutionMode::Types
+ }
+}
+
+pub trait SloppyImportResolverFs {
+ fn stat_sync(&self, path: &Path) -> Option<SloppyImportsFsEntry>;
+
+ fn is_file(&self, path: &Path) -> bool {
+ self.stat_sync(path) == Some(SloppyImportsFsEntry::File)
+ }
+}
+
+#[derive(Debug)]
+pub struct SloppyImportsResolver<Fs: SloppyImportResolverFs> {
+ fs: Fs,
+}
+
+impl<Fs: SloppyImportResolverFs> SloppyImportsResolver<Fs> {
+ pub fn new(fs: Fs) -> Self {
+ Self { fs }
+ }
+
+ pub fn resolve(
+ &self,
+ specifier: &Url,
+ mode: SloppyImportsResolutionMode,
+ ) -> Option<SloppyImportsResolution> {
+ fn path_without_ext(
+ path: &Path,
+ media_type: MediaType,
+ ) -> Option<Cow<str>> {
+ let old_path_str = path.to_string_lossy();
+ match media_type {
+ MediaType::Unknown => Some(old_path_str),
+ _ => old_path_str
+ .strip_suffix(media_type.as_ts_extension())
+ .map(|s| Cow::Owned(s.to_string())),
+ }
+ }
+
+ fn media_types_to_paths(
+ path_no_ext: &str,
+ original_media_type: MediaType,
+ probe_media_type_types: Vec<MediaType>,
+ reason: SloppyImportsResolutionReason,
+ ) -> Vec<(PathBuf, SloppyImportsResolutionReason)> {
+ probe_media_type_types
+ .into_iter()
+ .filter(|media_type| *media_type != original_media_type)
+ .map(|media_type| {
+ (
+ PathBuf::from(format!(
+ "{}{}",
+ path_no_ext,
+ media_type.as_ts_extension()
+ )),
+ reason,
+ )
+ })
+ .collect::<Vec<_>>()
+ }
+
+ if specifier.scheme() != "file" {
+ return None;
+ }
+
+ let path = url_to_file_path(specifier).ok()?;
+
+ #[derive(Clone, Copy)]
+ enum SloppyImportsResolutionReason {
+ JsToTs,
+ NoExtension,
+ Directory,
+ }
+
+ let probe_paths: Vec<(PathBuf, SloppyImportsResolutionReason)> =
+ match self.fs.stat_sync(&path) {
+ Some(SloppyImportsFsEntry::File) => {
+ if mode.is_types() {
+ let media_type = MediaType::from_specifier(specifier);
+ // attempt to resolve the .d.ts file before the .js file
+ let probe_media_type_types = match media_type {
+ MediaType::JavaScript => {
+ vec![(MediaType::Dts), MediaType::JavaScript]
+ }
+ MediaType::Mjs => {
+ vec![MediaType::Dmts, MediaType::Dts, MediaType::Mjs]
+ }
+ MediaType::Cjs => {
+ vec![MediaType::Dcts, MediaType::Dts, MediaType::Cjs]
+ }
+ _ => return None,
+ };
+ let path_no_ext = path_without_ext(&path, media_type)?;
+ media_types_to_paths(
+ &path_no_ext,
+ media_type,
+ probe_media_type_types,
+ SloppyImportsResolutionReason::JsToTs,
+ )
+ } else {
+ return None;
+ }
+ }
+ entry @ None | entry @ Some(SloppyImportsFsEntry::Dir) => {
+ let media_type = MediaType::from_specifier(specifier);
+ let probe_media_type_types = match media_type {
+ MediaType::JavaScript => (
+ if mode.is_types() {
+ vec![MediaType::TypeScript, MediaType::Tsx, MediaType::Dts]
+ } else {
+ vec![MediaType::TypeScript, MediaType::Tsx]
+ },
+ SloppyImportsResolutionReason::JsToTs,
+ ),
+ MediaType::Jsx => {
+ (vec![MediaType::Tsx], SloppyImportsResolutionReason::JsToTs)
+ }
+ MediaType::Mjs => (
+ if mode.is_types() {
+ vec![MediaType::Mts, MediaType::Dmts, MediaType::Dts]
+ } else {
+ vec![MediaType::Mts]
+ },
+ SloppyImportsResolutionReason::JsToTs,
+ ),
+ MediaType::Cjs => (
+ if mode.is_types() {
+ vec![MediaType::Cts, MediaType::Dcts, MediaType::Dts]
+ } else {
+ vec![MediaType::Cts]
+ },
+ SloppyImportsResolutionReason::JsToTs,
+ ),
+ MediaType::TypeScript
+ | MediaType::Mts
+ | MediaType::Cts
+ | MediaType::Dts
+ | MediaType::Dmts
+ | MediaType::Dcts
+ | MediaType::Tsx
+ | MediaType::Json
+ | MediaType::Wasm
+ | MediaType::TsBuildInfo
+ | MediaType::SourceMap => {
+ return None;
+ }
+ MediaType::Unknown => (
+ if mode.is_types() {
+ vec![
+ MediaType::TypeScript,
+ MediaType::Tsx,
+ MediaType::Mts,
+ MediaType::Dts,
+ MediaType::Dmts,
+ MediaType::Dcts,
+ MediaType::JavaScript,
+ MediaType::Jsx,
+ MediaType::Mjs,
+ ]
+ } else {
+ vec![
+ MediaType::TypeScript,
+ MediaType::JavaScript,
+ MediaType::Tsx,
+ MediaType::Jsx,
+ MediaType::Mts,
+ MediaType::Mjs,
+ ]
+ },
+ SloppyImportsResolutionReason::NoExtension,
+ ),
+ };
+ let mut probe_paths = match path_without_ext(&path, media_type) {
+ Some(path_no_ext) => media_types_to_paths(
+ &path_no_ext,
+ media_type,
+ probe_media_type_types.0,
+ probe_media_type_types.1,
+ ),
+ None => vec![],
+ };
+
+ if matches!(entry, Some(SloppyImportsFsEntry::Dir)) {
+ // try to resolve at the index file
+ if mode.is_types() {
+ probe_paths.push((
+ path.join("index.ts"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+
+ probe_paths.push((
+ path.join("index.mts"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.d.ts"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.d.mts"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.js"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.mjs"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.tsx"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.jsx"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ } else {
+ probe_paths.push((
+ path.join("index.ts"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.mts"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.tsx"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.js"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.mjs"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ probe_paths.push((
+ path.join("index.jsx"),
+ SloppyImportsResolutionReason::Directory,
+ ));
+ }
+ }
+ if probe_paths.is_empty() {
+ return None;
+ }
+ probe_paths
+ }
+ };
+
+ for (probe_path, reason) in probe_paths {
+ if self.fs.is_file(&probe_path) {
+ if let Ok(specifier) = Url::from_file_path(probe_path) {
+ match reason {
+ SloppyImportsResolutionReason::JsToTs => {
+ return Some(SloppyImportsResolution::JsToTs(specifier));
+ }
+ SloppyImportsResolutionReason::NoExtension => {
+ return Some(SloppyImportsResolution::NoExtension(specifier));
+ }
+ SloppyImportsResolutionReason::Directory => {
+ return Some(SloppyImportsResolution::Directory(specifier));
+ }
+ }
+ }
+ }
+ }
+
+ None
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use test_util::TestContext;
+
+ use super::*;
+
+ #[test]
+ fn test_unstable_sloppy_imports() {
+ fn resolve(specifier: &Url) -> Option<SloppyImportsResolution> {
+ resolve_with_mode(specifier, SloppyImportsResolutionMode::Execution)
+ }
+
+ fn resolve_types(specifier: &Url) -> Option<SloppyImportsResolution> {
+ resolve_with_mode(specifier, SloppyImportsResolutionMode::Types)
+ }
+
+ fn resolve_with_mode(
+ specifier: &Url,
+ mode: SloppyImportsResolutionMode,
+ ) -> Option<SloppyImportsResolution> {
+ struct RealSloppyImportsResolverFs;
+ impl SloppyImportResolverFs for RealSloppyImportsResolverFs {
+ fn stat_sync(&self, path: &Path) -> Option<SloppyImportsFsEntry> {
+ let stat = std::fs::metadata(path).ok()?;
+ if stat.is_dir() {
+ Some(SloppyImportsFsEntry::Dir)
+ } else if stat.is_file() {
+ Some(SloppyImportsFsEntry::File)
+ } else {
+ None
+ }
+ }
+ }
+
+ SloppyImportsResolver::new(RealSloppyImportsResolverFs)
+ .resolve(specifier, mode)
+ }
+
+ let context = TestContext::default();
+ let temp_dir = context.temp_dir().path();
+
+ // scenarios like resolving ./example.js to ./example.ts
+ for (ext_from, ext_to) in [("js", "ts"), ("js", "tsx"), ("mjs", "mts")] {
+ let ts_file = temp_dir.join(format!("file.{}", ext_to));
+ ts_file.write("");
+ assert_eq!(resolve(&ts_file.url_file()), None);
+ assert_eq!(
+ resolve(
+ &temp_dir
+ .url_dir()
+ .join(&format!("file.{}", ext_from))
+ .unwrap()
+ ),
+ Some(SloppyImportsResolution::JsToTs(ts_file.url_file())),
+ );
+ ts_file.remove_file();
+ }
+
+ // no extension scenarios
+ for ext in ["js", "ts", "js", "tsx", "jsx", "mjs", "mts"] {
+ let file = temp_dir.join(format!("file.{}", ext));
+ file.write("");
+ assert_eq!(
+ resolve(
+ &temp_dir
+ .url_dir()
+ .join("file") // no ext
+ .unwrap()
+ ),
+ Some(SloppyImportsResolution::NoExtension(file.url_file()))
+ );
+ file.remove_file();
+ }
+
+ // .ts and .js exists, .js specified (goes to specified)
+ {
+ let ts_file = temp_dir.join("file.ts");
+ ts_file.write("");
+ let js_file = temp_dir.join("file.js");
+ js_file.write("");
+ assert_eq!(resolve(&js_file.url_file()), None);
+ }
+
+ // only js exists, .js specified
+ {
+ let js_only_file = temp_dir.join("js_only.js");
+ js_only_file.write("");
+ assert_eq!(resolve(&js_only_file.url_file()), None);
+ assert_eq!(resolve_types(&js_only_file.url_file()), None);
+ }
+
+ // resolving a directory to an index file
+ {
+ let routes_dir = temp_dir.join("routes");
+ routes_dir.create_dir_all();
+ let index_file = routes_dir.join("index.ts");
+ index_file.write("");
+ assert_eq!(
+ resolve(&routes_dir.url_file()),
+ Some(SloppyImportsResolution::Directory(index_file.url_file())),
+ );
+ }
+
+ // both a directory and a file with specifier is present
+ {
+ let api_dir = temp_dir.join("api");
+ api_dir.create_dir_all();
+ let bar_file = api_dir.join("bar.ts");
+ bar_file.write("");
+ let api_file = temp_dir.join("api.ts");
+ api_file.write("");
+ assert_eq!(
+ resolve(&api_dir.url_file()),
+ Some(SloppyImportsResolution::NoExtension(api_file.url_file())),
+ );
+ }
+ }
+
+ #[test]
+ fn test_sloppy_import_resolution_suggestion_message() {
+ // directory
+ assert_eq!(
+ SloppyImportsResolution::Directory(
+ Url::parse("file:///dir/index.js").unwrap()
+ )
+ .as_suggestion_message(),
+ "Maybe specify path to 'index.js' file in directory instead"
+ );
+ // no ext
+ assert_eq!(
+ SloppyImportsResolution::NoExtension(
+ Url::parse("file:///dir/index.mjs").unwrap()
+ )
+ .as_suggestion_message(),
+ "Maybe add a '.mjs' extension"
+ );
+ // js to ts
+ assert_eq!(
+ SloppyImportsResolution::JsToTs(
+ Url::parse("file:///dir/index.mts").unwrap()
+ )
+ .as_suggestion_message(),
+ "Maybe change the extension to '.mts'"
+ );
+ }
+}
diff --git a/ext/node_resolver/Cargo.toml b/resolvers/node/Cargo.toml
index 104204569..104204569 100644
--- a/ext/node_resolver/Cargo.toml
+++ b/resolvers/node/Cargo.toml
diff --git a/ext/node_resolver/README.md b/resolvers/node/README.md
index 8f2f63ca1..8f2f63ca1 100644
--- a/ext/node_resolver/README.md
+++ b/resolvers/node/README.md
diff --git a/ext/node_resolver/analyze.rs b/resolvers/node/analyze.rs
index deb56d064..deb56d064 100644
--- a/ext/node_resolver/analyze.rs
+++ b/resolvers/node/analyze.rs
diff --git a/ext/node_resolver/clippy.toml b/resolvers/node/clippy.toml
index 86150781b..86150781b 100644
--- a/ext/node_resolver/clippy.toml
+++ b/resolvers/node/clippy.toml
diff --git a/ext/node_resolver/env.rs b/resolvers/node/env.rs
index b520ece0f..b520ece0f 100644
--- a/ext/node_resolver/env.rs
+++ b/resolvers/node/env.rs
diff --git a/ext/node_resolver/errors.rs b/resolvers/node/errors.rs
index 4ba829eda..4ba829eda 100644
--- a/ext/node_resolver/errors.rs
+++ b/resolvers/node/errors.rs
diff --git a/ext/node_resolver/lib.rs b/resolvers/node/lib.rs
index f03f77048..f03f77048 100644
--- a/ext/node_resolver/lib.rs
+++ b/resolvers/node/lib.rs
diff --git a/ext/node_resolver/npm.rs b/resolvers/node/npm.rs
index 77df57c48..77df57c48 100644
--- a/ext/node_resolver/npm.rs
+++ b/resolvers/node/npm.rs
diff --git a/ext/node_resolver/package_json.rs b/resolvers/node/package_json.rs
index de750f1d7..de750f1d7 100644
--- a/ext/node_resolver/package_json.rs
+++ b/resolvers/node/package_json.rs
diff --git a/ext/node_resolver/path.rs b/resolvers/node/path.rs
index ece270cd9..ece270cd9 100644
--- a/ext/node_resolver/path.rs
+++ b/resolvers/node/path.rs
diff --git a/ext/node_resolver/resolution.rs b/resolvers/node/resolution.rs
index ad9dbb710..ad9dbb710 100644
--- a/ext/node_resolver/resolution.rs
+++ b/resolvers/node/resolution.rs
diff --git a/ext/node_resolver/sync.rs b/resolvers/node/sync.rs
index 3c4729aa2..3c4729aa2 100644
--- a/ext/node_resolver/sync.rs
+++ b/resolvers/node/sync.rs