diff --git a/Cargo.lock b/Cargo.lock index b1550a0448..8de6a2fdbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4752,6 +4752,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-tungstenite", + "url", "winapi 0.3.9", ] diff --git a/cli/build.rs b/cli/build.rs index 98d044a3e9..b72153871f 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -201,6 +201,11 @@ fn create_compiler_snapshot( false } + #[op] + fn op_is_node_file() -> bool { + false + } + #[op] fn op_script_version( _state: &mut OpState, @@ -266,6 +271,7 @@ fn create_compiler_snapshot( op_build_info::decl(), op_cwd::decl(), op_exists::decl(), + op_is_node_file::decl(), op_load::decl(), op_script_version::decl(), ]) diff --git a/cli/dts/README.md b/cli/dts/README.md index 9d07188f7d..14d937c88d 100644 --- a/cli/dts/README.md +++ b/cli/dts/README.md @@ -4,16 +4,26 @@ The files in this directory are mostly from the TypeScript repository. We currently (unfortunately) have a rather manual process for upgrading TypeScript. It works like this currently: -1. Checkout typescript repo in a separate directory. -2. Copy typescript.js into Deno repo. -3. Copy d.ts files into dts directory. +1. Checkout denoland/TypeScript repo in a separate directory. +1. Add Microsoft/TypeScript as a remote and fetch its latest tags +1. Checkout a new branch based on this tag. +1. Cherry pick the custom commit we made in a previous release to the new one. +1. This commit has a "deno.ts" file in it. Read the instructions in it. +1. Copy typescript.js into Deno repo. +1. Copy d.ts files into dts directory. So that might look something like this: ``` -git clone https://github.com/microsoft/TypeScript.git +git clone https://github.com/denoland/TypeScript.git cd typescript +git remote add upstream https://github.com/Microsoft/TypeScript +git fetch upstream git checkout v3.9.7 +git checkout -b branch_v3.9.7 +git cherry pick <previous-release-branch-commit-we-did> +npm install +gulp local rsync lib/typescript.js ~/src/deno/cli/tsc/00_typescript.js rsync --exclude=protocol.d.ts --exclude=tsserverlibrary.d.ts --exclude=typescriptServices.d.ts lib/*.d.ts ~/src/deno/cli/dts/ ``` diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index aef5ae8898..aac6e58627 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -458,6 +458,13 @@ async fn generate_lint_diagnostics( break; } + // ignore any npm package files + if let Some(npm_resolver) = &snapshot.maybe_npm_resolver { + if npm_resolver.in_npm_package(document.specifier()) { + continue; + } + } + let version = document.maybe_lsp_version(); diagnostics_vec.push(( document.specifier().clone(), @@ -597,6 +604,8 @@ pub enum DenoDiagnostic { NoCacheBlob, /// A data module was not found in the cache. NoCacheData(ModuleSpecifier), + /// A remote npm package reference was not found in the cache. + NoCacheNpm(NpmPackageReference, ModuleSpecifier), /// A local module was not found on the local file system. NoLocal(ModuleSpecifier), /// The specifier resolved to a remote specifier that was redirected to @@ -622,6 +631,7 @@ impl DenoDiagnostic { Self::NoCache(_) => "no-cache", Self::NoCacheBlob => "no-cache-blob", Self::NoCacheData(_) => "no-cache-data", + Self::NoCacheNpm(_, _) => "no-cache-npm", Self::NoLocal(_) => "no-local", Self::Redirect { .. } => "redirect", Self::ResolutionError(err) => match err { @@ -690,16 +700,17 @@ impl DenoDiagnostic { }), ..Default::default() }, - "no-cache" | "no-cache-data" => { + "no-cache" | "no-cache-data" | "no-cache-npm" => { let data = diagnostic .data .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let data: DiagnosticDataSpecifier = serde_json::from_value(data)?; - let title = if code == "no-cache" { - format!("Cache \"{}\" and its dependencies.", data.specifier) - } else { - "Cache the data URL and its dependencies.".to_string() + let title = match code.as_str() { + "no-cache" | "no-cache-npm" => { + format!("Cache \"{}\" and its dependencies.", data.specifier) + } + _ => "Cache the data URL and its dependencies.".to_string(), }; lsp::CodeAction { title, @@ -757,6 +768,7 @@ impl DenoDiagnostic { code.as_str(), "import-map-remap" | "no-cache" + | "no-cache-npm" | "no-cache-data" | "no-assert-type" | "redirect" @@ -777,6 +789,7 @@ impl DenoDiagnostic { Self::NoCache(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Uncached or missing remote URL: \"{}\".", specifier), Some(json!({ "specifier": specifier }))), Self::NoCacheBlob => (lsp::DiagnosticSeverity::ERROR, "Uncached blob URL.".to_string(), None), Self::NoCacheData(specifier) => (lsp::DiagnosticSeverity::ERROR, "Uncached data URL.".to_string(), Some(json!({ "specifier": specifier }))), + Self::NoCacheNpm(pkg_ref, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Uncached or missing npm package: \"{}\".", pkg_ref.req), Some(json!({ "specifier": specifier }))), Self::NoLocal(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier), None), Self::Redirect { from, to} => (lsp::DiagnosticSeverity::INFORMATION, format!("The import of \"{}\" was redirected to \"{}\".", from, to), Some(json!({ "specifier": from, "redirect": to }))), Self::ResolutionError(err) => (lsp::DiagnosticSeverity::ERROR, err.to_string(), None), @@ -847,8 +860,20 @@ fn diagnose_resolved( .push(DenoDiagnostic::NoAssertType.to_lsp_diagnostic(&range)), } } - } else if NpmPackageReference::from_specifier(specifier).is_ok() { - // ignore npm specifiers for now + } else if let Ok(pkg_ref) = NpmPackageReference::from_specifier(specifier) + { + if let Some(npm_resolver) = &snapshot.maybe_npm_resolver { + // show diagnostics for npm package references that aren't cached + if npm_resolver + .resolve_package_folder_from_deno_module(&pkg_ref.req) + .is_err() + { + diagnostics.push( + DenoDiagnostic::NoCacheNpm(pkg_ref, specifier.clone()) + .to_lsp_diagnostic(&range), + ); + } + } } else { // When the document is not available, it means that it cannot be found // in the cache or locally on the disk, so we want to issue a diagnostic @@ -882,6 +907,12 @@ fn diagnose_dependency( dependency_key: &str, dependency: &deno_graph::Dependency, ) { + if let Some(npm_resolver) = &snapshot.maybe_npm_resolver { + if npm_resolver.in_npm_package(referrer) { + return; // ignore, surface typescript errors instead + } + } + if let Some(import_map) = &snapshot.maybe_import_map { if let Resolved::Ok { specifier, range, .. @@ -938,8 +969,8 @@ async fn generate_deno_diagnostics( &mut diagnostics, snapshot, specifier, - &dependency_key, - &dependency, + dependency_key, + dependency, ); } } diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 61db899e59..57216eecbc 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -12,6 +12,13 @@ use crate::file_fetcher::SUPPORTED_SCHEMES; use crate::fs_util::specifier_to_file_path; use crate::http_cache; use crate::http_cache::HttpCache; +use crate::node; +use crate::node::node_resolve_npm_reference; +use crate::node::NodeResolution; +use crate::node::NodeResolutionMode; +use crate::npm::NpmPackageReference; +use crate::npm::NpmPackageReq; +use crate::npm::NpmPackageResolver; use crate::resolver::ImportMapResolver; use crate::resolver::JsxResolver; use crate::text_encoding; @@ -209,6 +216,29 @@ impl AssetOrDocument { } } +#[derive(Debug, Default)] +struct DocumentDependencies { + deps: BTreeMap<String, deno_graph::Dependency>, + maybe_types_dependency: Option<(String, Resolved)>, +} + +impl DocumentDependencies { + pub fn from_maybe_module(maybe_module: &MaybeModuleResult) -> Self { + if let Some(Ok(module)) = &maybe_module { + Self::from_module(module) + } else { + Self::default() + } + } + + pub fn from_module(module: &deno_graph::Module) -> Self { + Self { + deps: module.dependencies.clone(), + maybe_types_dependency: module.maybe_types_dependency.clone(), + } + } +} + type MaybeModuleResult = Option<Result<deno_graph::Module, deno_graph::ModuleGraphError>>; type MaybeParsedSourceResult = @@ -217,7 +247,7 @@ type MaybeParsedSourceResult = #[derive(Debug, Clone)] struct DocumentInner { /// contains the last-known-good set of dependencies from parsing the module - dependencies: Arc<BTreeMap<String, deno_graph::Dependency>>, + dependencies: Arc<DocumentDependencies>, fs_version: String, line_index: Arc<LineIndex>, maybe_language_id: Option<LanguageId>, @@ -249,12 +279,9 @@ impl Document { maybe_headers, maybe_resolver, ); - let dependencies = if let Some(Ok(module)) = &maybe_module { - Arc::new(module.dependencies.clone()) - } else { - Arc::new(BTreeMap::new()) - }; - // todo(dsherret): retrieve this from the parsed source if it + let dependencies = + Arc::new(DocumentDependencies::from_maybe_module(&maybe_module)); + // todo(dsherret): retrieve this from the parsed source if it exists let text_info = SourceTextInfo::new(content); let line_index = Arc::new(LineIndex::new(text_info.text_str())); Self(Arc::new(DocumentInner { @@ -289,11 +316,8 @@ impl Document { } else { (None, None) }; - let dependencies = if let Some(Ok(module)) = &maybe_module { - Arc::new(module.dependencies.clone()) - } else { - Arc::new(BTreeMap::new()) - }; + let dependencies = + Arc::new(DocumentDependencies::from_maybe_module(&maybe_module)); let source = SourceTextInfo::new(content); let line_index = Arc::new(LineIndex::new(source.text_str())); Self(Arc::new(DocumentInner { @@ -355,9 +379,9 @@ impl Document { (None, None) }; let dependencies = if let Some(Ok(module)) = &maybe_module { - Arc::new(module.dependencies.clone()) + Arc::new(DocumentDependencies::from_module(module)) } else { - self.0.dependencies.clone() + self.0.dependencies.clone() // use the last known good }; let text_info = SourceTextInfo::new(content); let line_index = if index_valid == IndexValid::All { @@ -435,15 +459,9 @@ impl Document { } pub fn maybe_types_dependency(&self) -> deno_graph::Resolved { - let module_result = match self.0.maybe_module.as_ref() { - Some(module_result) => module_result, - _ => return deno_graph::Resolved::None, - }; - let module = match module_result.as_ref() { - Ok(module) => module, - Err(_) => return deno_graph::Resolved::None, - }; - if let Some((_, maybe_dep)) = module.maybe_types_dependency.as_ref() { + if let Some((_, maybe_dep)) = + self.0.dependencies.maybe_types_dependency.as_ref() + { maybe_dep.clone() } else { deno_graph::Resolved::None @@ -479,13 +497,8 @@ impl Document { self.0.maybe_navigation_tree.clone() } - pub fn dependencies(&self) -> Vec<(String, deno_graph::Dependency)> { - self - .0 - .dependencies - .iter() - .map(|(s, d)| (s.clone(), d.clone())) - .collect() + pub fn dependencies(&self) -> &BTreeMap<String, deno_graph::Dependency> { + &self.0.dependencies.deps } /// If the supplied position is within a dependency range, return the resolved @@ -698,6 +711,8 @@ pub struct Documents { maybe_import_map: Option<ImportMapResolver>, /// The optional JSX resolver, which is used when JSX imports are configured. maybe_jsx_resolver: Option<JsxResolver>, + /// The npm package requirements. + npm_reqs: HashSet<NpmPackageReq>, /// Resolves a specifier to its final redirected to specifier. specifier_resolver: Arc<SpecifierResolver>, } @@ -713,6 +728,7 @@ impl Documents { imports: Default::default(), maybe_import_map: None, maybe_jsx_resolver: None, + npm_reqs: HashSet::new(), specifier_resolver: Arc::new(SpecifierResolver::new(location)), } } @@ -847,6 +863,12 @@ impl Documents { } } + /// Returns a collection of npm package requirements. + pub fn npm_package_reqs(&mut self) -> HashSet<NpmPackageReq> { + self.calculate_dependents_if_dirty(); + self.npm_reqs.clone() + } + /// Return a document for the specifier. pub fn get(&self, original_specifier: &ModuleSpecifier) -> Option<Document> { let specifier = self.specifier_resolver.resolve(original_specifier)?; @@ -921,10 +943,28 @@ impl Documents { &self, specifiers: Vec<String>, referrer: &ModuleSpecifier, + maybe_npm_resolver: Option<&NpmPackageResolver>, ) -> Option<Vec<Option<(ModuleSpecifier, MediaType)>>> { let dependencies = self.get(referrer)?.0.dependencies.clone(); let mut results = Vec::new(); for specifier in specifiers { + if let Some(npm_resolver) = maybe_npm_resolver { + if npm_resolver.in_npm_package(referrer) { + // we're in an npm package, so use node resolution + results.push(Some(NodeResolution::into_specifier_and_media_type( + node::node_resolve( + &specifier, + referrer, + node::NodeResolutionMode::Types, + npm_resolver, + ) + .ok() + .flatten(), + ))); + continue; + } + } + // handle npm:<package> urls if specifier.starts_with("asset:") { if let Ok(specifier) = ModuleSpecifier::parse(&specifier) { let media_type = MediaType::from(&specifier); @@ -932,11 +972,11 @@ impl Documents { } else { results.push(None); } - } else if let Some(dep) = dependencies.get(&specifier) { + } else if let Some(dep) = dependencies.deps.get(&specifier) { if let Resolved::Ok { specifier, .. } = &dep.maybe_type { - results.push(self.resolve_dependency(specifier)); + results.push(self.resolve_dependency(specifier, maybe_npm_resolver)); } else if let Resolved::Ok { specifier, .. } = &dep.maybe_code { - results.push(self.resolve_dependency(specifier)); + results.push(self.resolve_dependency(specifier, maybe_npm_resolver)); } else { results.push(None); } @@ -945,7 +985,19 @@ impl Documents { { // clone here to avoid double borrow of self let specifier = specifier.clone(); - results.push(self.resolve_dependency(&specifier)); + results.push(self.resolve_dependency(&specifier, maybe_npm_resolver)); + } else if let Ok(npm_ref) = NpmPackageReference::from_str(&specifier) { + results.push(maybe_npm_resolver.map(|npm_resolver| { + NodeResolution::into_specifier_and_media_type( + node_resolve_npm_reference( + &npm_ref, + NodeResolutionMode::Types, + npm_resolver, + ) + .ok() + .flatten(), + ) + })); } else { results.push(None); } @@ -1038,32 +1090,36 @@ impl Documents { // favour documents that are open in case a document exists in both collections let documents = file_system_docs.docs.iter().chain(self.open_docs.iter()); for (specifier, doc) in documents { - if let Some(Ok(module)) = doc.maybe_module() { - for dependency in module.dependencies.values() { - if let Some(dep) = dependency.get_code() { - dependents_map - .entry(dep.clone()) - .or_default() - .insert(specifier.clone()); - } - if let Some(dep) = dependency.get_type() { - dependents_map - .entry(dep.clone()) - .or_default() - .insert(specifier.clone()); - } + for dependency in doc.dependencies().values() { + if let Some(dep) = dependency.get_code() { + dependents_map + .entry(dep.clone()) + .or_default() + .insert(specifier.clone()); } - if let Some((_, Resolved::Ok { specifier: dep, .. })) = - &module.maybe_types_dependency - { + if let Some(dep) = dependency.get_type() { dependents_map .entry(dep.clone()) .or_default() .insert(specifier.clone()); } } + if let Resolved::Ok { specifier: dep, .. } = doc.maybe_types_dependency() + { + dependents_map + .entry(dep.clone()) + .or_default() + .insert(specifier.clone()); + } + } + let mut npm_reqs = HashSet::new(); + for specifier in dependents_map.keys() { + if let Ok(reference) = NpmPackageReference::from_specifier(specifier) { + npm_reqs.insert(reference.req); + } } self.dependents_map = Arc::new(dependents_map); + self.npm_reqs = npm_reqs; self.dirty = false; file_system_docs.dirty = false; } @@ -1079,7 +1135,21 @@ impl Documents { fn resolve_dependency( &self, specifier: &ModuleSpecifier, + maybe_npm_resolver: Option<&NpmPackageResolver>, ) -> Option<(ModuleSpecifier, MediaType)> { + if let Ok(npm_ref) = NpmPackageReference::from_specifier(specifier) { + return maybe_npm_resolver.map(|npm_resolver| { + NodeResolution::into_specifier_and_media_type( + node_resolve_npm_reference( + &npm_ref, + NodeResolutionMode::Types, + npm_resolver, + ) + .ok() + .flatten(), + ) + }); + } let doc = self.get(specifier)?; let maybe_module = doc.maybe_module().and_then(|r| r.as_ref().ok()); let maybe_types_dependency = maybe_module.and_then(|m| { @@ -1088,7 +1158,7 @@ impl Documents { .map(|(_, resolved)| resolved.clone()) }); if let Some(Resolved::Ok { specifier, .. }) = maybe_types_dependency { - self.resolve_dependency(&specifier) + self.resolve_dependency(&specifier, maybe_npm_resolver) } else { let media_type = doc.media_type(); Some((specifier.clone(), media_type)) @@ -1113,12 +1183,12 @@ impl Documents { } /// Loader that will look at the open documents. -pub struct DocumentsDenoGraphLoader<'a> { +pub struct OpenDocumentsGraphLoader<'a> { pub inner_loader: &'a mut dyn deno_graph::source::Loader, pub open_docs: &'a HashMap<ModuleSpecifier, Document>, } -impl<'a> deno_graph::source::Loader for DocumentsDenoGraphLoader<'a> { +impl<'a> deno_graph::source::Loader for OpenDocumentsGraphLoader<'a> { fn load( &mut self, specifier: &ModuleSpecifier, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 27d69127c4..a3f516615a 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -66,10 +66,15 @@ use crate::args::LintConfig; use crate::args::TsConfig; use crate::deno_dir; use crate::file_fetcher::get_source_from_data_url; +use crate::file_fetcher::CacheSetting; use crate::fs_util; use crate::graph_util::graph_valid; +use crate::npm::NpmCache; +use crate::npm::NpmPackageResolver; +use crate::npm::NpmRegistryApi; use crate::proc_state::import_map_from_text; use crate::proc_state::ProcState; +use crate::progress_bar::ProgressBar; use crate::tools::fmt::format_file; use crate::tools::fmt::format_parsed_source; @@ -87,6 +92,7 @@ pub struct StateSnapshot { pub documents: Documents, pub maybe_import_map: Option<Arc<ImportMap>>, pub root_uri: Option<Url>, + pub maybe_npm_resolver: Option<NpmPackageResolver>, } #[derive(Debug)] @@ -125,6 +131,8 @@ pub struct Inner { pub maybe_lint_config: Option<LintConfig>, /// A lazily create "server" for handling test run requests. maybe_testing_server: Option<testing::TestServer>, + /// Resolver for npm packages. + npm_resolver: NpmPackageResolver, /// A collection of measurements which instrument that performance of the LSP. performance: Arc<Performance>, /// A memoized version of fixable diagnostic codes retrieved from TypeScript. @@ -250,6 +258,26 @@ impl Inner { ts_server.clone(), ); let assets = Assets::new(ts_server.clone()); + let registry_url = NpmRegistryApi::default_url(); + // Use an "only" cache setting in order to make the + // user do an explicit "cache" command and prevent + // the cache from being filled with lots of packages while + // the user is typing. + let cache_setting = CacheSetting::Only; + let progress_bar = ProgressBar::default(); + let npm_cache = NpmCache::from_deno_dir( + &dir, + cache_setting.clone(), + progress_bar.clone(), + ); + let api = NpmRegistryApi::new( + registry_url, + npm_cache.clone(), + cache_setting, + progress_bar, + ); + let npm_resolver = + NpmPackageResolver::new(npm_cache, api, true, false, None); Self { assets, @@ -267,6 +295,7 @@ impl Inner { maybe_testing_server: None, module_registries, module_registries_location, + npm_resolver, performance, ts_fixable_diagnostics: Default::default(), ts_server, @@ -435,6 +464,7 @@ impl Inner { cache_metadata: self.cache_metadata.clone(), documents: self.documents.clone(), maybe_import_map: self.maybe_import_map.clone(), + maybe_npm_resolver: Some(self.npm_resolver.snapshotted()), root_uri: self.config.root_uri.clone(), }) } @@ -828,7 +858,7 @@ impl Inner { if let Err(err) = self.client.register_capability(vec![registration]).await { - warn!("Client errored on capabilities.\n{}", err); + warn!("Client errored on capabilities.\n{:#}", err); } } self.config.update_enabled_paths(self.client.clone()).await; @@ -891,6 +921,7 @@ impl Inner { ) { Ok(document) => { if document.is_diagnosable() { + self.refresh_npm_specifiers().await; self .diagnostics_server .invalidate(&self.documents.dependents(&specifier)); @@ -903,6 +934,13 @@ impl Inner { self.performance.measure(mark); } + async fn refresh_npm_specifiers(&mut self) { + let package_reqs = self.documents.npm_package_reqs(); + if let Err(err) = self.npm_resolver.set_package_reqs(package_reqs).await { + warn!("Could not set npm package requirements. {:#}", err); + } + } + async fn did_close(&mut self, params: DidCloseTextDocumentParams) { let mark = self.performance.mark("did_close", Some(¶ms)); if params.text_document.uri.scheme() == "deno" { @@ -917,6 +955,7 @@ impl Inner { error!("{}", err); } if self.is_diagnosable(&specifier) { + self.refresh_npm_specifiers().await; let mut specifiers = self.documents.dependents(&specifier); specifiers.push(specifier.clone()); self.diagnostics_server.invalidate(&specifiers); @@ -1135,7 +1174,7 @@ impl Inner { Ok(None) => Some(Vec::new()), Err(err) => { // TODO(lucacasonato): handle error properly - warn!("Format error: {}", err); + warn!("Format error: {:#}", err); None } } @@ -2476,6 +2515,7 @@ impl tower_lsp::LanguageServer for LanguageServer { let has_specifier_settings = inner.config.has_specifier_settings(&specifier); if document.is_diagnosable() { + inner.refresh_npm_specifiers().await; let specifiers = inner.documents.dependents(&specifier); inner.diagnostics_server.invalidate(&specifiers); // don't send diagnostics yet if we don't have the specifier settings @@ -2834,7 +2874,7 @@ impl Inner { .collect::<HashMap<_, _>>(); let ps = ProcState::from_options(Arc::new(cli_options)).await?; let mut inner_loader = ps.create_graph_loader(); - let mut loader = crate::lsp::documents::DocumentsDenoGraphLoader { + let mut loader = crate::lsp::documents::OpenDocumentsGraphLoader { inner_loader: &mut inner_loader, open_docs: &open_docs, }; @@ -2870,6 +2910,9 @@ impl Inner { ca_stores: None, ca_file: None, unsafely_ignore_certificate_errors: None, + // this is to allow loading npm specifiers, so we can remove this + // once stabilizing them + unstable: true, ..Default::default() }, self.maybe_config_file.clone(), @@ -2892,6 +2935,7 @@ impl Inner { // For that we're invalidating all the existing diagnostics and restarting // the language server for TypeScript (as it might hold to some stale // documents). + self.refresh_npm_specifiers().await; self.diagnostics_server.invalidate_all(); let _: bool = self .ts_server diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 6c21369903..bed06e0882 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -2678,6 +2678,20 @@ fn op_is_cancelled(state: &mut OpState) -> bool { state.token.is_cancelled() } +#[op] +fn op_is_node_file(state: &mut OpState, path: String) -> bool { + let state = state.borrow::<State>(); + match ModuleSpecifier::parse(&path) { + Ok(specifier) => state + .state_snapshot + .maybe_npm_resolver + .as_ref() + .map(|r| r.in_npm_package(&specifier)) + .unwrap_or(false), + Err(_) => false, + } +} + #[op] fn op_load( state: &mut OpState, @@ -2692,7 +2706,7 @@ fn op_load( Some(doc) => { json!({ "data": doc.text(), - "scriptKind": crate::tsc::as_ts_script_kind(&doc.media_type()), + "scriptKind": crate::tsc::as_ts_script_kind(doc.media_type()), "version": state.script_version(&specifier), }) } @@ -2709,11 +2723,11 @@ fn op_resolve( let mark = state.performance.mark("op_resolve", Some(&args)); let referrer = state.normalize_specifier(&args.base)?; - let result = if let Some(resolved) = state - .state_snapshot - .documents - .resolve(args.specifiers, &referrer) - { + let result = if let Some(resolved) = state.state_snapshot.documents.resolve( + args.specifiers, + &referrer, + state.state_snapshot.maybe_npm_resolver.as_ref(), + ) { Ok( resolved .into_iter() @@ -2789,6 +2803,7 @@ fn init_extension(performance: Arc<Performance>) -> Extension { .ops(vec![ op_exists::decl(), op_is_cancelled::decl(), + op_is_node_file::decl(), op_load::decl(), op_resolve::decl(), op_respond::decl(), diff --git a/cli/main.rs b/cli/main.rs index f192bea5e4..3a4cc3c37e 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -525,6 +525,7 @@ async fn create_graph_and_maybe_check( &graph.roots, Arc::new(RwLock::new(graph.as_ref().into())), &cache, + ps.npm_resolver.clone(), check::CheckOptions { type_check_mode: ps.options.type_check_mode(), debug, diff --git a/cli/node/mod.rs b/cli/node/mod.rs index 97240cfe7b..56f228d3f7 100644 --- a/cli/node/mod.rs +++ b/cli/node/mod.rs @@ -31,6 +31,7 @@ use deno_runtime::deno_node::PathClean; use deno_runtime::deno_node::RequireNpmResolver; use deno_runtime::deno_node::DEFAULT_CONDITIONS; use deno_runtime::deno_node::NODE_GLOBAL_THIS_NAME; +use deno_runtime::deno_node::TYPES_CONDITIONS; use once_cell::sync::Lazy; use regex::Regex; @@ -55,9 +56,61 @@ impl NodeResolution { match self { Self::Esm(u) => u, Self::CommonJs(u) => u, - _ => unreachable!(), + Self::BuiltIn(specifier) => { + if specifier.starts_with("node:") { + ModuleSpecifier::parse(&specifier).unwrap() + } else { + ModuleSpecifier::parse(&format!("node:{}", specifier)).unwrap() + } + } } } + + pub fn into_specifier_and_media_type( + resolution: Option<Self>, + ) -> (ModuleSpecifier, MediaType) { + match resolution { + Some(NodeResolution::CommonJs(specifier)) => { + let media_type = MediaType::from(&specifier); + ( + specifier, + match media_type { + MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs, + MediaType::TypeScript | MediaType::Tsx => MediaType::Cts, + MediaType::Dts => MediaType::Dcts, + _ => media_type, + }, + ) + } + Some(NodeResolution::Esm(specifier)) => { + let media_type = MediaType::from(&specifier); + ( + specifier, + match media_type { + MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs, + MediaType::TypeScript | MediaType::Tsx => MediaType::Mts, + MediaType::Dts => MediaType::Dmts, + _ => media_type, + }, + ) + } + maybe_response => { + let specifier = match maybe_response { + Some(response) => response.into_url(), + None => { + ModuleSpecifier::parse("deno:///missing_dependency.d.ts").unwrap() + } + }; + (specifier, MediaType::Dts) + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NodeResolutionMode { + Execution, + Types, } struct NodeModulePolyfill { @@ -389,6 +442,7 @@ pub async fn initialize_binary_command( pub fn node_resolve( specifier: &str, referrer: &ModuleSpecifier, + mode: NodeResolutionMode, npm_resolver: &dyn RequireNpmResolver, ) -> Result<Option<NodeResolution>, AnyError> { // Note: if we are here, then the referrer is an esm module @@ -425,12 +479,22 @@ pub fn node_resolve( } } - let conditions = DEFAULT_CONDITIONS; + let conditions = mode_conditions(mode); let url = module_resolve(specifier, referrer, conditions, npm_resolver)?; let url = match url { Some(url) => url, None => return Ok(None), }; + let url = match mode { + NodeResolutionMode::Execution => url, + NodeResolutionMode::Types => { + let path = url.to_file_path().unwrap(); + // todo(16370): the module kind is not correct here. I think we need + // typescript to tell us if the referrer is esm or cjs + let path = path_to_declaration_path(path, NodeModuleKind::Esm); + ModuleSpecifier::from_file_path(path).unwrap() + } + }; let resolve_response = url_to_node_resolution(url, npm_resolver)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and @@ -440,24 +504,36 @@ pub fn node_resolve( pub fn node_resolve_npm_reference( reference: &NpmPackageReference, + mode: NodeResolutionMode, npm_resolver: &NpmPackageResolver, ) -> Result<Option<NodeResolution>, AnyError> { let package_folder = npm_resolver.resolve_package_folder_from_deno_module(&reference.req)?; - let resolved_path = package_config_resolve( + let node_module_kind = NodeModuleKind::Esm; + let maybe_resolved_path = package_config_resolve( &reference .sub_path .as_ref() .map(|s| format!("./{}", s)) .unwrap_or_else(|| ".".to_string()), &package_folder, + node_module_kind, + mode_conditions(mode), npm_resolver, - NodeModuleKind::Esm, ) .with_context(|| { format!("Error resolving package config for '{}'.", reference) })?; - + let resolved_path = match maybe_resolved_path { + Some(resolved_path) => resolved_path, + None => return Ok(None), + }; + let resolved_path = match mode { + NodeResolutionMode::Execution => resolved_path, + NodeResolutionMode::Types => { + path_to_declaration_path(resolved_path, node_module_kind) + } + }; let url = ModuleSpecifier::from_file_path(resolved_path).unwrap(); let resolve_response = url_to_node_resolution(url, npm_resolver)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and @@ -465,6 +541,41 @@ pub fn node_resolve_npm_reference( Ok(Some(resolve_response)) } +fn mode_conditions(mode: NodeResolutionMode) -> &'static [&'static str] { + match mode { + NodeResolutionMode::Execution => DEFAULT_CONDITIONS, + NodeResolutionMode::Types => TYPES_CONDITIONS, + } +} + +/// Checks if the resolved file has a corresponding declaration file. +fn path_to_declaration_path( + path: PathBuf, + referrer_kind: NodeModuleKind, +) -> PathBuf { + let lowercase_path = path.to_string_lossy().to_lowercase(); + if lowercase_path.ends_with(".d.ts") + || lowercase_path.ends_with(".d.cts") + || lowercase_path.ends_with(".d.ts") + { + return path; + } + let specific_dts_path = match referrer_kind { + NodeModuleKind::Cjs => path.with_extension("d.cts"), + NodeModuleKind::Esm => path.with_extension("d.mts"), + }; + if specific_dts_path.exists() { + specific_dts_path + } else { + let dts_path = path.with_extension("d.ts"); + if dts_path.exists() { + dts_path + } else { + path + } + } +} + pub fn node_resolve_binary_export( pkg_req: &NpmPackageReq, bin_name: Option<&str>, @@ -562,45 +673,56 @@ pub fn load_cjs_module_from_ext_node( fn package_config_resolve( package_subpath: &str, package_dir: &Path, - npm_resolver: &dyn RequireNpmResolver, referrer_kind: NodeModuleKind, -) -> Result<PathBuf, AnyError> { + conditions: &[&str], + npm_resolver: &dyn RequireNpmResolver, +) -> Result<Option<PathBuf>, AnyError> { let package_json_path = package_dir.join("package.json"); let referrer = ModuleSpecifier::from_directory_path(package_dir).unwrap(); let package_config = PackageJson::load(npm_resolver, package_json_path.clone())?; if let Some(exports) = &package_config.exports { + let is_types = conditions == TYPES_CONDITIONS; + if is_types && package_subpath == "." { + if let Ok(Some(path)) = + legacy_main_resolve(&package_config, referrer_kind, conditions) + { + return Ok(Some(path)); + } + } return package_exports_resolve( &package_json_path, package_subpath.to_string(), exports, &referrer, referrer_kind, - DEFAULT_CONDITIONS, + conditions, npm_resolver, - ); + ) + .map(Some); } if package_subpath == "." { - return legacy_main_resolve(&package_config, referrer_kind); + return legacy_main_resolve(&package_config, referrer_kind, conditions); } - Ok(package_dir.join(package_subpath)) + Ok(Some(package_dir.join(package_subpath))) } pub fn url_to_node_resolution( url: ModuleSpecifier, npm_resolver: &dyn RequireNpmResolver, ) -> Result<NodeResolution, AnyError> { - Ok(if url.as_str().starts_with("http") { + let url_str = url.as_str().to_lowercase(); + Ok(if url_str.starts_with("http") { NodeResolution::Esm(url) - } else if url.as_str().ends_with(".js") { + } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") { let package_config = get_closest_package_json(&url, npm_resolver)?; if package_config.typ == "module" { NodeResolution::Esm(url) } else { NodeResolution::CommonJs(url) } - } else if url.as_str().ends_with(".mjs") { + } else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") { NodeResolution::Esm(url) } else { NodeResolution::CommonJs(url) @@ -666,7 +788,16 @@ fn module_resolve( // note: if we're here, the referrer is an esm module let url = if should_be_treated_as_relative_or_absolute_path(specifier) { let resolved_specifier = referrer.join(specifier)?; - Some(resolved_specifier) + if conditions == TYPES_CONDITIONS { + let file_path = to_file_path(&resolved_specifier); + // todo(dsherret): the node module kind is not correct and we + // should use the value provided by typescript instead + let declaration_path = + path_to_declaration_path(file_path, NodeModuleKind::Esm); + Some(ModuleSpecifier::from_file_path(declaration_path).unwrap()) + } else { + Some(resolved_specifier) + } } else if specifier.starts_with('#') { Some( package_imports_resolve( @@ -681,16 +812,14 @@ fn module_resolve( } else if let Ok(resolved) = Url::parse(specifier) { Some(resolved) } else { - Some( - package_resolve( - specifier, - referrer, - NodeModuleKind::Esm, - conditions, - npm_resolver, - ) - .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?, - ) + package_resolve( + specifier, + referrer, + NodeModuleKind::Esm, + conditions, + npm_resolver, + )? + .map(|p| ModuleSpecifier::from_file_path(p).unwrap()) }; Ok(match url { Some(url) => Some(finalize_resolution(url, referrer)?), @@ -913,6 +1042,7 @@ fn resolve( let module_dir = npm_resolver.resolve_package_folder_from_package( package_specifier.as_str(), &referrer_path, + conditions, )?; let package_json_path = module_dir.join("package.json"); diff --git a/cli/npm/cache.rs b/cli/npm/cache.rs index 77ecf12285..6a0d72b3a5 100644 --- a/cli/npm/cache.rs +++ b/cli/npm/cache.rs @@ -160,13 +160,14 @@ impl ReadonlyNpmCache { .take(if is_scoped_package { 3 } else { 2 }) .map(|(_, part)| part) .collect::<Vec<_>>(); + if parts.len() < 2 { + return None; + } let version = parts.pop().unwrap(); let name = parts.join("/"); - - Some(NpmPackageId { - name, - version: NpmVersion::parse(version).unwrap(), - }) + NpmVersion::parse(version) + .ok() + .map(|version| NpmPackageId { name, version }) } pub fn get_cache_location(&self) -> PathBuf { diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index d60c06f144..738e1766f7 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -13,4 +13,5 @@ pub use resolution::NpmPackageId; pub use resolution::NpmPackageReference; pub use resolution::NpmPackageReq; pub use resolution::NpmResolutionPackage; +pub use resolution::NpmResolutionSnapshot; pub use resolvers::NpmPackageResolver; diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs index 2ceaa86ab2..8656d8a3c6 100644 --- a/cli/npm/resolution.rs +++ b/cli/npm/resolution.rs @@ -21,6 +21,7 @@ use super::registry::NpmPackageVersionDistInfo; use super::registry::NpmPackageVersionInfo; use super::registry::NpmRegistryApi; use super::semver::NpmVersion; +use super::semver::NpmVersionReq; use super::semver::SpecifierVersionReq; /// The version matcher used for npm schemed urls is more strict than @@ -375,15 +376,57 @@ impl NpmResolution { pub async fn add_package_reqs( &self, - mut package_reqs: Vec<NpmPackageReq>, + package_reqs: Vec<NpmPackageReq>, ) -> Result<(), AnyError> { - // multiple packages are resolved in alphabetical order - package_reqs.sort_by(|a, b| a.name.cmp(&b.name)); - // only allow one thread in here at a time let _permit = self.update_sempahore.acquire().await.unwrap(); - let mut snapshot = self.snapshot.read().clone(); - let mut pending_dependencies = VecDeque::new(); + let snapshot = self.snapshot.read().clone(); + + let snapshot = self + .add_package_reqs_to_snapshot(package_reqs, snapshot) + .await?; + + *self.snapshot.write() = snapshot; + Ok(()) + } + + pub async fn set_package_reqs( + &self, + package_reqs: HashSet<NpmPackageReq>, + ) -> Result<(), AnyError> { + // only allow one thread in here at a time + let _permit = self.update_sempahore.acquire().await.unwrap(); + let snapshot = self.snapshot.read().clone(); + + let has_removed_package = !snapshot + .package_reqs + .keys() + .all(|req| package_reqs.contains(req)); + // if any packages were removed, we need to completely recreate the npm resolution snapshot + let snapshot = if has_removed_package { + NpmResolutionSnapshot::default() + } else { + snapshot + }; + let snapshot = self + .add_package_reqs_to_snapshot( + package_reqs.into_iter().collect(), + snapshot, + ) + .await?; + + *self.snapshot.write() = snapshot; + + Ok(()) + } + + async fn add_package_reqs_to_snapshot( + &self, + mut package_reqs: Vec<NpmPackageReq>, + mut snapshot: NpmResolutionSnapshot, + ) -> Result<NpmResolutionSnapshot, AnyError> { + // multiple packages are resolved in alphabetical order + package_reqs.sort_by(|a, b| a.name.cmp(&b.name)); // go over the top level packages first, then down the // tree one level at a time through all the branches @@ -418,6 +461,7 @@ impl NpmResolution { })); } + let mut pending_dependencies = VecDeque::new(); for result in futures::future::join_all(unresolved_tasks).await { let (package_req, info) = result??; let version_and_info = get_resolved_package_version_and_info( @@ -546,8 +590,7 @@ impl NpmResolution { } } - *self.snapshot.write() = snapshot; - Ok(()) + Ok(snapshot) } pub fn resolve_package_from_package( @@ -601,6 +644,22 @@ fn get_resolved_package_version_and_info( ) -> Result<VersionAndInfo, AnyError> { let mut maybe_best_version: Option<VersionAndInfo> = None; if let Some(tag) = version_matcher.tag() { + // For when someone just specifies @types/node, we want to pull in a + // "known good" version of @types/node that works well with Deno and + // not necessarily the latest version. For example, we might only be + // compatible with Node vX, but then Node vY is published so we wouldn't + // want to pull that in. + // Note: If the user doesn't want this behavior, then they can specify an + // explicit version. + if tag == "latest" && pkg_name == "@types/node" { + return get_resolved_package_version_and_info( + pkg_name, + &NpmVersionReq::parse("18.0.0 - 18.8.2").unwrap(), + info, + parent, + ); + } + if let Some(version) = info.dist_tags.get(tag) { match info.versions.get(version) { Some(info) => { diff --git a/cli/npm/resolvers/common.rs b/cli/npm/resolvers/common.rs index b91deae9e6..4981d5613a 100644 --- a/cli/npm/resolvers/common.rs +++ b/cli/npm/resolvers/common.rs @@ -1,5 +1,6 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use std::collections::HashSet; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; @@ -26,6 +27,7 @@ pub trait InnerNpmPackageResolver: Send + Sync { &self, name: &str, referrer: &ModuleSpecifier, + conditions: &[&str], ) -> Result<PathBuf, AnyError>; fn resolve_package_folder_from_specifier( @@ -40,6 +42,11 @@ pub trait InnerNpmPackageResolver: Send + Sync { packages: Vec<NpmPackageReq>, ) -> BoxFuture<'static, Result<(), AnyError>>; + fn set_package_reqs( + &self, + packages: HashSet<NpmPackageReq>, + ) -> BoxFuture<'static, Result<(), AnyError>>; + fn ensure_read_permission(&self, path: &Path) -> Result<(), AnyError>; fn snapshot(&self) -> NpmResolutionSnapshot; diff --git a/cli/npm/resolvers/global.rs b/cli/npm/resolvers/global.rs index c1b6818fde..8eafc19f4d 100644 --- a/cli/npm/resolvers/global.rs +++ b/cli/npm/resolvers/global.rs @@ -2,6 +2,7 @@ //! Code for global npm cache resolution. +use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -11,6 +12,8 @@ use deno_core::error::AnyError; use deno_core::futures::future::BoxFuture; use deno_core::futures::FutureExt; use deno_core::url::Url; +use deno_runtime::deno_node::PackageJson; +use deno_runtime::deno_node::TYPES_CONDITIONS; use crate::npm::resolution::NpmResolution; use crate::npm::resolution::NpmResolutionSnapshot; @@ -65,14 +68,35 @@ impl InnerNpmPackageResolver for GlobalNpmPackageResolver { &self, name: &str, referrer: &ModuleSpecifier, + conditions: &[&str], ) -> Result<PathBuf, AnyError> { let referrer_pkg_id = self .cache .resolve_package_id_from_specifier(referrer, &self.registry_url)?; - let pkg = self + let pkg_result = self .resolution - .resolve_package_from_package(name, &referrer_pkg_id)?; - Ok(self.package_folder(&pkg.id)) + .resolve_package_from_package(name, &referrer_pkg_id); + if conditions == TYPES_CONDITIONS && !name.starts_with("@types/") { + // When doing types resolution, the package must contain a "types" + // entry, or else it will then search for a @types package + if let Ok(pkg) = pkg_result { + let package_folder = self.package_folder(&pkg.id); + let package_json = PackageJson::load_skip_read_permission( + package_folder.join("package.json"), + )?; + if package_json.types.is_some() { + return Ok(package_folder); + } + } + + let name = format!("@types/{}", name); + let pkg = self + .resolution + .resolve_package_from_package(&name, &referrer_pkg_id)?; + Ok(self.package_folder(&pkg.id)) + } else { + Ok(self.package_folder(&pkg_result?.id)) + } } fn resolve_package_folder_from_specifier( @@ -96,12 +120,19 @@ impl InnerNpmPackageResolver for GlobalNpmPackageResolver { let resolver = self.clone(); async move { resolver.resolution.add_package_reqs(packages).await?; - cache_packages( - resolver.resolution.all_packages(), - &resolver.cache, - &resolver.registry_url, - ) - .await + cache_packages_in_resolver(&resolver).await + } + .boxed() + } + + fn set_package_reqs( + &self, + packages: HashSet<NpmPackageReq>, + ) -> BoxFuture<'static, Result<(), AnyError>> { + let resolver = self.clone(); + async move { + resolver.resolution.set_package_reqs(packages).await?; + cache_packages_in_resolver(&resolver).await } .boxed() } @@ -115,3 +146,14 @@ impl InnerNpmPackageResolver for GlobalNpmPackageResolver { self.resolution.snapshot() } } + +async fn cache_packages_in_resolver( + resolver: &GlobalNpmPackageResolver, +) -> Result<(), AnyError> { + cache_packages( + resolver.resolution.all_packages(), + &resolver.cache, + &resolver.registry_url, + ) + .await +} diff --git a/cli/npm/resolvers/local.rs b/cli/npm/resolvers/local.rs index cd79320b79..b51593d4ca 100644 --- a/cli/npm/resolvers/local.rs +++ b/cli/npm/resolvers/local.rs @@ -17,6 +17,8 @@ use deno_core::futures::future::BoxFuture; use deno_core::futures::FutureExt; use deno_core::url::Url; use deno_runtime::deno_core::futures; +use deno_runtime::deno_node::PackageJson; +use deno_runtime::deno_node::TYPES_CONDITIONS; use tokio::task::JoinHandle; use crate::fs_util; @@ -124,6 +126,7 @@ impl InnerNpmPackageResolver for LocalNpmPackageResolver { &self, name: &str, referrer: &ModuleSpecifier, + conditions: &[&str], ) -> Result<PathBuf, AnyError> { let local_path = self.resolve_folder_for_specifier(referrer)?; let package_root_path = self.resolve_package_root(&local_path); @@ -132,8 +135,28 @@ impl InnerNpmPackageResolver for LocalNpmPackageResolver { current_folder = get_next_node_modules_ancestor(current_folder); let sub_dir = join_package_name(current_folder, name); if sub_dir.is_dir() { - return Ok(sub_dir); + // if doing types resolution, only resolve the package if it specifies a types property + if conditions == TYPES_CONDITIONS && !name.starts_with("@types/") { + let package_json = PackageJson::load_skip_read_permission( + sub_dir.join("package.json"), + )?; + if package_json.types.is_some() { + return Ok(sub_dir); + } + } else { + return Ok(sub_dir); + } } + + // if doing type resolution, check for the existance of a @types package + if conditions == TYPES_CONDITIONS && !name.starts_with("@types/") { + let sub_dir = + join_package_name(current_folder, &format!("@types/{}", name)); + if sub_dir.is_dir() { + return Ok(sub_dir); + } + } + if current_folder == self.root_node_modules_path { bail!( "could not find package '{}' from referrer '{}'.", @@ -164,15 +187,20 @@ impl InnerNpmPackageResolver for LocalNpmPackageResolver { let resolver = self.clone(); async move { resolver.resolution.add_package_reqs(packages).await?; + sync_resolver_with_fs(&resolver).await?; + Ok(()) + } + .boxed() + } - sync_resolution_with_fs( - &resolver.resolution.snapshot(), - &resolver.cache, - &resolver.registry_url, - &resolver.root_node_modules_path, - ) - .await?; - + fn set_package_reqs( + &self, + packages: HashSet<NpmPackageReq>, + ) -> BoxFuture<'static, Result<(), AnyError>> { + let resolver = self.clone(); + async move { + resolver.resolution.set_package_reqs(packages).await?; + sync_resolver_with_fs(&resolver).await?; Ok(()) } .boxed() @@ -187,6 +215,18 @@ impl InnerNpmPackageResolver for LocalNpmPackageResolver { } } +async fn sync_resolver_with_fs( + resolver: &LocalNpmPackageResolver, +) -> Result<(), AnyError> { + sync_resolution_with_fs( + &resolver.resolution.snapshot(), + &resolver.cache, + &resolver.registry_url, + &resolver.root_node_modules_path, + ) + .await +} + /// Creates a pnpm style folder structure. async fn sync_resolution_with_fs( snapshot: &NpmResolutionSnapshot, diff --git a/cli/npm/resolvers/mod.rs b/cli/npm/resolvers/mod.rs index d290a55695..5498bbf75e 100644 --- a/cli/npm/resolvers/mod.rs +++ b/cli/npm/resolvers/mod.rs @@ -15,6 +15,7 @@ use global::GlobalNpmPackageResolver; use once_cell::sync::Lazy; use serde::Deserialize; use serde::Serialize; +use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -23,10 +24,10 @@ use crate::fs_util; use self::common::InnerNpmPackageResolver; use self::local::LocalNpmPackageResolver; -use super::resolution::NpmResolutionSnapshot; use super::NpmCache; use super::NpmPackageReq; use super::NpmRegistryApi; +use super::NpmResolutionSnapshot; const RESOLUTION_STATE_ENV_VAR_NAME: &str = "DENO_DONT_USE_INTERNAL_NODE_COMPAT_STATE"; @@ -67,6 +68,19 @@ pub struct NpmPackageResolver { no_npm: bool, inner: Arc<dyn InnerNpmPackageResolver>, local_node_modules_path: Option<PathBuf>, + api: NpmRegistryApi, + cache: NpmCache, +} + +impl std::fmt::Debug for NpmPackageResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NpmPackageResolver") + .field("unstable", &self.unstable) + .field("no_npm", &self.no_npm) + .field("inner", &"<omitted>") + .field("local_node_modules_path", &self.local_node_modules_path) + .finish() + } } impl NpmPackageResolver { @@ -76,6 +90,24 @@ impl NpmPackageResolver { unstable: bool, no_npm: bool, local_node_modules_path: Option<PathBuf>, + ) -> Self { + Self::new_with_maybe_snapshot( + cache, + api, + unstable, + no_npm, + local_node_modules_path, + None, + ) + } + + fn new_with_maybe_snapshot( + cache: NpmCache, + api: NpmRegistryApi, + unstable: bool, + no_npm: bool, + local_node_modules_path: Option<PathBuf>, + initial_snapshot: Option<NpmResolutionSnapshot>, ) -> Self { let process_npm_state = NpmProcessState::take(); let local_node_modules_path = local_node_modules_path.or_else(|| { @@ -83,24 +115,29 @@ impl NpmPackageResolver { .as_ref() .and_then(|s| s.local_node_modules_path.as_ref().map(PathBuf::from)) }); - let maybe_snapshot = process_npm_state.map(|s| s.snapshot); + let maybe_snapshot = + initial_snapshot.or_else(|| process_npm_state.map(|s| s.snapshot)); let inner: Arc<dyn InnerNpmPackageResolver> = match &local_node_modules_path { Some(node_modules_folder) => Arc::new(LocalNpmPackageResolver::new( - cache, - api, + cache.clone(), + api.clone(), node_modules_folder.clone(), maybe_snapshot, )), - None => { - Arc::new(GlobalNpmPackageResolver::new(cache, api, maybe_snapshot)) - } + None => Arc::new(GlobalNpmPackageResolver::new( + cache.clone(), + api.clone(), + maybe_snapshot, + )), }; Self { unstable, no_npm, inner, local_node_modules_path, + api, + cache, } } @@ -122,10 +159,11 @@ impl NpmPackageResolver { &self, name: &str, referrer: &ModuleSpecifier, + conditions: &[&str], ) -> Result<PathBuf, AnyError> { let path = self .inner - .resolve_package_folder_from_package(name, referrer)?; + .resolve_package_folder_from_package(name, referrer, conditions)?; log::debug!("Resolved {} from {} to {}", name, referrer, path.display()); Ok(path) } @@ -156,12 +194,14 @@ impl NpmPackageResolver { self.inner.has_packages() } - /// Adds a package requirement to the resolver and ensures everything is setup. + /// Adds package requirements to the resolver and ensures everything is setup. pub async fn add_package_reqs( &self, packages: Vec<NpmPackageReq>, ) -> Result<(), AnyError> { - assert!(!packages.is_empty()); + if packages.is_empty() { + return Ok(()); + } if !self.unstable { bail!( @@ -187,6 +227,14 @@ impl NpmPackageResolver { self.inner.add_package_reqs(packages).await } + /// Sets package requirements to the resolver, removing old requirements and adding new ones. + pub async fn set_package_reqs( + &self, + packages: HashSet<NpmPackageReq>, + ) -> Result<(), AnyError> { + self.inner.set_package_reqs(packages).await + } + // If the main module should be treated as being in an npm package. // This is triggered via a secret environment variable which is used // for functionality like child_process.fork. Users should NOT depend @@ -206,6 +254,18 @@ impl NpmPackageResolver { }) .unwrap() } + + /// Gets a new resolver with a new snapshotted state. + pub fn snapshotted(&self) -> Self { + Self::new_with_maybe_snapshot( + self.cache.clone(), + self.api.clone(), + self.unstable, + self.no_npm, + self.local_node_modules_path.clone(), + Some(self.inner.snapshot()), + ) + } } impl RequireNpmResolver for NpmPackageResolver { @@ -213,9 +273,10 @@ impl RequireNpmResolver for NpmPackageResolver { &self, specifier: &str, referrer: &std::path::Path, + conditions: &[&str], ) -> Result<PathBuf, AnyError> { let referrer = path_to_specifier(referrer)?; - self.resolve_package_folder_from_package(specifier, &referrer) + self.resolve_package_folder_from_package(specifier, &referrer, conditions) } fn resolve_package_folder_from_path( diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 07a7c1f8a6..95520ffded 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -34,9 +34,9 @@ use crate::tools::check; use deno_ast::MediaType; use deno_core::anyhow::anyhow; -use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::custom_error; +use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::futures; use deno_core::parking_lot::Mutex; @@ -433,8 +433,13 @@ impl ProcState { let check_cache = TypeCheckCache::new(&self.dir.type_checking_cache_db_file_path()); let graph_data = self.graph_data.clone(); - let check_result = - check::check(&roots, graph_data, &check_cache, options)?; + let check_result = check::check( + &roots, + graph_data, + &check_cache, + self.npm_resolver.clone(), + options, + )?; if !check_result.diagnostics.is_empty() { return Err(anyhow!(check_result.diagnostics)); } @@ -470,7 +475,7 @@ impl ProcState { ) -> Result<ModuleSpecifier, AnyError> { let response = match result? { Some(response) => response, - None => bail!("Not found."), + None => return Err(generic_error("not found")), }; if let NodeResolution::CommonJs(specifier) = &response { // remember that this was a common js resolution @@ -493,6 +498,7 @@ impl ProcState { .handle_node_resolve_result(node::node_resolve( specifier, &referrer, + node::NodeResolutionMode::Execution, &self.npm_resolver, )) .with_context(|| { @@ -516,6 +522,7 @@ impl ProcState { return self .handle_node_resolve_result(node::node_resolve_npm_reference( &reference, + node::NodeResolutionMode::Execution, &self.npm_resolver, )) .with_context(|| format!("Could not resolve '{}'.", reference)); diff --git a/cli/tests/integration/check_tests.rs b/cli/tests/integration/check_tests.rs index ab96670ef3..f42cd4a7af 100644 --- a/cli/tests/integration/check_tests.rs +++ b/cli/tests/integration/check_tests.rs @@ -50,6 +50,13 @@ itest!(declaration_header_file_with_no_exports { output_str: Some(""), }); +itest!(check_npm_install_diagnostics { + args: "check --quiet check/npm_install_diagnostics/main.ts", + output: "check/npm_install_diagnostics/main.out", + envs: vec![("NO_COLOR".to_string(), "1".to_string())], + exit_code: 1, +}); + #[test] fn cache_switching_config_then_no_config() { let deno_dir = util::new_deno_dir(); diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 8390b0f6f4..9a0d407dfe 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -3346,6 +3346,37 @@ fn lsp_code_actions_deno_cache() { session.shutdown_and_exit(); } +#[test] +fn lsp_code_actions_deno_cache_npm() { + let mut session = TestSession::from_file("initialize_params.json"); + let diagnostics = session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import chalk from \"npm:chalk\";\n\nconsole.log(chalk.green);\n" + } + })); + assert_eq!( + diagnostics.with_source("deno"), + load_fixture_as("code_actions/cache_npm/diagnostics.json") + ); + + let (maybe_res, maybe_err) = session + .client + .write_request( + "textDocument/codeAction", + load_fixture("code_actions/cache_npm/cache_action.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_actions/cache_npm/cache_response.json")) + ); + session.shutdown_and_exit(); +} + #[test] fn lsp_code_actions_imports() { let mut session = TestSession::from_file("initialize_params.json"); @@ -4046,6 +4077,169 @@ fn lsp_completions_no_snippet() { } } +#[test] +fn lsp_completions_npm() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import cjsDefault from 'npm:@denotest/cjs-default-export';import chalk from 'npm:chalk';\n\n", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [ + { + "uri": "npm:@denotest/cjs-default-export", + }, + { + "uri": "npm:chalk", + } + ] + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + + // check importing a cjs default import + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "text": "cjsDefault." + } + ] + }), + ) + .unwrap(); + read_diagnostics(&mut client); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 11 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!(list.items.len(), 3); + assert!(list.items.iter().any(|i| i.label == "default")); + assert!(list.items.iter().any(|i| i.label == "MyClass")); + } else { + panic!("unexpected response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + load_fixture("completions/npm/resolve_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("completions/npm/resolve_response.json")) + ); + + // now check chalk, which is esm + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 3 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 11 + } + }, + "text": "chalk." + } + ] + }), + ) + .unwrap(); + read_diagnostics(&mut client); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 6 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert!(list.items.iter().any(|i| i.label == "green")); + assert!(list.items.iter().any(|i| i.label == "red")); + } else { + panic!("unexpected response"); + } + + shutdown(&mut client); +} + #[test] fn lsp_completions_registry() { let _g = http_server(); diff --git a/cli/tests/integration/npm_tests.rs b/cli/tests/integration/npm_tests.rs index bc19c613de..9fc8171412 100644 --- a/cli/tests/integration/npm_tests.rs +++ b/cli/tests/integration/npm_tests.rs @@ -1,6 +1,5 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -use deno_core::url::Url; use std::process::Stdio; use test_util as util; use util::assert_contains; @@ -34,7 +33,7 @@ itest!(esm_module_deno_test { }); itest!(esm_import_cjs_default { - args: "run --allow-read --allow-env --unstable --quiet npm/esm_import_cjs_default/main.js", + args: "run --allow-read --allow-env --unstable --quiet --check=all npm/esm_import_cjs_default/main.ts", output: "npm/esm_import_cjs_default/main.out", envs: env_vars(), http_server: true, @@ -84,7 +83,7 @@ itest!(translate_cjs_to_esm { }); itest!(compare_globals { - args: "run --allow-read --unstable npm/compare_globals/main.js", + args: "run --allow-read --unstable --check=all npm/compare_globals/main.ts", output: "npm/compare_globals/main.out", envs: env_vars(), http_server: true, @@ -210,6 +209,38 @@ itest!(deno_cache { http_server: true, }); +itest!(check_all { + args: "check --unstable --remote npm/check_errors/main.ts", + output: "npm/check_errors/main_all.out", + envs: env_vars(), + http_server: true, + exit_code: 1, +}); + +itest!(check_local { + args: "check --unstable npm/check_errors/main.ts", + output: "npm/check_errors/main_local.out", + envs: env_vars(), + http_server: true, + exit_code: 1, +}); + +itest!(types_ambient_module { + args: "check --unstable --quiet npm/types_ambient_module/main.ts", + output: "npm/types_ambient_module/main.out", + envs: env_vars(), + http_server: true, + exit_code: 1, +}); + +itest!(types_ambient_module_import_map { + args: "check --unstable --quiet --import-map=npm/types_ambient_module/import_map.json npm/types_ambient_module/main_import_map.ts", + output: "npm/types_ambient_module/main_import_map.out", + envs: env_vars(), + http_server: true, + exit_code: 1, +}); + #[test] fn parallel_downloading() { let (out, _err) = util::run_and_collect_output_with_args( @@ -672,18 +703,10 @@ fn ensure_registry_files_local() { } } -fn std_file_url() -> String { - let u = Url::from_directory_path(util::std_path()).unwrap(); - u.to_string() -} - fn env_vars_no_sync_download() -> Vec<(String, String)> { vec![ - ("DENO_NODE_COMPAT_URL".to_string(), std_file_url()), - ( - "DENO_NPM_REGISTRY".to_string(), - "http://localhost:4545/npm/registry/".to_string(), - ), + ("DENO_NODE_COMPAT_URL".to_string(), util::std_file_url()), + ("DENO_NPM_REGISTRY".to_string(), util::npm_registry_url()), ("NO_COLOR".to_string(), "1".to_string()), ] } diff --git a/cli/tests/testdata/check/npm_install_diagnostics/main.out b/cli/tests/testdata/check/npm_install_diagnostics/main.out new file mode 100644 index 0000000000..fe46f0e423 --- /dev/null +++ b/cli/tests/testdata/check/npm_install_diagnostics/main.out @@ -0,0 +1,11 @@ +error: TS2581 [ERROR]: Cannot find name '$'. Did you mean to import jQuery? Try adding `import $ from "npm:jquery";`. +$; +^ + at file:///[WILDCARD]/npm_install_diagnostics/main.ts:1:1 + +TS2580 [ERROR]: Cannot find name 'process'. +process; +~~~~~~~ + at file:///[WILDCARD]/npm_install_diagnostics/main.ts:2:1 + +Found 2 errors. diff --git a/cli/tests/testdata/check/npm_install_diagnostics/main.ts b/cli/tests/testdata/check/npm_install_diagnostics/main.ts new file mode 100644 index 0000000000..62c0c56191 --- /dev/null +++ b/cli/tests/testdata/check/npm_install_diagnostics/main.ts @@ -0,0 +1,2 @@ +$; +process; diff --git a/cli/tests/testdata/lsp/code_actions/cache_npm/cache_action.json b/cli/tests/testdata/lsp/code_actions/cache_npm/cache_action.json new file mode 100644 index 0000000000..b698df3bd8 --- /dev/null +++ b/cli/tests/testdata/lsp/code_actions/cache_npm/cache_action.json @@ -0,0 +1,41 @@ +{ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { + "line": 0, + "character": 18 + }, + "end": { + "line": 0, + "character": 29 + } + }, + "context": { + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 18 + }, + "end": { + "line": 0, + "character": 29 + } + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: \"chalk\".", + "data": { + "specifier": "npm:chalk" + } + } + ], + "only": [ + "quickfix" + ] + } +} diff --git a/cli/tests/testdata/lsp/code_actions/cache_npm/cache_response.json b/cli/tests/testdata/lsp/code_actions/cache_npm/cache_response.json new file mode 100644 index 0000000000..1b41babcb8 --- /dev/null +++ b/cli/tests/testdata/lsp/code_actions/cache_npm/cache_response.json @@ -0,0 +1,36 @@ +[ + { + "title": "Cache \"npm:chalk\" and its dependencies.", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 18 + }, + "end": { + "line": 0, + "character": 29 + } + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: \"chalk\".", + "data": { + "specifier": "npm:chalk" + } + } + ], + "command": { + "title": "", + "command": "deno.cache", + "arguments": [ + [ + "npm:chalk" + ] + ] + } + } +] diff --git a/cli/tests/testdata/lsp/code_actions/cache_npm/diagnostics.json b/cli/tests/testdata/lsp/code_actions/cache_npm/diagnostics.json new file mode 100644 index 0000000000..63c9d0029d --- /dev/null +++ b/cli/tests/testdata/lsp/code_actions/cache_npm/diagnostics.json @@ -0,0 +1,25 @@ +{ + "uri": "file:///a/file.ts", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 18 + }, + "end": { + "line": 0, + "character": 29 + } + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: \"chalk\".", + "data": { + "specifier": "npm:chalk" + } + } + ], + "version": 1 +} diff --git a/cli/tests/testdata/lsp/completions/npm/resolve_params.json b/cli/tests/testdata/lsp/completions/npm/resolve_params.json new file mode 100644 index 0000000000..c83b8ce495 --- /dev/null +++ b/cli/tests/testdata/lsp/completions/npm/resolve_params.json @@ -0,0 +1,14 @@ +{ + "label": "MyClass", + "kind": 6, + "sortText": "1", + "insertTextFormat": 1, + "data": { + "tsc": { + "specifier": "file:///a/file.ts", + "position": 69, + "name": "MyClass", + "useCodeSnippet": false + } + } +} diff --git a/cli/tests/testdata/lsp/completions/npm/resolve_response.json b/cli/tests/testdata/lsp/completions/npm/resolve_response.json new file mode 100644 index 0000000000..c83b8ce495 --- /dev/null +++ b/cli/tests/testdata/lsp/completions/npm/resolve_response.json @@ -0,0 +1,14 @@ +{ + "label": "MyClass", + "kind": 6, + "sortText": "1", + "insertTextFormat": 1, + "data": { + "tsc": { + "specifier": "file:///a/file.ts", + "position": 69, + "name": "MyClass", + "useCodeSnippet": false + } + } +} diff --git a/cli/tests/testdata/npm/check_errors/main.ts b/cli/tests/testdata/npm/check_errors/main.ts new file mode 100644 index 0000000000..4b86841956 --- /dev/null +++ b/cli/tests/testdata/npm/check_errors/main.ts @@ -0,0 +1,3 @@ +import * as test from "npm:@denotest/check-error"; + +console.log(test.Asdf); // should error diff --git a/cli/tests/testdata/npm/check_errors/main_all.out b/cli/tests/testdata/npm/check_errors/main_all.out new file mode 100644 index 0000000000..96f16d0b93 --- /dev/null +++ b/cli/tests/testdata/npm/check_errors/main_all.out @@ -0,0 +1,19 @@ +Download http://localhost:4545/npm/registry/@denotest/check-error +Download http://localhost:4545/npm/registry/@denotest/check-error/1.0.0.tgz +Check file:///[WILDCARD]/check_errors/main.ts +error: TS2506 [ERROR]: 'Class1' is referenced directly or indirectly in its own base expression. +export class Class1 extends Class2 { + ~~~~~~ + at file:///[WILDCARD]/check-error/1.0.0/index.d.ts:2:14 + +TS2506 [ERROR]: 'Class2' is referenced directly or indirectly in its own base expression. +export class Class2 extends Class1 { + ~~~~~~ + at file:///[WILDCARD]/check-error/1.0.0/index.d.ts:5:14 + +TS2339 [ERROR]: Property 'Asdf' does not exist on type 'typeof import("file:///[WILDCARD]/@denotest/check-error/1.0.0/index.d.ts")'. +console.log(test.Asdf); // should error + ~~~~ + at file:///[WILDCARD]/check_errors/main.ts:3:18 + +Found 3 errors. diff --git a/cli/tests/testdata/npm/check_errors/main_local.out b/cli/tests/testdata/npm/check_errors/main_local.out new file mode 100644 index 0000000000..1624b98bc3 --- /dev/null +++ b/cli/tests/testdata/npm/check_errors/main_local.out @@ -0,0 +1,7 @@ +Download http://localhost:4545/npm/registry/@denotest/check-error +Download http://localhost:4545/npm/registry/@denotest/check-error/1.0.0.tgz +Check file:///[WILDCARD]/check_errors/main.ts +error: TS2339 [ERROR]: Property 'Asdf' does not exist on type 'typeof import("file:///[WILDCARD]/@denotest/check-error/1.0.0/index.d.ts")'. +console.log(test.Asdf); // should error + ~~~~ + at file:///[WILDCARD]/npm/check_errors/main.ts:3:18 diff --git a/cli/tests/testdata/npm/compare_globals/main.js b/cli/tests/testdata/npm/compare_globals/main.js deleted file mode 100644 index ce43e32b11..0000000000 --- a/cli/tests/testdata/npm/compare_globals/main.js +++ /dev/null @@ -1,2 +0,0 @@ -import * as globals from "npm:@denotest/globals"; -console.log(globals.global === globals.globalThis); diff --git a/cli/tests/testdata/npm/compare_globals/main.out b/cli/tests/testdata/npm/compare_globals/main.out index a1a5c0e8f3..1b22fd318b 100644 --- a/cli/tests/testdata/npm/compare_globals/main.out +++ b/cli/tests/testdata/npm/compare_globals/main.out @@ -1,3 +1,8 @@ Download http://localhost:4545/npm/registry/@denotest/globals +Download http://localhost:4545/npm/registry/@types/node Download http://localhost:4545/npm/registry/@denotest/globals/1.0.0.tgz +Download http://localhost:4545/npm/registry/@types/node/node-18.8.2.tgz +Check file:///[WILDCARD]/npm/compare_globals/main.ts +Check file:///[WILDCARD]/std/node/module_all.ts true +[] diff --git a/cli/tests/testdata/npm/compare_globals/main.ts b/cli/tests/testdata/npm/compare_globals/main.ts new file mode 100644 index 0000000000..5710d0bd59 --- /dev/null +++ b/cli/tests/testdata/npm/compare_globals/main.ts @@ -0,0 +1,14 @@ +/// <reference types="npm:@types/node" /> + +import * as globals from "npm:@denotest/globals"; +console.log(globals.global === globals.globalThis); +console.log(globals.process.execArgv); + +type AssertTrue<T extends true> = never; +type _TestNoProcessGlobal = AssertTrue< + typeof globalThis extends { process: any } ? false : true +>; +type _TestHasNodeJsGlobal = NodeJS.Architecture; + +const controller = new AbortController(); +controller.abort("reason"); // in the NodeJS declaration it doesn't have a reason diff --git a/cli/tests/testdata/npm/esm_import_cjs_default/main.js b/cli/tests/testdata/npm/esm_import_cjs_default/main.ts similarity index 98% rename from cli/tests/testdata/npm/esm_import_cjs_default/main.js rename to cli/tests/testdata/npm/esm_import_cjs_default/main.ts index f405a58999..f9c3280e52 100644 --- a/cli/tests/testdata/npm/esm_import_cjs_default/main.js +++ b/cli/tests/testdata/npm/esm_import_cjs_default/main.ts @@ -1,3 +1,4 @@ +// @ts-check import cjsDefault, { MyClass as MyCjsClass, } from "npm:@denotest/cjs-default-export"; diff --git a/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/index.d.ts b/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/index.d.ts new file mode 100644 index 0000000000..673c0035ef --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/index.d.ts @@ -0,0 +1,6 @@ +// intentional type checking errors +export class Class1 extends Class2 { +} + +export class Class2 extends Class1 { +} diff --git a/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/index.js b/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/index.js new file mode 100644 index 0000000000..7eb6b784d5 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/index.js @@ -0,0 +1,6 @@ +module.exports = { + Class1: class { + }, + Class2: class { + }, +}; diff --git a/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/package.json new file mode 100644 index 0000000000..295920a8f7 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/check-error/1.0.0/package.json @@ -0,0 +1,5 @@ +{ + "name": "@denotest/check-error", + "version": "1.0.0", + "types": "./index.d.ts" +} diff --git a/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/index.d.ts b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/index.d.ts new file mode 100644 index 0000000000..90fdfe5f67 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/index.d.ts @@ -0,0 +1,6 @@ +export default function (): number; +export declare function named(): number; +declare class MyClass { + static someStaticMethod(): string; +} +export { MyClass }; diff --git a/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json index 4765d25d27..8da28b9192 100644 --- a/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json +++ b/cli/tests/testdata/npm/registry/@denotest/cjs-default-export/1.0.0/package.json @@ -1,4 +1,5 @@ { "name": "@denotest/cjs-default-export", - "version": "1.0.0" + "version": "1.0.0", + "types": "./index.d.ts" } diff --git a/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json index 1840767994..f757a08fb2 100644 --- a/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json +++ b/cli/tests/testdata/npm/registry/@denotest/esm-import-cjs-default/1.0.0/package.json @@ -1,6 +1,7 @@ { "name": "@denotest/esm-import-cjs-default", "version": "1.0.0", + "main": "index.mjs", "dependencies": { "@denotest/cjs-default-export": "^1.0.0" } diff --git a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts new file mode 100644 index 0000000000..ee03712dd4 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.d.ts @@ -0,0 +1,13 @@ +declare const tempGlobalThis: typeof globalThis; +declare const tempGlobal: typeof global; +declare const tempProcess: typeof process; +export { + tempGlobalThis as globalThis, + tempGlobal as global, + tempProcess as process, +}; + +type AssertTrue<T extends true> = never; +type _TestHasProcessGlobal = AssertTrue< + typeof globalThis extends { process: any } ? true : false +>; diff --git a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js index be5e6e5ac4..50d2d3d2a3 100644 --- a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js +++ b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/index.js @@ -1,2 +1,3 @@ exports.globalThis = globalThis; exports.global = global; +exports.process = process; diff --git a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/package.json index cba0742c1a..1ce42ded43 100644 --- a/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/package.json +++ b/cli/tests/testdata/npm/registry/@denotest/globals/1.0.0/package.json @@ -1,4 +1,5 @@ { "name": "@denotest/globals", - "version": "1.0.0" + "version": "1.0.0", + "types": "index.d.ts" } diff --git a/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/index.d.ts b/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/index.d.ts new file mode 100644 index 0000000000..fc2199884a --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/index.d.ts @@ -0,0 +1,10 @@ +// Some packages do this. It's really not ideal because instead of allowing +// the package to be resolved at any specifier, it instead expects the package +// to be resolved via a "@denotest/types-ambient" specifier. To make this work, +// we've currently modified the typescript compiler to check for any "<package-name>" +// ambient modules when resolving an npm specifier at "npm:<package-name>" +declare module "@denotest/types-ambient" { + class Test { + prop: number; + } +} diff --git a/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/index.js b/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/index.js new file mode 100644 index 0000000000..47ff7adb2c --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/index.js @@ -0,0 +1,3 @@ +export class Test { + prop = 5; +} diff --git a/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/package.json new file mode 100644 index 0000000000..ef927cbe35 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/types-ambient/1.0.0/package.json @@ -0,0 +1,5 @@ +{ + "name": "@denotest/types-ambient", + "version": "1.0.0", + "types": "./index.d.ts" +} diff --git a/cli/tests/testdata/npm/registry/@types/node/node-18.8.2.tgz b/cli/tests/testdata/npm/registry/@types/node/node-18.8.2.tgz new file mode 100644 index 0000000000..8afc9d21df Binary files /dev/null and b/cli/tests/testdata/npm/registry/@types/node/node-18.8.2.tgz differ diff --git a/cli/tests/testdata/npm/registry/@types/node/registry.json b/cli/tests/testdata/npm/registry/@types/node/registry.json new file mode 100644 index 0000000000..3fff1578ed --- /dev/null +++ b/cli/tests/testdata/npm/registry/@types/node/registry.json @@ -0,0 +1,73 @@ +{ + "_id": "@types/node", + "_rev": "8944-025a921c7561ec7339c70b87995cb358", + "name": "@types/node", + "description": "TypeScript definitions for Node.js", + "dist-tags": { + "latest": "18.8.2" + }, + "versions": { + "18.8.2": { + "name": "@types/node", + "version": "18.8.2", + "description": "TypeScript definitions for Node.js", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node", + "license": "MIT", + "contributors": [ + ], + "main": "", + "types": "index.d.ts", + "typesVersions": { "<4.9.0-0": { "*": ["ts4.8/*"] } }, + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/node" + }, + "scripts": {}, + "dependencies": {}, + "typesPublisherContentHash": "034172ea945b66afc6502e6be34d6fb957c596091e39cf43672e8aca563a8c66", + "typeScriptVersion": "4.1", + "_id": "@types/node@18.8.2", + "dist": { + "integrity": "sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==", + "shasum": "17d42c6322d917764dd3d2d3a10d7884925de067", + "tarball": "http://localhost:4545/npm/registry/@types/node/node-18.8.2.tgz", + "fileCount": 124, + "unpackedSize": 3524549, + "signatures": [ + { + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "sig": "MEYCIQCAqI3XibndhBD647C/13AFb58Fhmg4WmfCoGrIYrgtzwIhAIB0b0D58Tigwb3qKaOVsKnuYOOr0strAmprZSCi/+oq" + } + ], + "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjPFItACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmrKAg/+IwaUWPgePlO4IxW7CVhFEEFiyhjEH3FHe0ogC3YmreoBFv/A\r\nPwQrwObdskbGWrBzsAOVFvhzYktzP3kc857HtU2ia9FXeaEYvsSQBqh6jZfA\r\njR1+h+jn+W5JnmbnwRGfNn2riCo/un4tYoZ4o/bKiMdNd9WrdIs0Oii1Dd4N\r\nnsBXPb05UPPP4Uu8cz68u1bj+QQ6aslr6keGNyNeILf8bJsEfcfVkEO3l4cu\r\njyTIrxiD+tM8jrUE9CDeodF8CZNuvLh3hqdMPJeH3U47qkDtPDKEOvZTbyYm\r\ngodto6mcnuBr8F9vmikAQfGiXV0U2cg2XRjWc1lI8HT4X0kESTIo+nzNuliD\r\niTpfjyZHdKBGGIuHP1Ou9dVvx4t5XZ1JsK9EK5WTilvAlu/qZrynxXxAV3Rc\r\nvL9UhIacISprMWB3Sohl9ZtfcmGnV/KMRpM+yPZOWp39gQQCKaKF/j2f/mir\r\n8YFbFUnySZkXKzxgsgjrSsh9QiK2dYAzcqHlazITeFN9jOYRzYUjAFj3qOFm\r\n7o1eJpW0qM5vgR+fPq30WxcdcQ4PaWgHSlb/ll8hiwQG1ZUihO/1RU7FtDoc\r\n1KwcfrGOAJPL6vBSLPReUkhDIUTSBwfmvfMxzqD1aDp6YV5WX7h03B0dWbPJ\r\nmPJmJZjjZje4Trk9jBJ5/ZLpB8/zkrDKvhE=\r\n=LPWa\r\n-----END PGP SIGNATURE-----\r\n" + }, + "_npmUser": { "name": "types", "email": "ts-npm-types@microsoft.com" }, + "directories": {}, + "maintainers": [ + { "name": "types", "email": "ts-npm-types@microsoft.com" } + ], + "_npmOperationalInternal": { + "host": "s3://npm-registry-packages", + "tmp": "tmp/node_18.8.2_1664897581729_0.9746861340465625" + }, + "_hasShrinkwrap": false + } + }, + "readme": "[object Object]", + "maintainers": [{ "name": "types", "email": "ts-npm-types@microsoft.com" }], + "time": { + "18.8.2": "2022-10-04T15:33:01.913Z" + }, + "license": "MIT", + "readmeFilename": "", + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/node" + }, + "users": { + }, + "contributors": [], + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node" +} diff --git a/cli/tests/testdata/npm/types_ambient_module/import_map.json b/cli/tests/testdata/npm/types_ambient_module/import_map.json new file mode 100644 index 0000000000..f61d99b474 --- /dev/null +++ b/cli/tests/testdata/npm/types_ambient_module/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "types-ambient": "npm:@denotest/types-ambient" + } +} diff --git a/cli/tests/testdata/npm/types_ambient_module/main.out b/cli/tests/testdata/npm/types_ambient_module/main.out new file mode 100644 index 0000000000..c84130707e --- /dev/null +++ b/cli/tests/testdata/npm/types_ambient_module/main.out @@ -0,0 +1,21 @@ +error: TS2551 [ERROR]: Property 'Test2' does not exist on type 'typeof import("@denotest/types-ambient")'. Did you mean 'Test'? +console.log(import1.Test2); // should error + ~~~~~ + at file:///[WILDCARD]/types_ambient_module/main.ts:5:21 + + 'Test' is declared here. + class Test { + ~~~~ + at file:///[WILDCARD]/@denotest/types-ambient/1.0.0/index.d.ts:7:9 + +TS2551 [ERROR]: Property 'Test2' does not exist on type 'typeof import("@denotest/types-ambient")'. Did you mean 'Test'? +console.log(import2.Test2); // should error + ~~~~~ + at file:///[WILDCARD]/types_ambient_module/main.ts:7:21 + + 'Test' is declared here. + class Test { + ~~~~ + at file:///[WILDCARD]/@denotest/types-ambient/1.0.0/index.d.ts:7:9 + +Found 2 errors. diff --git a/cli/tests/testdata/npm/types_ambient_module/main.ts b/cli/tests/testdata/npm/types_ambient_module/main.ts new file mode 100644 index 0000000000..8f77cabe8e --- /dev/null +++ b/cli/tests/testdata/npm/types_ambient_module/main.ts @@ -0,0 +1,7 @@ +import * as import1 from "npm:@denotest/types-ambient"; +import * as import2 from "npm:@denotest/types-ambient@1"; + +console.log(import1.Test); +console.log(import1.Test2); // should error +console.log(import2.Test); +console.log(import2.Test2); // should error diff --git a/cli/tests/testdata/npm/types_ambient_module/main_import_map.out b/cli/tests/testdata/npm/types_ambient_module/main_import_map.out new file mode 100644 index 0000000000..548f9b479c --- /dev/null +++ b/cli/tests/testdata/npm/types_ambient_module/main_import_map.out @@ -0,0 +1,9 @@ +error: TS2551 [ERROR]: Property 'Test2' does not exist on type 'typeof import("@denotest/types-ambient")'. Did you mean 'Test'? +console.log(mod.Test2); // should error + ~~~~~ + at file:///[WILDCARD]/main_import_map.ts:4:17 + + 'Test' is declared here. + class Test { + ~~~~ + at file:///[WILDCARD]/@denotest/types-ambient/1.0.0/index.d.ts:7:9 diff --git a/cli/tests/testdata/npm/types_ambient_module/main_import_map.ts b/cli/tests/testdata/npm/types_ambient_module/main_import_map.ts new file mode 100644 index 0000000000..2694c94b7a --- /dev/null +++ b/cli/tests/testdata/npm/types_ambient_module/main_import_map.ts @@ -0,0 +1,4 @@ +import * as mod from "npm:@denotest/types-ambient"; + +console.log(mod.Test); +console.log(mod.Test2); // should error diff --git a/cli/tools/check.rs b/cli/tools/check.rs index bb0b873f45..88c05e1308 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -18,6 +18,7 @@ use crate::cache::TypeCheckCache; use crate::diagnostics::Diagnostics; use crate::graph_util::GraphData; use crate::graph_util::ModuleEntry; +use crate::npm::NpmPackageResolver; use crate::tsc; use crate::tsc::Stats; use crate::version; @@ -57,6 +58,7 @@ pub fn check( roots: &[(ModuleSpecifier, ModuleKind)], graph_data: Arc<RwLock<GraphData>>, cache: &TypeCheckCache, + npm_resolver: NpmPackageResolver, options: CheckOptions, ) -> Result<CheckResult, AnyError> { let check_js = options.ts_config.get_check_js(); @@ -106,6 +108,7 @@ pub fn check( graph_data, hash_data, maybe_config_specifier: options.maybe_config_specifier, + maybe_npm_resolver: Some(npm_resolver.clone()), maybe_tsbuildinfo, root_names, })?; @@ -114,6 +117,9 @@ pub fn check( response.diagnostics.filter(|d| { if let Some(file_name) = &d.file_name { !file_name.starts_with("http") + && ModuleSpecifier::parse(file_name) + .map(|specifier| !npm_resolver.in_npm_package(&specifier)) + .unwrap_or(true) } else { true } diff --git a/cli/tsc.rs b/cli/tsc.rs index e9800d9d23..90ae7334ce 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -4,6 +4,12 @@ use crate::args::TsConfig; use crate::diagnostics::Diagnostics; use crate::graph_util::GraphData; use crate::graph_util::ModuleEntry; +use crate::node; +use crate::node::node_resolve_npm_reference; +use crate::node::NodeResolution; +use crate::node::NodeResolutionMode; +use crate::npm::NpmPackageReference; +use crate::npm::NpmPackageResolver; use deno_ast::MediaType; use deno_core::anyhow::anyhow; @@ -28,6 +34,7 @@ use deno_core::RuntimeOptions; use deno_core::Snapshot; use deno_graph::Resolved; use once_cell::sync::Lazy; +use std::borrow::Cow; use std::collections::HashMap; use std::fmt; use std::path::PathBuf; @@ -171,7 +178,7 @@ fn get_maybe_hash( } /// Hash the URL so it can be sent to `tsc` in a supportable way -fn hash_url(specifier: &ModuleSpecifier, media_type: &MediaType) -> String { +fn hash_url(specifier: &ModuleSpecifier, media_type: MediaType) -> String { let hash = crate::checksum::gen(&[specifier.path().as_bytes()]); format!( "{}:///{}{}", @@ -187,7 +194,7 @@ fn hash_url(specifier: &ModuleSpecifier, media_type: &MediaType) -> String { /// think a `.js` version exists, when it doesn't. fn maybe_remap_specifier( specifier: &ModuleSpecifier, - media_type: &MediaType, + media_type: MediaType, ) -> Option<String> { let path = if specifier.scheme() == "file" { if let Ok(path) = specifier.to_file_path() { @@ -279,6 +286,7 @@ pub struct Request { pub graph_data: Arc<RwLock<GraphData>>, pub hash_data: Vec<Vec<u8>>, pub maybe_config_specifier: Option<ModuleSpecifier>, + pub maybe_npm_resolver: Option<NpmPackageResolver>, pub maybe_tsbuildinfo: Option<String>, /// A vector of strings that represent the root/entry point modules for the /// program. @@ -295,13 +303,14 @@ pub struct Response { pub stats: Stats, } -#[derive(Debug)] +#[derive(Debug, Default)] struct State { hash_data: Vec<Vec<u8>>, graph_data: Arc<RwLock<GraphData>>, maybe_config_specifier: Option<ModuleSpecifier>, maybe_tsbuildinfo: Option<String>, maybe_response: Option<RespondArgs>, + maybe_npm_resolver: Option<NpmPackageResolver>, remapped_specifiers: HashMap<String, ModuleSpecifier>, root_map: HashMap<String, ModuleSpecifier>, } @@ -311,6 +320,7 @@ impl State { graph_data: Arc<RwLock<GraphData>>, hash_data: Vec<Vec<u8>>, maybe_config_specifier: Option<ModuleSpecifier>, + maybe_npm_resolver: Option<NpmPackageResolver>, maybe_tsbuildinfo: Option<String>, root_map: HashMap<String, ModuleSpecifier>, remapped_specifiers: HashMap<String, ModuleSpecifier>, @@ -319,6 +329,7 @@ impl State { hash_data, graph_data, maybe_config_specifier, + maybe_npm_resolver, maybe_tsbuildinfo, maybe_response: None, remapped_specifiers, @@ -417,7 +428,7 @@ struct LoadArgs { specifier: String, } -pub fn as_ts_script_kind(media_type: &MediaType) -> i32 { +pub fn as_ts_script_kind(media_type: MediaType) -> i32 { match media_type { MediaType::JavaScript => 1, MediaType::Jsx => 2, @@ -431,7 +442,10 @@ pub fn as_ts_script_kind(media_type: &MediaType) -> i32 { MediaType::Dcts => 3, MediaType::Tsx => 4, MediaType::Json => 6, - _ => 0, + MediaType::SourceMap + | MediaType::TsBuildInfo + | MediaType::Wasm + | MediaType::Unknown => 0, } } @@ -446,19 +460,19 @@ fn op_load(state: &mut OpState, args: Value) -> Result<Value, AnyError> { let mut media_type = MediaType::Unknown; let graph_data = state.graph_data.read(); let data = if &v.specifier == "deno:///.tsbuildinfo" { - state.maybe_tsbuildinfo.as_deref() + state.maybe_tsbuildinfo.as_deref().map(Cow::Borrowed) // in certain situations we return a "blank" module to tsc and we need to // handle the request for that module here. } else if &v.specifier == "deno:///missing_dependency.d.ts" { hash = Some("1".to_string()); media_type = MediaType::Dts; - Some("declare const __: any;\nexport = __;\n") + Some(Cow::Borrowed("declare const __: any;\nexport = __;\n")) } else if v.specifier.starts_with("asset:///") { let name = v.specifier.replace("asset:///", ""); let maybe_source = get_asset(&name); hash = get_maybe_hash(maybe_source, &state.hash_data); media_type = MediaType::from(&v.specifier); - maybe_source + maybe_source.map(Cow::Borrowed) } else { let specifier = if let Some(remapped_specifier) = state.remapped_specifiers.get(&v.specifier) @@ -477,18 +491,31 @@ fn op_load(state: &mut OpState, args: Value) -> Result<Value, AnyError> { graph_data.get(&graph_data.follow_redirect(&specifier)) { media_type = *mt; - Some(code as &str) + Some(Cow::Borrowed(code as &str)) + } else if state + .maybe_npm_resolver + .as_ref() + .map(|resolver| resolver.in_npm_package(&specifier)) + .unwrap_or(false) + { + media_type = MediaType::from(&specifier); + let file_path = specifier.to_file_path().unwrap(); + let code = std::fs::read_to_string(&file_path) + .with_context(|| format!("Unable to load {}", file_path.display()))?; + Some(Cow::Owned(code)) } else { media_type = MediaType::Unknown; None }; - hash = get_maybe_hash(maybe_source, &state.hash_data); + hash = get_maybe_hash(maybe_source.as_deref(), &state.hash_data); maybe_source }; - Ok( - json!({ "data": data, "version": hash, "scriptKind": as_ts_script_kind(&media_type) }), - ) + Ok(json!({ + "data": data, + "version": hash, + "scriptKind": as_ts_script_kind(media_type), + })) } #[derive(Debug, Deserialize, Serialize)] @@ -550,17 +577,51 @@ fn op_resolve( let types = graph_data.follow_redirect(specifier); match graph_data.get(&types) { Some(ModuleEntry::Module { media_type, .. }) => { - Some((types, media_type)) + Some((types, *media_type)) } _ => None, } } - _ => Some((specifier, media_type)), + _ => Some((specifier, *media_type)), }, - _ => None, + _ => { + // handle npm:<package> urls + if let Ok(npm_ref) = + NpmPackageReference::from_specifier(&specifier) + { + if let Some(npm_resolver) = &state.maybe_npm_resolver { + Some(resolve_npm_package_reference_types( + &npm_ref, + npm_resolver, + )?) + } else { + None + } + } else { + None + } + } } } - _ => None, + _ => { + state.maybe_npm_resolver.as_ref().and_then(|npm_resolver| { + if npm_resolver.in_npm_package(&referrer) { + // we're in an npm package, so use node resolution + Some(NodeResolution::into_specifier_and_media_type( + node::node_resolve( + specifier, + &referrer, + node::NodeResolutionMode::Types, + npm_resolver, + ) + .ok() + .flatten(), + )) + } else { + None + } + }) + } }; let result = match maybe_result { Some((specifier, media_type)) => { @@ -599,6 +660,33 @@ fn op_resolve( Ok(resolved) } +pub fn resolve_npm_package_reference_types( + npm_ref: &NpmPackageReference, + npm_resolver: &NpmPackageResolver, +) -> Result<(ModuleSpecifier, MediaType), AnyError> { + let maybe_resolution = node_resolve_npm_reference( + npm_ref, + NodeResolutionMode::Types, + npm_resolver, + )?; + Ok(NodeResolution::into_specifier_and_media_type( + maybe_resolution, + )) +} + +#[op] +fn op_is_node_file(state: &mut OpState, path: String) -> bool { + let state = state.borrow::<State>(); + match ModuleSpecifier::parse(&path) { + Ok(specifier) => state + .maybe_npm_resolver + .as_ref() + .map(|r| r.in_npm_package(&specifier)) + .unwrap_or(false), + Err(_) => false, + } +} + #[derive(Debug, Deserialize, Eq, PartialEq)] struct RespondArgs { pub diagnostics: Diagnostics, @@ -629,13 +717,13 @@ pub fn exec(request: Request) -> Result<Response, AnyError> { .iter() .map(|(s, mt)| match s.scheme() { "data" | "blob" => { - let specifier_str = hash_url(s, mt); + let specifier_str = hash_url(s, *mt); remapped_specifiers.insert(specifier_str.clone(), s.clone()); specifier_str } _ => { let ext_media_type = get_tsc_media_type(s); - if mt != &ext_media_type { + if *mt != ext_media_type { let new_specifier = format!("{}{}", s, mt.as_ts_extension()); root_map.insert(new_specifier.clone(), s.clone()); new_specifier @@ -653,6 +741,7 @@ pub fn exec(request: Request) -> Result<Response, AnyError> { op_create_hash::decl(), op_emit::decl(), op_exists::decl(), + op_is_node_file::decl(), op_load::decl(), op_resolve::decl(), op_respond::decl(), @@ -662,6 +751,7 @@ pub fn exec(request: Request) -> Result<Response, AnyError> { request.graph_data.clone(), request.hash_data.clone(), request.maybe_config_specifier.clone(), + request.maybe_npm_resolver.clone(), request.maybe_tsbuildinfo.clone(), root_map.clone(), remapped_specifiers.clone(), @@ -771,6 +861,7 @@ mod tests { Arc::new(RwLock::new((&graph).into())), hash_data, None, + None, maybe_tsbuildinfo, HashMap::new(), HashMap::new(), @@ -820,6 +911,7 @@ mod tests { graph_data: Arc::new(RwLock::new((&graph).into())), hash_data, maybe_config_specifier: None, + maybe_npm_resolver: None, maybe_tsbuildinfo: None, root_names: vec![(specifier.clone(), MediaType::TypeScript)], }; @@ -865,7 +957,7 @@ mod tests { "data:application/javascript,console.log(\"Hello%20Deno\");", ) .unwrap(); - assert_eq!(hash_url(&specifier, &MediaType::JavaScript), "data:///d300ea0796bd72b08df10348e0b70514c021f2e45bfe59cec24e12e97cd79c58.js"); + assert_eq!(hash_url(&specifier, MediaType::JavaScript), "data:///d300ea0796bd72b08df10348e0b70514c021f2e45bfe59cec24e12e97cd79c58.js"); } #[test] diff --git a/cli/tsc/00_typescript.js b/cli/tsc/00_typescript.js index 8b22dd4513..c39e70e245 100644 --- a/cli/tsc/00_typescript.js +++ b/cli/tsc/00_typescript.js @@ -48949,12 +48949,23 @@ var ts; var emitResolver = createResolver(); var nodeBuilder = createNodeBuilder(); var globals = ts.createSymbolTable(); + var nodeGlobals = ts.createSymbolTable(); var undefinedSymbol = createSymbol(4 /* SymbolFlags.Property */, "undefined"); undefinedSymbol.declarations = []; var globalThisSymbol = createSymbol(1536 /* SymbolFlags.Module */, "globalThis", 8 /* CheckFlags.Readonly */); globalThisSymbol.exports = globals; globalThisSymbol.declarations = []; globals.set(globalThisSymbol.escapedName, globalThisSymbol); + var denoContext = ts.deno.createDenoForkContext({ + globals: globals, + nodeGlobals: nodeGlobals, + mergeSymbol: mergeSymbol, + ambientModuleSymbolRegex: ambientModuleSymbolRegex, + }); + var nodeGlobalThisSymbol = createSymbol(1536 /* SymbolFlags.Module */, "globalThis", 8 /* CheckFlags.Readonly */); + nodeGlobalThisSymbol.exports = denoContext.combinedGlobals; + nodeGlobalThisSymbol.declarations = []; + nodeGlobals.set(nodeGlobalThisSymbol.escapedName, nodeGlobalThisSymbol); var argumentsSymbol = createSymbol(4 /* SymbolFlags.Property */, "arguments"); var requireSymbol = createSymbol(4 /* SymbolFlags.Property */, "require"); /** This will be set during calls to `getResolvedSignature` where services determines an apparent number of arguments greater than what is actually provided. */ @@ -49464,6 +49475,7 @@ var ts; var reverseMappedCache = new ts.Map(); var inInferTypeForHomomorphicMappedType = false; var ambientModulesCache; + var nodeAmbientModulesCache; /** * List of every ambient module with a "*" wildcard. * Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches. @@ -49851,7 +49863,7 @@ var ts; // Do not report an error when merging `var globalThis` with the built-in `globalThis`, // as we will already report a "Declaration name conflicts..." error, and this error // won't make much sense. - if (target !== globalThisSymbol) { + if (target !== globalThisSymbol && target !== nodeGlobalThisSymbol) { error(source.declarations && ts.getNameOfDeclaration(source.declarations[0]), ts.Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target)); } } @@ -49950,7 +49962,7 @@ var ts; return; } if (ts.isGlobalScopeAugmentation(moduleAugmentation)) { - mergeSymbolTable(globals, moduleAugmentation.symbol.exports); + denoContext.mergeGlobalSymbolTable(moduleAugmentation, moduleAugmentation.symbol.exports); } else { // find a module that about to be augmented @@ -50631,7 +50643,12 @@ var ts; } } if (!excludeGlobals) { - result = lookup(globals, name, meaning); + if (denoContext.hasNodeSourceFile(lastLocation)) { + result = lookup(nodeGlobals, name, meaning); + } + if (!result) { + result = lookup(globals, name, meaning); + } } } if (!result) { @@ -51888,6 +51905,24 @@ var ts; : undefined; } function resolveExternalModule(location, moduleReference, moduleNotFoundError, errorNode, isForAugmentation) { + var _a; + if (isForAugmentation === void 0) { isForAugmentation = false; } + var result = resolveExternalModuleInner(location, moduleReference, moduleNotFoundError, errorNode, isForAugmentation); + // deno: attempt to resolve an npm package reference to its bare specifier w/ path ambient module + // when not found and the symbol has zero exports + if (moduleReference.startsWith("npm:") && (result == null || ((_a = result === null || result === void 0 ? void 0 : result.exports) === null || _a === void 0 ? void 0 : _a.size) === 0)) { + var npmPackageRef = ts.deno.tryParseNpmPackageReference(moduleReference); + if (npmPackageRef) { + var bareSpecifier = npmPackageRef.name + (npmPackageRef.subPath == null ? "" : "/" + npmPackageRef.subPath); + var ambientModule = tryFindAmbientModule(bareSpecifier, /*withAugmentations*/ true); + if (ambientModule) { + return ambientModule; + } + } + } + return result; + } + function resolveExternalModuleInner(location, moduleReference, moduleNotFoundError, errorNode, isForAugmentation) { var _a, _b, _c, _d, _e, _f, _g, _h; if (isForAugmentation === void 0) { isForAugmentation = false; } if (ts.startsWith(moduleReference, "@types/")) { @@ -52630,6 +52665,12 @@ var ts; if (typeof state_2 === "object") return state_2.value; } + if (denoContext.hasNodeSourceFile(enclosingDeclaration)) { + result = callback(nodeGlobals, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true); + if (result) { + return result; + } + } return callback(globals, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true); } function getQualifiedLeftMeaning(rightMeaning) { @@ -52713,7 +52754,11 @@ var ts; } }); // If there's no result and we're looking at the global symbol table, treat `globalThis` like an alias and try to lookup thru that - return result || (symbols === globals ? getCandidateListForSymbol(globalThisSymbol, globalThisSymbol, ignoreQualification) : undefined); + if (result) { + return result; + } + var globalSymbol = symbols === nodeGlobals ? nodeGlobalThisSymbol : symbols === globals ? globalThisSymbol : undefined; + return globalSymbol != null ? getCandidateListForSymbol(globalSymbol, globalSymbol, ignoreQualification) : undefined; } function getCandidateListForSymbol(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification) { if (isAccessible(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification)) { @@ -59159,7 +59204,7 @@ var ts; var indexInfos; if (symbol.exports) { members = getExportsOfSymbol(symbol); - if (symbol === globalThisSymbol) { + if (symbol === globalThisSymbol || symbol === nodeGlobalThisSymbol) { var varsOnly_1 = new ts.Map(); members.forEach(function (p) { var _a; @@ -60321,7 +60366,7 @@ var ts; if (ts.isExternalModuleNameRelative(moduleName)) { return undefined; } - var symbol = getSymbol(globals, '"' + moduleName + '"', 512 /* SymbolFlags.ValueModule */); + var symbol = getSymbol(denoContext.combinedGlobals, '"' + moduleName + '"', 512 /* SymbolFlags.ValueModule */); // merged symbol is module declaration symbol combined with all augmentations return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; } @@ -62988,6 +63033,10 @@ var ts; if (objectType.symbol === globalThisSymbol && propName !== undefined && globalThisSymbol.exports.has(propName) && (globalThisSymbol.exports.get(propName).flags & 418 /* SymbolFlags.BlockScoped */)) { error(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(propName), typeToString(objectType)); } + // deno: ensure condition and body match the above + else if (objectType.symbol === nodeGlobalThisSymbol && propName !== undefined && nodeGlobalThisSymbol.exports.has(propName) && (nodeGlobalThisSymbol.exports.get(propName).flags & 418 /* SymbolFlags.BlockScoped */)) { + error(accessExpression, ts.Diagnostics.Property_0_does_not_exist_on_type_1, ts.unescapeLeadingUnderscores(propName), typeToString(objectType)); + } else if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors && !(accessFlags & 128 /* AccessFlags.SuppressNoImplicitAnyError */)) { if (propName !== undefined && typeHasStaticProperty(propName, objectType)) { var typeName = typeToString(objectType); @@ -71860,7 +71909,7 @@ var ts; if (type.flags & 1048576 /* TypeFlags.Union */ || type.flags & 524288 /* TypeFlags.Object */ && declaredType !== type && !(declaredType === unknownType && isEmptyAnonymousObjectType(type)) || ts.isThisTypeParameter(type) - || type.flags & 2097152 /* TypeFlags.Intersection */ && ts.every(type.types, function (t) { return t.symbol !== globalThisSymbol; })) { + || type.flags & 2097152 /* TypeFlags.Intersection */ && ts.every(type.types, function (t) { return t.symbol !== globalThisSymbol && t.symbol !== nodeGlobalThisSymbol; })) { return filterType(type, function (t) { return isTypePresencePossible(t, name, assumeTrue); }); } return type; @@ -73019,6 +73068,9 @@ var ts; return undefinedType; } else if (includeGlobalThis) { + if (denoContext.hasNodeSourceFile(container)) { + return getTypeOfSymbol(nodeGlobalThisSymbol); + } return getTypeOfSymbol(globalThisSymbol); } } @@ -75690,6 +75742,11 @@ var ts; } return anyType; } + // deno: ensure condition matches above + if (leftType.symbol === nodeGlobalThisSymbol) { + // deno: don't bother with errors like above for simplicity + return anyType; + } if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) { reportNonexistentProperty(right, ts.isThisTypeParameter(leftType) ? apparentType : leftType, isUncheckedJS); } @@ -75997,7 +76054,7 @@ var ts; if (symbol) return symbol; var candidates; - if (symbols === globals) { + if (symbols === globals || symbols === nodeGlobals) { var primitives = ts.mapDefined(["string", "number", "boolean", "object", "bigint", "symbol"], function (s) { return symbols.has((s.charAt(0).toUpperCase() + s.slice(1))) ? createSymbol(524288 /* SymbolFlags.TypeAlias */, s) : undefined; }); @@ -86656,7 +86713,7 @@ var ts; // find immediate value referenced by exported name (SymbolFlags.Alias is set so we don't chase down aliases) var symbol = resolveName(exportedName, exportedName.escapedText, 111551 /* SymbolFlags.Value */ | 788968 /* SymbolFlags.Type */ | 1920 /* SymbolFlags.Namespace */ | 2097152 /* SymbolFlags.Alias */, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); - if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || symbol.declarations && isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { + if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || symbol === nodeGlobalThisSymbol || symbol.declarations && isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { error(exportedName, ts.Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, ts.idText(exportedName)); } else { @@ -87343,6 +87400,9 @@ var ts; isStaticSymbol = ts.isStatic(location); location = location.parent; } + if (denoContext.hasNodeSourceFile(location)) { + copySymbols(nodeGlobals, meaning); + } copySymbols(globals, meaning); } /** @@ -88717,25 +88777,24 @@ var ts; amalgamatedDuplicates = new ts.Map(); // Initialize global symbol table var augmentations; - for (var _b = 0, _c = host.getSourceFiles(); _b < _c.length; _b++) { - var file = _c[_b]; + var _loop_35 = function (file) { if (file.redirectInfo) { - continue; + return "continue"; } if (!ts.isExternalOrCommonJsModule(file)) { // It is an error for a non-external-module (i.e. script) to declare its own `globalThis`. // We can't use `builtinGlobals` for this due to synthetic expando-namespace generation in JS files. var fileGlobalThisSymbol = file.locals.get("globalThis"); if (fileGlobalThisSymbol === null || fileGlobalThisSymbol === void 0 ? void 0 : fileGlobalThisSymbol.declarations) { - for (var _d = 0, _e = fileGlobalThisSymbol.declarations; _d < _e.length; _d++) { - var declaration = _e[_d]; + for (var _h = 0, _j = fileGlobalThisSymbol.declarations; _h < _j.length; _h++) { + var declaration = _j[_h]; diagnostics.add(ts.createDiagnosticForNode(declaration, ts.Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0, "globalThis")); } } - mergeSymbolTable(globals, file.locals); + denoContext.mergeGlobalSymbolTable(file, file.locals); } if (file.jsGlobalAugmentations) { - mergeSymbolTable(globals, file.jsGlobalAugmentations); + denoContext.mergeGlobalSymbolTable(file, file.jsGlobalAugmentations); } if (file.patternAmbientModules && file.patternAmbientModules.length) { patternAmbientModules = ts.concatenate(patternAmbientModules, file.patternAmbientModules); @@ -88746,12 +88805,18 @@ var ts; if (file.symbol && file.symbol.globalExports) { // Merge in UMD exports with first-in-wins semantics (see #9771) var source = file.symbol.globalExports; + var isNodeFile_1 = denoContext.hasNodeSourceFile(file); source.forEach(function (sourceSymbol, id) { - if (!globals.has(id)) { - globals.set(id, sourceSymbol); + var envGlobals = isNodeFile_1 ? denoContext.getGlobalsForName(id) : globals; + if (!envGlobals.has(id)) { + envGlobals.set(id, sourceSymbol); } }); } + }; + for (var _b = 0, _c = host.getSourceFiles(); _b < _c.length; _b++) { + var file = _c[_b]; + _loop_35(file); } // We do global augmentations separately from module augmentations (and before creating global types) because they // 1. Affect global types. We won't have the correct global types until global augmentations are merged. Also, @@ -88762,10 +88827,10 @@ var ts; if (augmentations) { // merge _global_ module augmentations. // this needs to be done after global symbol table is initialized to make sure that all ambient modules are indexed - for (var _f = 0, augmentations_1 = augmentations; _f < augmentations_1.length; _f++) { - var list = augmentations_1[_f]; - for (var _g = 0, list_1 = list; _g < list_1.length; _g++) { - var augmentation = list_1[_g]; + for (var _d = 0, augmentations_1 = augmentations; _d < augmentations_1.length; _d++) { + var list = augmentations_1[_d]; + for (var _e = 0, list_1 = list; _e < list_1.length; _e++) { + var augmentation = list_1[_e]; if (!ts.isGlobalScopeAugmentation(augmentation.parent)) continue; mergeModuleAugmentation(augmentation); @@ -88778,6 +88843,7 @@ var ts; getSymbolLinks(argumentsSymbol).type = getGlobalType("IArguments", /*arity*/ 0, /*reportErrors*/ true); getSymbolLinks(unknownSymbol).type = errorType; getSymbolLinks(globalThisSymbol).type = createObjectType(16 /* ObjectFlags.Anonymous */, globalThisSymbol); + getSymbolLinks(nodeGlobalThisSymbol).type = createObjectType(16 /* ObjectFlags.Anonymous */, nodeGlobalThisSymbol); // Initialize special types globalArrayType = getGlobalType("Array", /*arity*/ 1, /*reportErrors*/ true); globalObjectType = getGlobalType("Object", /*arity*/ 0, /*reportErrors*/ true); @@ -88800,10 +88866,10 @@ var ts; if (augmentations) { // merge _nonglobal_ module augmentations. // this needs to be done after global symbol table is initialized to make sure that all ambient modules are indexed - for (var _h = 0, augmentations_2 = augmentations; _h < augmentations_2.length; _h++) { - var list = augmentations_2[_h]; - for (var _j = 0, list_2 = list; _j < list_2.length; _j++) { - var augmentation = list_2[_j]; + for (var _f = 0, augmentations_2 = augmentations; _f < augmentations_2.length; _f++) { + var list = augmentations_2[_f]; + for (var _g = 0, list_2 = list; _g < list_2.length; _g++) { + var augmentation = list_2[_g]; if (ts.isGlobalScopeAugmentation(augmentation.parent)) continue; mergeModuleAugmentation(augmentation); @@ -90373,17 +90439,30 @@ var ts; } return false; } - function getAmbientModules() { - if (!ambientModulesCache) { - ambientModulesCache = []; - globals.forEach(function (global, sym) { + function getAmbientModules(sourceFile) { + var isNode = denoContext.hasNodeSourceFile(sourceFile); + if (isNode) { + if (!nodeAmbientModulesCache) { + nodeAmbientModulesCache = getAmbientModules(denoContext.combinedGlobals); + } + return nodeAmbientModulesCache; + } + else { + if (!ambientModulesCache) { + ambientModulesCache = getAmbientModules(globals); + } + return ambientModulesCache; + } + function getAmbientModules(envGlobals) { + var cache = []; + envGlobals.forEach(function (global, sym) { // No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module. if (ambientModuleSymbolRegex.test(sym)) { - ambientModulesCache.push(global); + cache.push(global); } }); + return cache; } - return ambientModulesCache; } function checkGrammarImportClause(node) { var _a; @@ -90562,6 +90641,234 @@ var ts; } ts.signatureHasLiteralTypes = signatureHasLiteralTypes; })(ts || (ts = {})); +/* @internal */ +var ts; +(function (ts) { + var deno; + (function (deno) { + var isNodeSourceFile = function () { return false; }; + function setIsNodeSourceFileCallback(callback) { + isNodeSourceFile = callback; + } + deno.setIsNodeSourceFileCallback = setIsNodeSourceFileCallback; + // When upgrading: + // 1. Inspect all usages of "globals" and "globalThisSymbol" in checker.ts + // - Beware that `globalThisType` might refer to the global `this` type + // and not the global `globalThis` type + // 2. Inspect the types in @types/node for anything that might need to go below + // as well. + var nodeOnlyGlobalNames = new ts.Set([ + "NodeRequire", + "RequireResolve", + "RequireResolve", + "process", + "console", + "__filename", + "__dirname", + "require", + "module", + "exports", + "gc", + "BufferEncoding", + "BufferConstructor", + "WithImplicitCoercion", + "Buffer", + "Console", + "ImportMeta", + "setTimeout", + "setInterval", + "setImmediate", + "Global", + "AbortController", + "AbortSignal", + "Blob", + "BroadcastChannel", + "MessageChannel", + "MessagePort", + "Event", + "EventTarget", + "performance", + "TextDecoder", + "TextEncoder", + "URL", + "URLSearchParams", + ]); + function createDenoForkContext(_a) { + var mergeSymbol = _a.mergeSymbol, globals = _a.globals, nodeGlobals = _a.nodeGlobals, ambientModuleSymbolRegex = _a.ambientModuleSymbolRegex; + return { + hasNodeSourceFile: hasNodeSourceFile, + getGlobalsForName: getGlobalsForName, + mergeGlobalSymbolTable: mergeGlobalSymbolTable, + combinedGlobals: createNodeGlobalsSymbolTable(), + }; + function hasNodeSourceFile(node) { + if (!node) + return false; + var sourceFile = ts.getSourceFileOfNode(node); + return isNodeSourceFile(sourceFile); + } + function getGlobalsForName(id) { + // Node ambient modules are only accessible in the node code, + // so put them on the node globals + if (ambientModuleSymbolRegex.test(id)) + return nodeGlobals; + return nodeOnlyGlobalNames.has(id) ? nodeGlobals : globals; + } + function mergeGlobalSymbolTable(node, source, unidirectional) { + if (unidirectional === void 0) { unidirectional = false; } + var sourceFile = ts.getSourceFileOfNode(node); + var isNodeFile = hasNodeSourceFile(sourceFile); + source.forEach(function (sourceSymbol, id) { + var target = isNodeFile ? getGlobalsForName(id) : globals; + var targetSymbol = target.get(id); + target.set(id, targetSymbol ? mergeSymbol(targetSymbol, sourceSymbol, unidirectional) : sourceSymbol); + }); + } + function createNodeGlobalsSymbolTable() { + return new Proxy(globals, { + get: function (target, prop, receiver) { + if (prop === "get") { + return function (key) { + var _a; + return (_a = nodeGlobals.get(key)) !== null && _a !== void 0 ? _a : globals.get(key); + }; + } + else if (prop === "has") { + return function (key) { + return nodeGlobals.has(key) || globals.has(key); + }; + } + else if (prop === "size") { + var i_2 = 0; + forEachEntry(function () { + i_2++; + }); + return i_2; + } + else if (prop === "forEach") { + return function (action) { + forEachEntry(function (_a) { + var key = _a[0], value = _a[1]; + action(value, key); + }); + }; + } + else if (prop === "entries") { + return function () { + return getEntries(function (kv) { return kv; }); + }; + } + else if (prop === "keys") { + return function () { + return getEntries(function (kv) { return kv[0]; }); + }; + } + else if (prop === "values") { + return function () { + return getEntries(function (kv) { return kv[1]; }); + }; + } + else if (prop === Symbol.iterator) { + return function () { + // Need to convert this to an array since typescript targets ES5 + // and providing back the iterator won't work here. I don't want + // to change the target to ES6 because I'm not sure if that would + // surface any issues. + return ts.arrayFrom(getEntries(function (kv) { return kv; }))[Symbol.iterator](); + }; + } + else { + var value_3 = target[prop]; + if (value_3 instanceof Function) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return value_3.apply(this === receiver ? target : this, args); + }; + } + return value_3; + } + }, + }); + function forEachEntry(action) { + var iterator = getEntries(function (entry) { + action(entry); + }); + // drain the iterator to do the action + while (!iterator.next().done) { } + } + function getEntries(transform) { + var foundKeys, _i, _a, entries, next; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + foundKeys = new ts.Set(); + _i = 0, _a = [nodeGlobals.entries(), globals.entries()]; + _b.label = 1; + case 1: + if (!(_i < _a.length)) return [3 /*break*/, 6]; + entries = _a[_i]; + next = entries.next(); + _b.label = 2; + case 2: + if (!!next.done) return [3 /*break*/, 5]; + if (!!foundKeys.has(next.value[0])) return [3 /*break*/, 4]; + return [4 /*yield*/, transform(next.value)]; + case 3: + _b.sent(); + foundKeys.add(next.value[0]); + _b.label = 4; + case 4: + next = entries.next(); + return [3 /*break*/, 2]; + case 5: + _i++; + return [3 /*break*/, 1]; + case 6: return [2 /*return*/]; + } + }); + } + } + } + deno.createDenoForkContext = createDenoForkContext; + function tryParseNpmPackageReference(text) { + try { + return parseNpmPackageReference(text); + } + catch (_a) { + return undefined; + } + } + deno.tryParseNpmPackageReference = tryParseNpmPackageReference; + function parseNpmPackageReference(text) { + if (!text.startsWith("npm:")) { + throw new Error("Not an npm specifier: ".concat(text)); + } + text = text.replace(/^npm:/, ""); + var parts = text.split("/"); + var namePartLen = text.startsWith("@") ? 2 : 1; + if (parts.length < namePartLen) { + throw new Error("Not a valid package: ".concat(text)); + } + var nameParts = parts.slice(0, namePartLen); + var lastNamePart = nameParts.at(-1); + var lastAtIndex = lastNamePart.lastIndexOf("@"); + var versionReq = undefined; + if (lastAtIndex > 0) { + versionReq = lastNamePart.substring(lastAtIndex + 1); + nameParts[nameParts.length - 1] = lastNamePart.substring(0, lastAtIndex); + } + return { + name: nameParts.join("/"), + versionReq: versionReq, + subPath: parts.length > nameParts.length ? parts.slice(nameParts.length).join("/") : undefined, + }; + } + deno.parseNpmPackageReference = parseNpmPackageReference; + })(deno = ts.deno || (ts.deno = {})); +})(ts || (ts = {})); var ts; (function (ts) { function visitNode(node, visitor, test, lift) { @@ -121271,7 +121578,7 @@ var ts; } } // From ambient modules - for (var _f = 0, _g = program.getTypeChecker().getAmbientModules(); _f < _g.length; _f++) { + for (var _f = 0, _g = program.getTypeChecker().getAmbientModules(sourceFile); _f < _g.length; _f++) { var ambientModule = _g[_f]; if (ambientModule.declarations && ambientModule.declarations.length > 1) { addReferenceFromAmbientModule(ambientModule); @@ -124123,7 +124430,7 @@ var ts; }); // Sort by paths closest to importing file Name directory var sortedPaths = []; - var _loop_35 = function (directory) { + var _loop_36 = function (directory) { var directoryStart = ts.ensureTrailingDirectorySeparator(directory); var pathsInDirectory; allFileNames.forEach(function (_a, fileName) { @@ -124147,7 +124454,7 @@ var ts; }; var out_directory_1; for (var directory = ts.getDirectoryPath(importingFileName); allFileNames.size !== 0;) { - var state_11 = _loop_35(directory); + var state_11 = _loop_36(directory); directory = out_directory_1; if (state_11 === "break") break; @@ -124218,7 +124525,7 @@ var ts; } function tryGetModuleNameFromPaths(relativeToBaseUrl, paths, allowedEndings, host, compilerOptions) { for (var key in paths) { - var _loop_36 = function (patternText_1) { + var _loop_37 = function (patternText_1) { var pattern = ts.normalizePath(patternText_1); var indexOfStar = pattern.indexOf("*"); // In module resolution, if `pattern` itself has an extension, a file with that extension is looked up directly, @@ -124285,7 +124592,7 @@ var ts; }; for (var _i = 0, _a = paths[key]; _i < _a.length; _i++) { var patternText_1 = _a[_i]; - var state_12 = _loop_36(patternText_1); + var state_12 = _loop_37(patternText_1); if (typeof state_12 === "object") return state_12.value; } @@ -137638,7 +137945,8 @@ var ts; if (globalSymbol && checker.getTypeOfSymbolAtLocation(globalSymbol, sourceFile) === type) { return true; } - var globalThisSymbol = checker.resolveName("globalThis", /*location*/ undefined, 111551 /* SymbolFlags.Value */, /*excludeGlobals*/ false); + // deno: provide sourceFile so that it can figure out if it's a node or deno globalThis + var globalThisSymbol = checker.resolveName("globalThis", /*location*/ sourceFile, 111551 /* SymbolFlags.Value */, /*excludeGlobals*/ false); if (globalThisSymbol && checker.getTypeOfSymbolAtLocation(globalThisSymbol, sourceFile) === type) { return true; } diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index b39f56cf6a..7929d3b445 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -29,6 +29,7 @@ delete Object.prototype.__proto__; // This map stores that relationship, and the original can be restored by the // normalized specifier. // See: https://github.com/denoland/deno/issues/9277#issuecomment-769653834 + /** @type {Map<string, string>} */ const normalizedToOriginalMap = new Map(); /** @@ -40,6 +41,16 @@ delete Object.prototype.__proto__; "languageVersion" in value; } + /** + * @param {ts.ScriptTarget | ts.CreateSourceFileOptions | undefined} versionOrOptions + * @returns {ts.CreateSourceFileOptions} + */ + function getCreateSourceFileOptions(versionOrOptions) { + return isCreateSourceFileOptions(versionOrOptions) + ? versionOrOptions + : { languageVersion: versionOrOptions ?? ts.ScriptTarget.ESNext }; + } + function setLogDebug(debug, source) { logDebug = debug; if (source) { @@ -119,8 +130,23 @@ delete Object.prototype.__proto__; return result; } - // In the case of the LSP, this is initialized with the assets - // when snapshotting and never added to or removed after that. + class SpecifierIsCjsCache { + /** @type {Set<string>} */ + #cache = new Set(); + + /** @param {[string, ts.Extension]} param */ + add([specifier, ext]) { + if (ext === ".cjs" || ext === ".d.cts" || ext === ".cts") { + this.#cache.add(specifier); + } + } + + has(specifier) { + return this.#cache.has(specifier); + } + } + + // In the case of the LSP, this will only ever contain the assets. /** @type {Map<string, ts.SourceFile>} */ const sourceFileCache = new Map(); @@ -130,6 +156,181 @@ delete Object.prototype.__proto__; /** @type {Map<string, string>} */ const scriptVersionCache = new Map(); + /** @type {Map<string, boolean>} */ + const isNodeSourceFileCache = new Map(); + + const isCjsCache = new SpecifierIsCjsCache(); + + /** + * @param {ts.CompilerOptions | ts.MinimalResolutionCacheHost} settingsOrHost + * @returns {ts.CompilerOptions} + */ + function getCompilationSettings(settingsOrHost) { + if (typeof settingsOrHost.getCompilationSettings === "function") { + return settingsOrHost.getCompilationSettings(); + } + return /** @type {ts.CompilerOptions} */ (settingsOrHost); + } + + // We need to use a custom document registry in order to provide source files + // with an impliedNodeFormat to the ts language service + + /** @type {Map<string, ts.SourceFile} */ + const documentRegistrySourceFileCache = new Map(); + const { getKeyForCompilationSettings } = ts.createDocumentRegistry(); // reuse this code + /** @type {ts.DocumentRegistry} */ + const documentRegistry = { + acquireDocument( + fileName, + compilationSettingsOrHost, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const key = getKeyForCompilationSettings( + getCompilationSettings(compilationSettingsOrHost), + ); + return this.acquireDocumentWithKey( + fileName, + /** @type {ts.Path} */ (fileName), + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ); + }, + + acquireDocumentWithKey( + fileName, + path, + _compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const mapKey = path + key; + let sourceFile = documentRegistrySourceFileCache.get(mapKey); + if (!sourceFile || sourceFile.version !== version) { + sourceFile = ts.createLanguageServiceSourceFile( + fileName, + scriptSnapshot, + { + ...getCreateSourceFileOptions(sourceFileOptions), + impliedNodeFormat: isCjsCache.has(fileName) + ? ts.ModuleKind.CommonJS + : ts.ModuleKind.ESNext, + }, + version, + true, + scriptKind, + ); + documentRegistrySourceFileCache.set(mapKey, sourceFile); + } + return sourceFile; + }, + + updateDocument( + fileName, + compilationSettingsOrHost, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const key = getKeyForCompilationSettings( + getCompilationSettings(compilationSettingsOrHost), + ); + return this.updateDocumentWithKey( + fileName, + /** @type {ts.Path} */ (fileName), + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ); + }, + + updateDocumentWithKey( + fileName, + path, + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const mapKey = path + key; + let sourceFile = documentRegistrySourceFileCache.get(mapKey) ?? + this.acquireDocumentWithKey( + fileName, + path, + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ); + + if (sourceFile.version !== version) { + sourceFile = ts.updateLanguageServiceSourceFile( + sourceFile, + scriptSnapshot, + version, + scriptSnapshot.getChangeRange(sourceFile.scriptSnapShot), + ); + } + return sourceFile; + }, + + getKeyForCompilationSettings(settings) { + return getKeyForCompilationSettings(settings); + }, + + releaseDocument( + fileName, + compilationSettings, + scriptKind, + impliedNodeFormat, + ) { + const key = getKeyForCompilationSettings(compilationSettings); + return this.releaseDocumentWithKey( + /** @type {ts.Path} */ (fileName), + key, + scriptKind, + impliedNodeFormat, + ); + }, + + releaseDocumentWithKey(path, key, _scriptKind, _impliedNodeFormat) { + const mapKey = path + key; + documentRegistrySourceFileCache.remove(mapKey); + }, + + reportStats() { + return "[]"; + }, + }; + + ts.deno.setIsNodeSourceFileCallback((sourceFile) => { + const fileName = sourceFile.fileName; + let isNodeSourceFile = isNodeSourceFileCache.get(fileName); + if (isNodeSourceFile == null) { + const result = ops.op_is_node_file(fileName); + isNodeSourceFile = /** @type {boolean} */ (result); + isNodeSourceFileCache.set(fileName, isNodeSourceFile); + } + return isNodeSourceFile; + }); + /** @param {ts.DiagnosticRelatedInformation} diagnostic */ function fromRelatedInformation({ start, @@ -189,6 +390,10 @@ delete Object.prototype.__proto__; /** Diagnostics that are intentionally ignored when compiling TypeScript in * Deno, as they provide misleading or incorrect information. */ const IGNORED_DIAGNOSTICS = [ + // TS1452: 'resolution-mode' assertions are only supported when `moduleResolution` is `node16` or `nodenext`. + // We specify the resolution mode to be CommonJS for some npm files and this + // diagnostic gets generated even though we're using custom module resolution. + 1452, // TS2306: File '.../index.d.ts' is not a module. // We get this for `x-typescript-types` declaration files which don't export // anything. We prefer to treat these as modules with no exports. @@ -228,10 +433,12 @@ delete Object.prototype.__proto__; target: ts.ScriptTarget.ESNext, }; + // todo(dsherret): can we remove this and just use ts.OperationCanceledException? /** Error thrown on cancellation. */ class OperationCanceledError extends Error { } + // todo(dsherret): we should investigate if throttling is really necessary /** * Inspired by ThrottledCancellationToken in ts server. * @@ -291,13 +498,10 @@ delete Object.prototype.__proto__; _onError, _shouldCreateNewSourceFile, ) { + const createOptions = getCreateSourceFileOptions(languageVersion); debug( `host.getSourceFile("${specifier}", ${ - ts.ScriptTarget[ - isCreateSourceFileOptions(languageVersion) - ? languageVersion.languageVersion - : languageVersion - ] + ts.ScriptTarget[createOptions.languageVersion] })`, ); @@ -320,7 +524,12 @@ delete Object.prototype.__proto__; sourceFile = ts.createSourceFile( specifier, data, - languageVersion, + { + ...createOptions, + impliedNodeFormat: isCjsCache.has(specifier) + ? ts.ModuleKind.CommonJS + : ts.ModuleKind.ESNext, + }, false, scriptKind, ); @@ -355,6 +564,50 @@ delete Object.prototype.__proto__; getNewLine() { return "\n"; }, + resolveTypeReferenceDirectives( + typeDirectiveNames, + containingFilePath, + redirectedReference, + options, + containingFileMode, + ) { + return typeDirectiveNames.map((arg) => { + /** @type {ts.FileReference} */ + const fileReference = typeof arg === "string" + ? { + pos: -1, + end: -1, + fileName: arg, + } + : arg; + if (fileReference.fileName.startsWith("npm:")) { + /** @type {[string, ts.Extension] | undefined} */ + const resolved = ops.op_resolve({ + specifiers: [fileReference.fileName], + base: containingFilePath, + })?.[0]; + if (resolved) { + isCjsCache.add(resolved); + return { + primary: true, + resolvedFileName: resolved[0], + }; + } else { + return undefined; + } + } else { + return ts.resolveTypeReferenceDirective( + fileReference.fileName, + containingFilePath, + options, + host, + redirectedReference, + undefined, + containingFileMode ?? fileReference.resolutionMode, + ).resolvedTypeReferenceDirective; + } + }); + }, resolveModuleNames(specifiers, base) { debug(`host.resolveModuleNames()`); debug(` base: ${base}`); @@ -367,7 +620,12 @@ delete Object.prototype.__proto__; if (resolved) { const result = resolved.map((item) => { if (item) { + isCjsCache.add(item); const [resolvedFileName, extension] = item; + if (resolvedFileName.startsWith("node:")) { + // probably means the user doesn't have @types/node, so resolve to undefined + return undefined; + } return { resolvedFileName, extension, @@ -444,6 +702,23 @@ delete Object.prototype.__proto__; }, }; + // override the npm install @types package diagnostics to be deno specific + ts.setLocalizedDiagnosticMessages((() => { + const nodeMessage = "Cannot find name '{0}'."; // don't offer any suggestions + const jqueryMessage = + "Cannot find name '{0}'. Did you mean to import jQuery? Try adding `import $ from \"npm:jquery\";`."; + return { + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashno_2580": + nodeMessage, + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashno_2591": + nodeMessage, + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2581": + jqueryMessage, + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2592": + jqueryMessage, + }; + })()); + /** @type {Array<[string, number]>} */ const stats = []; let statsStart = 0; @@ -557,7 +832,25 @@ delete Object.prototype.__proto__; ...program.getOptionsDiagnostics(), ...program.getGlobalDiagnostics(), ...program.getSemanticDiagnostics(), - ].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)); + ].filter((diagnostic) => { + if (IGNORED_DIAGNOSTICS.includes(diagnostic.code)) { + return false; + } else if ( + diagnostic.code === 1259 && + typeof diagnostic.messageText === "string" && + diagnostic.messageText.startsWith( + "Module '\"deno:///missing_dependency.d.ts\"' can only be default-imported using the 'allowSyntheticDefaultImports' flag", + ) + ) { + // For now, ignore diagnostics like: + // > TS1259 [ERROR]: Module '"deno:///missing_dependency.d.ts"' can only be default-imported using the 'allowSyntheticDefaultImports' flag + // This diagnostic has surfaced due to supporting node cjs imports because this module does `export =`. + // See discussion in https://github.com/microsoft/TypeScript/pull/51136 + return false; + } else { + return true; + } + }); // emit the tsbuildinfo file // @ts-ignore: emitBuildInfo is not exposed (https://github.com/microsoft/TypeScript/issues/49871) @@ -922,13 +1215,14 @@ delete Object.prototype.__proto__; } hasStarted = true; cwd = rootUri; - languageService = ts.createLanguageService(host); + languageService = ts.createLanguageService(host, documentRegistry); setLogDebug(debugFlag, "TSLS"); debug("serverInit()"); } function serverRestart() { - languageService = ts.createLanguageService(host); + languageService = ts.createLanguageService(host, documentRegistry); + isNodeSourceFileCache.clear(); debug("serverRestart()"); } diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index b550c938bc..62a1bbdd2b 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -12,6 +12,7 @@ declare global { var normalizePath: (path: string) => string; interface SourceFile { version?: string; + fileName: string; } interface CompilerHost { @@ -24,6 +25,12 @@ declare global { } var performance: Performance; + + namespace deno { + function setIsNodeSourceFileCallback( + callback: (sourceFile: SourceFile) => boolean, + ); + } } namespace ts { diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 5178d81f7c..d9e5dcdbd5 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -28,6 +28,7 @@ pub use resolution::package_imports_resolve; pub use resolution::package_resolve; pub use resolution::NodeModuleKind; pub use resolution::DEFAULT_CONDITIONS; +pub use resolution::TYPES_CONDITIONS; pub trait NodePermissions { fn check_read(&mut self, path: &Path) -> Result<(), AnyError>; @@ -38,6 +39,7 @@ pub trait RequireNpmResolver { &self, specifier: &str, referrer: &Path, + conditions: &[&str], ) -> Result<PathBuf, AnyError>; fn resolve_package_folder_from_path( @@ -304,6 +306,7 @@ fn op_require_resolve_deno_dir( .resolve_package_folder_from_package( &request, &PathBuf::from(parent_filename), + DEFAULT_CONDITIONS, ) .ok() .map(|p| p.to_string_lossy().to_string()) diff --git a/ext/node/package_json.rs b/ext/node/package_json.rs index 81daa7ca71..85caac5f4a 100644 --- a/ext/node/package_json.rs +++ b/ext/node/package_json.rs @@ -50,6 +50,12 @@ impl PackageJson { path: PathBuf, ) -> Result<PackageJson, AnyError> { resolver.ensure_read_permission(&path)?; + Self::load_skip_read_permission(path) + } + + pub fn load_skip_read_permission( + path: PathBuf, + ) -> Result<PackageJson, AnyError> { let source = match std::fs::read_to_string(&path) { Ok(source) => source, Err(err) if err.kind() == ErrorKind::NotFound => { diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs index 1bde997093..de8f6e87b2 100644 --- a/ext/node/resolution.rs +++ b/ext/node/resolution.rs @@ -19,6 +19,7 @@ use crate::RequireNpmResolver; pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; +pub static TYPES_CONDITIONS: &[&str] = &["types"]; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NodeModuleKind { @@ -251,13 +252,17 @@ fn resolve_package_target_string( }; let package_json_url = ModuleSpecifier::from_file_path(package_json_path).unwrap(); - return package_resolve( + return match package_resolve( &export_target, &package_json_url, referrer_kind, conditions, npm_resolver, - ); + ) { + Ok(Some(path)) => Ok(path), + Ok(None) => Err(generic_error("not found")), + Err(err) => Err(err), + }; } } return Err(throw_invalid_package_target( @@ -593,7 +598,7 @@ pub fn package_resolve( referrer_kind: NodeModuleKind, conditions: &[&str], npm_resolver: &dyn RequireNpmResolver, -) -> Result<PathBuf, AnyError> { +) -> Result<Option<PathBuf>, AnyError> { let (package_name, package_subpath, _is_scoped) = parse_package_name(specifier, referrer)?; @@ -611,13 +616,15 @@ pub fn package_resolve( referrer_kind, conditions, npm_resolver, - ); + ) + .map(Some); } } let package_dir_path = npm_resolver.resolve_package_folder_from_package( &package_name, &referrer.to_file_path().unwrap(), + conditions, )?; let package_json_path = package_dir_path.join("package.json"); @@ -645,13 +652,16 @@ pub fn package_resolve( referrer_kind, conditions, npm_resolver, - ); + ) + .map(Some); } if package_subpath == "." { - return legacy_main_resolve(&package_json, referrer_kind); + return legacy_main_resolve(&package_json, referrer_kind, conditions); } - Ok(package_json.path.parent().unwrap().join(&package_subpath)) + Ok(Some( + package_json.path.parent().unwrap().join(&package_subpath), + )) } pub fn get_package_scope_config( @@ -706,41 +716,40 @@ fn file_exists(path: &Path) -> bool { pub fn legacy_main_resolve( package_json: &PackageJson, referrer_kind: NodeModuleKind, -) -> Result<PathBuf, AnyError> { - let maybe_main = package_json.main(referrer_kind); + conditions: &[&str], +) -> Result<Option<PathBuf>, AnyError> { + let is_types = conditions == TYPES_CONDITIONS; + let maybe_main = if is_types { + package_json.types.as_ref() + } else { + package_json.main(referrer_kind) + }; let mut guess; if let Some(main) = maybe_main { guess = package_json.path.parent().unwrap().join(main).clean(); if file_exists(&guess) { - return Ok(guess); + return Ok(Some(guess)); } let mut found = false; - // todo(dsherret): investigate exactly how node handles this - let endings = match referrer_kind { - NodeModuleKind::Cjs => vec![ - ".js", - ".cjs", - ".json", - ".node", - "/index.js", - "/index.cjs", - "/index.json", - "/index.node", - ], - NodeModuleKind::Esm => vec![ - ".js", - ".mjs", - ".json", - ".node", - "/index.js", - "/index.mjs", - ".cjs", - "/index.cjs", - "/index.json", - "/index.node", - ], + // todo(dsherret): investigate exactly how node and typescript handles this + let endings = if is_types { + match referrer_kind { + NodeModuleKind::Cjs => { + vec![".d.ts", ".d.cts", "/index.d.ts", "/index.d.cts"] + } + NodeModuleKind::Esm => vec![ + ".d.ts", + ".d.mts", + "/index.d.ts", + "/index.d.mts", + ".d.cts", + "/index.d.cts", + ], + } + } else { + vec![".js", "/index.js"] }; for ending in endings { guess = package_json @@ -757,21 +766,18 @@ pub fn legacy_main_resolve( if found { // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(guess); + return Ok(Some(guess)); } } - let index_file_names = match referrer_kind { - NodeModuleKind::Cjs => { - vec!["index.js", "index.cjs", "index.json", "index.node"] + let index_file_names = if is_types { + // todo(dsherret): investigate exactly how typescript does this + match referrer_kind { + NodeModuleKind::Cjs => vec!["index.d.ts", "index.d.cts"], + NodeModuleKind::Esm => vec!["index.d.ts", "index.d.mts", "index.d.cts"], } - NodeModuleKind::Esm => vec![ - "index.js", - "index.mjs", - "index.cjs", - "index.json", - "index.node", - ], + } else { + vec!["index.js"] }; for index_file_name in index_file_names { guess = package_json @@ -782,11 +788,11 @@ pub fn legacy_main_resolve( .clean(); if file_exists(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(guess); + return Ok(Some(guess)); } } - Err(generic_error("not found")) + Ok(None) } #[cfg(test)] diff --git a/test_util/Cargo.toml b/test_util/Cargo.toml index 114f52c616..ac38584996 100644 --- a/test_util/Cargo.toml +++ b/test_util/Cargo.toml @@ -35,6 +35,7 @@ tar = "0.4.38" tokio = { version = "1.21", features = ["full"] } tokio-rustls = "0.23" tokio-tungstenite = "0.16" +url = { version = "2.3.1", features = ["serde", "expose_internals"] } [target.'cfg(unix)'.dependencies] pty = "0.2.2" diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index b5d5099454..5bbccacd89 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -49,6 +49,7 @@ use tokio::net::TcpStream; use tokio_rustls::rustls; use tokio_rustls::TlsAcceptor; use tokio_tungstenite::accept_async; +use url::Url; pub mod assertions; pub mod lsp; @@ -120,10 +121,19 @@ pub fn napi_tests_path() -> PathBuf { root_path().join("test_napi") } +/// Test server registry url. +pub fn npm_registry_url() -> String { + "http://localhost:4545/npm/registry/".to_string() +} + pub fn std_path() -> PathBuf { root_path().join("test_util").join("std") } +pub fn std_file_url() -> String { + Url::from_directory_path(std_path()).unwrap().to_string() +} + pub fn target_dir() -> PathBuf { let current_exe = std::env::current_exe().unwrap(); let target_dir = current_exe.parent().unwrap().parent().unwrap(); diff --git a/test_util/src/lsp.rs b/test_util/src/lsp.rs index c61793f983..c0fd8ff1b0 100644 --- a/test_util/src/lsp.rs +++ b/test_util/src/lsp.rs @@ -1,5 +1,8 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use crate::npm_registry_url; +use crate::std_file_url; + use super::new_deno_dir; use super::TempDir; @@ -230,6 +233,8 @@ impl LspClient { let mut command = Command::new(deno_exe); command .env("DENO_DIR", deno_dir.path()) + .env("DENO_NODE_COMPAT_URL", std_file_url()) + .env("DENO_NPM_REGISTRY", npm_registry_url()) .arg("lsp") .stdin(Stdio::piped()) .stdout(Stdio::piped());