diff --git a/cli/build.rs b/cli/build.rs index 03aa47c517c607..131d404a7dc805 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -17,6 +17,7 @@ mod ts { use deno_core::error::AnyError; use deno_core::op; use deno_core::OpState; + use deno_runtime::deno_node::SUPPORTED_BUILTIN_NODE_MODULES; use regex::Regex; use serde::Deserialize; use serde_json::json; @@ -164,10 +165,16 @@ mod ts { #[op] fn op_build_info(state: &mut OpState) -> Value { let build_specifier = "asset:///bootstrap.ts"; + + let node_built_in_module_names = SUPPORTED_BUILTIN_NODE_MODULES + .iter() + .map(|s| s.name) + .collect::>(); let build_libs = state.borrow::>(); json!({ "buildSpecifier": build_specifier, "libs": build_libs, + "nodeBuiltInModuleNames": node_built_in_module_names, }) } diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index 4d255b01423054..c8fcaa22343965 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -65,7 +65,7 @@ impl FetchCacher { impl Loader for FetchCacher { fn get_cache_info(&self, specifier: &ModuleSpecifier) -> Option { - if specifier.scheme() == "npm" { + if matches!(specifier.scheme(), "npm" | "node") { return None; } @@ -101,7 +101,26 @@ impl Loader for FetchCacher { )); } - let specifier = specifier.clone(); + let specifier = + if let Some(module_name) = specifier.as_str().strip_prefix("node:") { + if module_name == "module" { + // the source code for "node:module" is built-in rather than + // being from deno_std like the other modules + return Box::pin(futures::future::ready(Ok(Some( + deno_graph::source::LoadResponse::External { + specifier: specifier.clone(), + }, + )))); + } + + match crate::node::resolve_builtin_node_module(module_name) { + Ok(specifier) => specifier, + Err(err) => return Box::pin(futures::future::ready(Err(err))), + } + } else { + specifier.clone() + }; + let permissions = if is_dynamic { self.dynamic_permissions.clone() } else { diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 22bddf137ea2fb..243fd6eef4fc09 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -8,7 +8,7 @@ use crate::cache; use crate::cache::TypeCheckCache; use crate::colors; use crate::errors::get_error_class_name; -use crate::npm::resolve_npm_package_reqs; +use crate::npm::resolve_graph_npm_info; use crate::npm::NpmPackageReference; use crate::npm::NpmPackageReq; use crate::proc_state::ProcState; @@ -25,6 +25,7 @@ use deno_graph::GraphImport; use deno_graph::MediaType; use deno_graph::ModuleGraph; use deno_graph::ModuleGraphError; +use deno_graph::ModuleKind; use deno_graph::Range; use deno_graph::Resolved; use deno_runtime::permissions::PermissionsContainer; @@ -54,7 +55,10 @@ pub enum ModuleEntry { #[derive(Debug, Default)] pub struct GraphData { modules: HashMap, + /// Specifiers that are built-in or external. + external_specifiers: HashSet, npm_packages: Vec, + has_node_builtin_specifier: bool, /// Map of first known referrer locations for each module. Used to enhance /// error messages. referrer_map: HashMap>, @@ -83,13 +87,12 @@ impl GraphData { let mut has_npm_specifier_in_graph = false; for (specifier, result) in graph.specifiers() { - if NpmPackageReference::from_specifier(specifier).is_ok() { - has_npm_specifier_in_graph = true; + if self.modules.contains_key(specifier) { continue; } - if self.modules.contains_key(specifier) { - continue; + if !self.has_node_builtin_specifier && specifier.scheme() == "node" { + self.has_node_builtin_specifier = true; } if let Some(found) = graph.redirects.get(specifier) { @@ -97,8 +100,19 @@ impl GraphData { self.modules.insert(specifier.clone(), module_entry); continue; } + match result { - Ok((_, _, media_type)) => { + Ok((_, module_kind, media_type)) => { + if module_kind == ModuleKind::External { + if !has_npm_specifier_in_graph + && NpmPackageReference::from_specifier(specifier).is_ok() + { + has_npm_specifier_in_graph = true; + } + self.external_specifiers.insert(specifier.clone()); + continue; // ignore npm and node specifiers + } + let module = graph.get(specifier).unwrap(); let code = match &module.maybe_source { Some(source) => source.clone(), @@ -147,7 +161,9 @@ impl GraphData { } if has_npm_specifier_in_graph { - self.npm_packages.extend(resolve_npm_package_reqs(graph)); + self + .npm_packages + .extend(resolve_graph_npm_info(graph).package_reqs); } } @@ -157,6 +173,11 @@ impl GraphData { self.modules.iter() } + /// Gets if the graph had a "node:" specifier. + pub fn has_node_builtin_specifier(&self) -> bool { + self.has_node_builtin_specifier + } + /// Gets the npm package requirements from all the encountered graphs /// in the order that they should be resolved. pub fn npm_package_reqs(&self) -> &Vec { @@ -195,13 +216,14 @@ impl GraphData { } } while let Some(specifier) = visiting.pop_front() { - if NpmPackageReference::from_specifier(specifier).is_ok() { - continue; // skip analyzing npm specifiers - } - let (specifier, entry) = match self.modules.get_key_value(specifier) { Some(pair) => pair, - None => return None, + None => { + if self.external_specifiers.contains(specifier) { + continue; + } + return None; + } }; result.insert(specifier, entry); match entry { @@ -281,6 +303,8 @@ impl GraphData { } Some(Self { modules, + external_specifiers: self.external_specifiers.clone(), + has_node_builtin_specifier: self.has_node_builtin_specifier, npm_packages: self.npm_packages.clone(), referrer_map, graph_imports: self.graph_imports.to_vec(), @@ -547,6 +571,14 @@ pub async fn create_graph_and_maybe_check( } if ps.options.type_check_mode() != TypeCheckMode::None { + // node built-in specifiers use the @types/node package to determine + // types, so inject that now after the lockfile has been written + if graph_data.has_node_builtin_specifier() { + ps.npm_resolver + .inject_synthetic_types_node_package() + .await?; + } + let ts_config_result = ps.options.resolve_ts_config_for_emit(TsConfigType::Check { lib: ps.options.ts_type_lib_window(), diff --git a/cli/lsp/cache.rs b/cli/lsp/cache.rs index 47f6e44ba9c479..23469a583ff8b4 100644 --- a/cli/lsp/cache.rs +++ b/cli/lsp/cache.rs @@ -68,7 +68,7 @@ impl CacheMetadata { &self, specifier: &ModuleSpecifier, ) -> Option>> { - if specifier.scheme() == "file" || specifier.scheme() == "npm" { + if matches!(specifier.scheme(), "file" | "npm" | "node") { return None; } let version = self diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index b4be63a552708d..4faff2faea6bd5 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -13,6 +13,7 @@ use super::tsc; use super::tsc::TsServer; use crate::args::LintOptions; +use crate::node; use crate::npm::NpmPackageReference; use crate::tools::lint::get_configured_rules; @@ -614,6 +615,8 @@ pub enum DenoDiagnostic { }, /// An error occurred when resolving the specifier string. ResolutionError(deno_graph::ResolutionError), + /// Invalid `node:` specifier. + InvalidNodeSpecifier(ModuleSpecifier), } impl DenoDiagnostic { @@ -641,6 +644,7 @@ impl DenoDiagnostic { }, ResolutionError::ResolverError { .. } => "resolver-error", }, + Self::InvalidNodeSpecifier(_) => "resolver-error", } } @@ -791,6 +795,7 @@ impl DenoDiagnostic { 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), + Self::InvalidNodeSpecifier(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Unknown Node built-in module: {}", specifier.path()), None), }; lsp::Diagnostic { range: *range, @@ -872,6 +877,30 @@ fn diagnose_resolved( ); } } + } else if let Some(module_name) = specifier.as_str().strip_prefix("node:") + { + if node::resolve_builtin_node_module(module_name).is_err() { + diagnostics.push( + DenoDiagnostic::InvalidNodeSpecifier(specifier.clone()) + .to_lsp_diagnostic(&range), + ); + } else if let Some(npm_resolver) = &snapshot.maybe_npm_resolver { + // check that a @types/node package exists in the resolver + let types_node_ref = + NpmPackageReference::from_str("npm:@types/node").unwrap(); + if npm_resolver + .resolve_package_folder_from_deno_module(&types_node_ref.req) + .is_err() + { + diagnostics.push( + DenoDiagnostic::NoCacheNpm( + types_node_ref, + ModuleSpecifier::parse("npm:@types/node").unwrap(), + ) + .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 diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index bc1e4a808d34a4..b99b64bfe3d911 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -770,6 +770,9 @@ pub struct Documents { maybe_resolver: Option, /// The npm package requirements. npm_reqs: Arc>, + /// Gets if any document had a node: specifier such that a @types/node package + /// should be injected. + has_injected_types_node_package: bool, /// Resolves a specifier to its final redirected to specifier. specifier_resolver: Arc, } @@ -785,6 +788,7 @@ impl Documents { imports: Default::default(), maybe_resolver: None, npm_reqs: Default::default(), + has_injected_types_node_package: false, specifier_resolver: Arc::new(SpecifierResolver::new(location)), } } @@ -925,6 +929,12 @@ impl Documents { (*self.npm_reqs).clone() } + /// Returns if a @types/node package was injected into the npm + /// resolver based on the state of the documents. + pub fn has_injected_types_node_package(&self) -> bool { + self.has_injected_types_node_package + } + /// Return a document for the specifier. pub fn get(&self, original_specifier: &ModuleSpecifier) -> Option { let specifier = self.specifier_resolver.resolve(original_specifier)?; @@ -985,11 +995,15 @@ impl Documents { /// tsc when type checking. pub fn resolve( &self, - specifiers: &[String], - referrer: &ModuleSpecifier, + specifiers: Vec, + referrer_doc: &AssetOrDocument, maybe_npm_resolver: Option<&NpmPackageResolver>, - ) -> Option>> { - let dependencies = self.get(referrer)?.0.dependencies.clone(); + ) -> Vec> { + let referrer = referrer_doc.specifier(); + let dependencies = match referrer_doc { + AssetOrDocument::Asset(_) => None, + AssetOrDocument::Document(doc) => Some(doc.0.dependencies.clone()), + }; let mut results = Vec::new(); for specifier in specifiers { if let Some(npm_resolver) = maybe_npm_resolver { @@ -997,7 +1011,7 @@ impl Documents { // we're in an npm package, so use node resolution results.push(Some(NodeResolution::into_specifier_and_media_type( node::node_resolve( - specifier, + &specifier, referrer, NodeResolutionMode::Types, npm_resolver, @@ -1009,15 +1023,28 @@ impl Documents { continue; } } - // handle npm: urls + if let Some(module_name) = specifier.strip_prefix("node:") { + if crate::node::resolve_builtin_node_module(module_name).is_ok() { + // return itself for node: specifiers because during type checking + // we resolve to the ambient modules in the @types/node package + // rather than deno_std/node + results.push(Some(( + ModuleSpecifier::parse(&specifier).unwrap(), + MediaType::Dts, + ))); + continue; + } + } if specifier.starts_with("asset:") { - if let Ok(specifier) = ModuleSpecifier::parse(specifier) { + if let Ok(specifier) = ModuleSpecifier::parse(&specifier) { let media_type = MediaType::from(&specifier); results.push(Some((specifier, media_type))); } else { results.push(None); } - } else if let Some(dep) = dependencies.deps.get(specifier) { + } else if let Some(dep) = + dependencies.as_ref().and_then(|d| d.deps.get(&specifier)) + { if let Resolved::Ok { specifier, .. } = &dep.maybe_type { results.push(self.resolve_dependency(specifier, maybe_npm_resolver)); } else if let Resolved::Ok { specifier, .. } = &dep.maybe_code { @@ -1026,12 +1053,12 @@ impl Documents { results.push(None); } } else if let Some(Resolved::Ok { specifier, .. }) = - self.resolve_imports_dependency(specifier) + self.resolve_imports_dependency(&specifier) { // clone here to avoid double borrow of self let specifier = specifier.clone(); results.push(self.resolve_dependency(&specifier, maybe_npm_resolver)); - } else if let Ok(npm_ref) = NpmPackageReference::from_str(specifier) { + } 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( @@ -1048,7 +1075,7 @@ impl Documents { results.push(None); } } - Some(results) + results } /// Update the location of the on disk cache for the document store. @@ -1125,6 +1152,7 @@ impl Documents { analyzed_specifiers: HashSet, pending_specifiers: VecDeque, npm_reqs: HashSet, + has_node_builtin_specifier: bool, } impl DocAnalyzer { @@ -1148,7 +1176,11 @@ impl Documents { fn analyze_doc(&mut self, specifier: &ModuleSpecifier, doc: &Document) { self.analyzed_specifiers.insert(specifier.clone()); - for dependency in doc.dependencies().values() { + for (name, dependency) in doc.dependencies() { + if !self.has_node_builtin_specifier && name.starts_with("node:") { + self.has_node_builtin_specifier = true; + } + if let Some(dep) = dependency.get_code() { self.add(dep, specifier); } @@ -1185,8 +1217,19 @@ impl Documents { } } + let mut npm_reqs = doc_analyzer.npm_reqs; + // Ensure a @types/node package exists when any module uses a node: specifier. + // Unlike on the command line, here we just add @types/node to the npm package + // requirements since this won't end up in the lockfile. + self.has_injected_types_node_package = doc_analyzer + .has_node_builtin_specifier + && !npm_reqs.iter().any(|r| r.name == "@types/node"); + if self.has_injected_types_node_package { + npm_reqs.insert(NpmPackageReq::from_str("@types/node").unwrap()); + } + self.dependents_map = Arc::new(doc_analyzer.dependents_map); - self.npm_reqs = Arc::new(doc_analyzer.npm_reqs); + self.npm_reqs = Arc::new(npm_reqs); self.dirty = false; file_system_docs.dirty = false; } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index ced8fb69930db8..3619f529c1f255 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -216,7 +216,7 @@ fn new_assets_map() -> Arc> { let asset = AssetDocument::new(specifier.clone(), v); (specifier, asset) }) - .collect(); + .collect::(); Arc::new(Mutex::new(assets)) } @@ -2728,28 +2728,29 @@ fn op_resolve( let state = state.borrow_mut::(); 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, - state.state_snapshot.maybe_npm_resolver.as_ref(), - ) { - Ok( - resolved - .into_iter() - .map(|o| { - o.map(|(s, mt)| (s.to_string(), mt.as_ts_extension().to_string())) - }) - .collect(), - ) - } else { - Err(custom_error( + let result = match state.get_asset_or_document(&referrer) { + Some(referrer_doc) => { + let resolved = state.state_snapshot.documents.resolve( + args.specifiers, + &referrer_doc, + state.state_snapshot.maybe_npm_resolver.as_ref(), + ); + Ok( + resolved + .into_iter() + .map(|o| { + o.map(|(s, mt)| (s.to_string(), mt.as_ts_extension().to_string())) + }) + .collect(), + ) + } + None => Err(custom_error( "NotFound", format!( "Error resolving. Referring specifier \"{}\" was not found.", args.base ), - )) + )), }; state.performance.measure(mark); @@ -2764,15 +2765,20 @@ fn op_respond(state: &mut OpState, args: Response) -> bool { } #[op] -fn op_script_names(state: &mut OpState) -> Vec { +fn op_script_names(state: &mut OpState) -> Vec { let state = state.borrow_mut::(); - state - .state_snapshot - .documents - .documents(true, true) - .into_iter() - .map(|d| d.specifier().clone()) - .collect() + let documents = &state.state_snapshot.documents; + let open_docs = documents.documents(true, true); + + let mut result = Vec::with_capacity(open_docs.len() + 1); + + if documents.has_injected_types_node_package() { + // ensure this is first so it resolves the node types first + result.push("asset:///node_types.d.ts".to_string()); + } + + result.extend(open_docs.into_iter().map(|d| d.specifier().to_string())); + result } #[derive(Debug, Deserialize, Serialize)] diff --git a/cli/node/mod.rs b/cli/node/mod.rs index aed639bc48769c..2125f670e188db 100644 --- a/cli/node/mod.rs +++ b/cli/node/mod.rs @@ -25,6 +25,7 @@ use deno_runtime::deno_node::package_imports_resolve; use deno_runtime::deno_node::package_resolve; use deno_runtime::deno_node::path_to_declaration_path; use deno_runtime::deno_node::NodeModuleKind; +use deno_runtime::deno_node::NodeModulePolyfill; use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NodeResolutionMode; use deno_runtime::deno_node::PackageJson; @@ -32,6 +33,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::SUPPORTED_BUILTIN_NODE_MODULES; use deno_runtime::permissions::PermissionsContainer; use once_cell::sync::Lazy; use regex::Regex; @@ -106,200 +108,6 @@ impl NodeResolution { } } -struct NodeModulePolyfill { - /// Name of the module like "assert" or "timers/promises" - name: &'static str, - - /// Specifier relative to the root of `deno_std` repo, like "node/asser.ts" - specifier: &'static str, -} - -static SUPPORTED_MODULES: &[NodeModulePolyfill] = &[ - NodeModulePolyfill { - name: "assert", - specifier: "node/assert.ts", - }, - NodeModulePolyfill { - name: "assert/strict", - specifier: "node/assert/strict.ts", - }, - NodeModulePolyfill { - name: "async_hooks", - specifier: "node/async_hooks.ts", - }, - NodeModulePolyfill { - name: "buffer", - specifier: "node/buffer.ts", - }, - NodeModulePolyfill { - name: "child_process", - specifier: "node/child_process.ts", - }, - NodeModulePolyfill { - name: "cluster", - specifier: "node/cluster.ts", - }, - NodeModulePolyfill { - name: "console", - specifier: "node/console.ts", - }, - NodeModulePolyfill { - name: "constants", - specifier: "node/constants.ts", - }, - NodeModulePolyfill { - name: "crypto", - specifier: "node/crypto.ts", - }, - NodeModulePolyfill { - name: "dgram", - specifier: "node/dgram.ts", - }, - NodeModulePolyfill { - name: "dns", - specifier: "node/dns.ts", - }, - NodeModulePolyfill { - name: "dns/promises", - specifier: "node/dns/promises.ts", - }, - NodeModulePolyfill { - name: "domain", - specifier: "node/domain.ts", - }, - NodeModulePolyfill { - name: "events", - specifier: "node/events.ts", - }, - NodeModulePolyfill { - name: "fs", - specifier: "node/fs.ts", - }, - NodeModulePolyfill { - name: "fs/promises", - specifier: "node/fs/promises.ts", - }, - NodeModulePolyfill { - name: "http", - specifier: "node/http.ts", - }, - NodeModulePolyfill { - name: "https", - specifier: "node/https.ts", - }, - NodeModulePolyfill { - name: "module", - // NOTE(bartlomieju): `module` is special, because we don't want to use - // `deno_std/node/module.ts`, but instead use a special shim that we - // provide in `ext/node`. - specifier: "[USE `deno_node::MODULE_ES_SHIM` to get this module]", - }, - NodeModulePolyfill { - name: "net", - specifier: "node/net.ts", - }, - NodeModulePolyfill { - name: "os", - specifier: "node/os.ts", - }, - NodeModulePolyfill { - name: "path", - specifier: "node/path.ts", - }, - NodeModulePolyfill { - name: "path/posix", - specifier: "node/path/posix.ts", - }, - NodeModulePolyfill { - name: "path/win32", - specifier: "node/path/win32.ts", - }, - NodeModulePolyfill { - name: "perf_hooks", - specifier: "node/perf_hooks.ts", - }, - NodeModulePolyfill { - name: "process", - specifier: "node/process.ts", - }, - NodeModulePolyfill { - name: "querystring", - specifier: "node/querystring.ts", - }, - NodeModulePolyfill { - name: "readline", - specifier: "node/readline.ts", - }, - NodeModulePolyfill { - name: "stream", - specifier: "node/stream.ts", - }, - NodeModulePolyfill { - name: "stream/consumers", - specifier: "node/stream/consumers.mjs", - }, - NodeModulePolyfill { - name: "stream/promises", - specifier: "node/stream/promises.mjs", - }, - NodeModulePolyfill { - name: "stream/web", - specifier: "node/stream/web.ts", - }, - NodeModulePolyfill { - name: "string_decoder", - specifier: "node/string_decoder.ts", - }, - NodeModulePolyfill { - name: "sys", - specifier: "node/sys.ts", - }, - NodeModulePolyfill { - name: "timers", - specifier: "node/timers.ts", - }, - NodeModulePolyfill { - name: "timers/promises", - specifier: "node/timers/promises.ts", - }, - NodeModulePolyfill { - name: "tls", - specifier: "node/tls.ts", - }, - NodeModulePolyfill { - name: "tty", - specifier: "node/tty.ts", - }, - NodeModulePolyfill { - name: "url", - specifier: "node/url.ts", - }, - NodeModulePolyfill { - name: "util", - specifier: "node/util.ts", - }, - NodeModulePolyfill { - name: "util/types", - specifier: "node/util/types.ts", - }, - NodeModulePolyfill { - name: "v8", - specifier: "node/v8.ts", - }, - NodeModulePolyfill { - name: "vm", - specifier: "node/vm.ts", - }, - NodeModulePolyfill { - name: "worker_threads", - specifier: "node/worker_threads.ts", - }, - NodeModulePolyfill { - name: "zlib", - specifier: "node/zlib.ts", - }, -]; - static NODE_COMPAT_URL: Lazy = Lazy::new(|| { if let Ok(url_str) = std::env::var("DENO_NODE_COMPAT_URL") { let url = Url::parse(&url_str).expect( @@ -315,7 +123,9 @@ pub static MODULE_ALL_URL: Lazy = Lazy::new(|| NODE_COMPAT_URL.join("node/module_all.ts").unwrap()); fn find_builtin_node_module(specifier: &str) -> Option<&NodeModulePolyfill> { - SUPPORTED_MODULES.iter().find(|m| m.name == specifier) + SUPPORTED_BUILTIN_NODE_MODULES + .iter() + .find(|m| m.name == specifier) } fn is_builtin_node_module(specifier: &str) -> bool { @@ -336,7 +146,7 @@ pub fn resolve_builtin_node_module(specifier: &str) -> Result { } Err(generic_error(format!( - "Unknown built-in Node module: {}", + "Unknown built-in \"node:\" module: {}", specifier ))) } diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index ce6fec5e51f0d2..9f41e508a8e2f2 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -14,7 +14,7 @@ pub use cache::NpmCache; pub use registry::NpmPackageVersionDistInfo; pub use registry::NpmRegistryApi; pub use registry::RealNpmRegistryApi; -pub use resolution::resolve_npm_package_reqs; +pub use resolution::resolve_graph_npm_info; pub use resolution::NpmPackageId; pub use resolution::NpmPackageReference; pub use resolution::NpmPackageReq; diff --git a/cli/npm/resolution/graph.rs b/cli/npm/resolution/graph.rs index 81ff58546f2e01..e2104814996a4e 100644 --- a/cli/npm/resolution/graph.rs +++ b/cli/npm/resolution/graph.rs @@ -1081,7 +1081,7 @@ fn tag_to_version_info<'a>( // explicit version. if tag == "latest" && info.name == "@types/node" { return get_resolved_package_version_and_info( - &NpmVersionReq::parse("18.0.0 - 18.8.2").unwrap(), + &NpmVersionReq::parse("18.0.0 - 18.11.18").unwrap(), info, parent, ); diff --git a/cli/npm/resolution/mod.rs b/cli/npm/resolution/mod.rs index f10678cbeec5ad..ed194bbac8910a 100644 --- a/cli/npm/resolution/mod.rs +++ b/cli/npm/resolution/mod.rs @@ -28,7 +28,7 @@ mod specifier; use graph::Graph; pub use snapshot::NpmResolutionSnapshot; -pub use specifier::resolve_npm_package_reqs; +pub use specifier::resolve_graph_npm_info; pub use specifier::NpmPackageReference; pub use specifier::NpmPackageReq; diff --git a/cli/npm/resolution/specifier.rs b/cli/npm/resolution/specifier.rs index 9a7f67cf8a25ef..6667c60dd11426 100644 --- a/cli/npm/resolution/specifier.rs +++ b/cli/npm/resolution/specifier.rs @@ -168,8 +168,15 @@ impl NpmVersionMatcher for NpmPackageReq { } } -/// Resolves the npm package requirements from the graph attempting. The order -/// returned is the order they should be resolved in. +pub struct GraphNpmInfo { + /// The order of these package requirements is the order they + /// should be resolved in. + pub package_reqs: Vec, + /// Gets if the graph had a built-in node specifier (ex. `node:fs`). + pub has_node_builtin_specifier: bool, +} + +/// Resolves npm specific information from the graph. /// /// This function will analyze the module graph for parent-most folder /// specifiers of all modules, then group npm specifiers together as found in @@ -211,7 +218,7 @@ impl NpmVersionMatcher for NpmPackageReq { /// /// Then it would resolve the npm specifiers in each of those groups according /// to that tree going by tree depth. -pub fn resolve_npm_package_reqs(graph: &ModuleGraph) -> Vec { +pub fn resolve_graph_npm_info(graph: &ModuleGraph) -> GraphNpmInfo { fn collect_specifiers<'a>( graph: &'a ModuleGraph, module: &'a deno_graph::Module, @@ -248,6 +255,7 @@ pub fn resolve_npm_package_reqs(graph: &ModuleGraph) -> Vec { graph: &ModuleGraph, specifier_graph: &mut SpecifierTree, seen: &mut HashSet, + has_node_builtin_specifier: &mut bool, ) { if !seen.insert(module.specifier.clone()) { return; // already visited @@ -267,12 +275,22 @@ pub fn resolve_npm_package_reqs(graph: &ModuleGraph) -> Vec { .dependencies .insert(get_folder_path_specifier(specifier)); } + + if !*has_node_builtin_specifier && specifier.scheme() == "node" { + *has_node_builtin_specifier = true; + } } // now visit all the dependencies for specifier in &specifiers { if let Some(module) = graph.get(specifier) { - analyze_module(module, graph, specifier_graph, seen); + analyze_module( + module, + graph, + specifier_graph, + seen, + has_node_builtin_specifier, + ); } } } @@ -284,9 +302,16 @@ pub fn resolve_npm_package_reqs(graph: &ModuleGraph) -> Vec { .collect::>(); let mut seen = HashSet::new(); let mut specifier_graph = SpecifierTree::default(); + let mut has_node_builtin_specifier = false; for root in &root_specifiers { if let Some(module) = graph.get(root) { - analyze_module(module, graph, &mut specifier_graph, &mut seen); + analyze_module( + module, + graph, + &mut specifier_graph, + &mut seen, + &mut has_node_builtin_specifier, + ); } } @@ -324,7 +349,10 @@ pub fn resolve_npm_package_reqs(graph: &ModuleGraph) -> Vec { } } - result + GraphNpmInfo { + has_node_builtin_specifier, + package_reqs: result, + } } fn get_folder_path_specifier(specifier: &ModuleSpecifier) -> ModuleSpecifier { @@ -979,7 +1007,8 @@ mod tests { }, ) .await; - let reqs = resolve_npm_package_reqs(&graph) + let reqs = resolve_graph_npm_info(&graph) + .package_reqs .into_iter() .map(|r| r.to_string()) .collect::>(); diff --git a/cli/npm/resolvers/mod.rs b/cli/npm/resolvers/mod.rs index 767187f5e5e5e2..4307f2b2eca96c 100644 --- a/cli/npm/resolvers/mod.rs +++ b/cli/npm/resolvers/mod.rs @@ -95,59 +95,51 @@ impl NpmPackageResolver { no_npm: bool, local_node_modules_path: Option, ) -> Self { - Self::new_with_maybe_snapshot( + Self::new_inner(cache, api, no_npm, local_node_modules_path, None, None) + } + + pub async fn new_with_maybe_lockfile( + cache: NpmCache, + api: RealNpmRegistryApi, + no_npm: bool, + local_node_modules_path: Option, + maybe_lockfile: Option>>, + ) -> Result { + let maybe_snapshot = if let Some(lockfile) = &maybe_lockfile { + if lockfile.lock().overwrite { + None + } else { + Some( + NpmResolutionSnapshot::from_lockfile(lockfile.clone(), &api) + .await + .with_context(|| { + format!( + "failed reading lockfile '{}'", + lockfile.lock().filename.display() + ) + })?, + ) + } + } else { + None + }; + Ok(Self::new_inner( cache, api, no_npm, local_node_modules_path, - None, - ) + maybe_snapshot, + maybe_lockfile, + )) } - /// This function will replace current resolver with a new one built from a - /// snapshot created out of the lockfile. - pub async fn add_lockfile_and_maybe_regenerate_snapshot( - &mut self, - lockfile: Arc>, - ) -> Result<(), AnyError> { - self.maybe_lockfile = Some(lockfile.clone()); - - if lockfile.lock().overwrite { - return Ok(()); - } - - let snapshot = - NpmResolutionSnapshot::from_lockfile(lockfile.clone(), &self.api) - .await - .with_context(|| { - format!( - "failed reading lockfile '{}'", - lockfile.lock().filename.display() - ) - })?; - if let Some(node_modules_folder) = &self.local_node_modules_path { - self.inner = Arc::new(LocalNpmPackageResolver::new( - self.cache.clone(), - self.api.clone(), - node_modules_folder.clone(), - Some(snapshot), - )); - } else { - self.inner = Arc::new(GlobalNpmPackageResolver::new( - self.cache.clone(), - self.api.clone(), - Some(snapshot), - )); - } - Ok(()) - } - - fn new_with_maybe_snapshot( + fn new_inner( cache: NpmCache, api: RealNpmRegistryApi, no_npm: bool, local_node_modules_path: Option, initial_snapshot: Option, + maybe_lockfile: Option>>, ) -> Self { let process_npm_state = NpmProcessState::take(); let local_node_modules_path = local_node_modules_path.or_else(|| { @@ -177,7 +169,7 @@ impl NpmPackageResolver { local_node_modules_path, api, cache, - maybe_lockfile: None, + maybe_lockfile, } } @@ -320,12 +312,13 @@ impl NpmPackageResolver { /// Gets a new resolver with a new snapshotted state. pub fn snapshotted(&self) -> Self { - Self::new_with_maybe_snapshot( + Self::new_inner( self.cache.clone(), self.api.clone(), self.no_npm, self.local_node_modules_path.clone(), Some(self.snapshot()), + None, ) } @@ -336,6 +329,19 @@ impl NpmPackageResolver { pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { self.inner.lock(lockfile) } + + pub async fn inject_synthetic_types_node_package( + &self, + ) -> Result<(), AnyError> { + // add and ensure this isn't added to the lockfile + self + .inner + .add_package_reqs(vec![NpmPackageReq::from_str("@types/node").unwrap()]) + .await?; + self.inner.cache_packages().await?; + + Ok(()) + } } impl RequireNpmResolver for NpmPackageResolver { diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 16e80d6f6eb66c..fd5ad484073b1d 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -23,7 +23,7 @@ use crate::graph_util::ModuleEntry; use crate::http_util::HttpClient; use crate::node; use crate::node::NodeResolution; -use crate::npm::resolve_npm_package_reqs; +use crate::npm::resolve_graph_npm_info; use crate::npm::NpmCache; use crate::npm::NpmPackageReference; use crate::npm::NpmPackageResolver; @@ -261,20 +261,16 @@ impl ProcState { http_client.clone(), progress_bar.clone(), ); - let maybe_lockfile = lockfile.as_ref().cloned(); - let mut npm_resolver = NpmPackageResolver::new( + let npm_resolver = NpmPackageResolver::new_with_maybe_lockfile( npm_cache.clone(), api, cli_options.no_npm(), cli_options .resolve_local_node_modules_folder() .with_context(|| "Resolving local node_modules folder.")?, - ); - if let Some(lockfile) = maybe_lockfile { - npm_resolver - .add_lockfile_and_maybe_regenerate_snapshot(lockfile) - .await?; - } + lockfile.as_ref().cloned(), + ) + .await?; let node_analysis_cache = NodeAnalysisCache::new(Some(dir.node_analysis_db_file_path())); @@ -424,7 +420,7 @@ impl ProcState { graph_data.entries().map(|(s, _)| s).cloned().collect() }; - let npm_package_reqs = { + let (npm_package_reqs, has_node_builtin_specifier) = { let mut graph_data = self.graph_data.write(); graph_data.add_graph(&graph); let check_js = self.options.check_js(); @@ -435,7 +431,10 @@ impl ProcState { check_js, ) .unwrap()?; - graph_data.npm_package_reqs().clone() + ( + graph_data.npm_package_reqs().clone(), + graph_data.has_node_builtin_specifier(), + ) }; if !npm_package_reqs.is_empty() { @@ -443,6 +442,15 @@ impl ProcState { self.prepare_node_std_graph().await?; } + if has_node_builtin_specifier + && self.options.type_check_mode() != TypeCheckMode::None + { + self + .npm_resolver + .inject_synthetic_types_node_package() + .await?; + } + drop(_pb_clear_guard); // type check if necessary @@ -614,6 +622,11 @@ impl ProcState { } } + // Built-in Node modules + if let Some(module_name) = specifier.strip_prefix("node:") { + return node::resolve_builtin_node_module(module_name); + } + // FIXME(bartlomieju): this is a hacky way to provide compatibility with REPL // and `Deno.core.evalContext` API. Ideally we should always have a referrer filled // but sadly that's not the case due to missing APIs in V8. @@ -732,9 +745,20 @@ impl ProcState { .await; // add the found npm package requirements to the npm resolver and cache them - let npm_package_reqs = resolve_npm_package_reqs(&graph); - if !npm_package_reqs.is_empty() { - self.npm_resolver.add_package_reqs(npm_package_reqs).await?; + let graph_npm_info = resolve_graph_npm_info(&graph); + if !graph_npm_info.package_reqs.is_empty() { + self + .npm_resolver + .add_package_reqs(graph_npm_info.package_reqs) + .await?; + } + if graph_npm_info.has_node_builtin_specifier + && self.options.type_check_mode() != TypeCheckMode::None + { + self + .npm_resolver + .inject_synthetic_types_node_package() + .await?; } Ok(graph) diff --git a/cli/tests/integration/check_tests.rs b/cli/tests/integration/check_tests.rs index 38301f079466f3..66433f81dadd14 100644 --- a/cli/tests/integration/check_tests.rs +++ b/cli/tests/integration/check_tests.rs @@ -64,6 +64,18 @@ itest!(check_static_response_json { exit_code: 0, }); +itest!(check_node_builtin_modules_ts { + args: "check --quiet check/node_builtin_modules/mod.ts", + output: "check/node_builtin_modules/mod.ts.out", + exit_code: 1, +}); + +itest!(check_node_builtin_modules_js { + args: "check --quiet check/node_builtin_modules/mod.js", + output: "check/node_builtin_modules/mod.js.out", + exit_code: 1, +}); + itest!(check_no_error_truncation { args: "check --quiet check/no_error_truncation/main.ts --config check/no_error_truncation/deno.json", output: "check/no_error_truncation/main.out", diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 3fba65c46a5003..4f08ad84bf653e 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -4407,6 +4407,190 @@ fn lsp_npm_specifier_unopened_file() { } } +#[test] +fn lsp_completions_node_specifier() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + let diagnostics = CollectedDiagnostics(did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import fs from 'node:non-existent';\n\n", + } + }), + )); + + let non_existent_diagnostics = diagnostics + .with_file_and_source("file:///a/file.ts", "deno") + .diagnostics + .into_iter() + .filter(|d| { + d.code == Some(lsp::NumberOrString::String("resolver-error".to_string())) + }) + .collect::>(); + assert_eq!( + json!(non_existent_diagnostics), + json!([ + { + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 34 + } + }, + "severity": 1, + "code": "resolver-error", + "source": "deno", + "message": "Unknown Node built-in module: non-existent" + } + ]) + ); + + // update to have node:fs import + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 0, + "character": 33 + } + }, + "text": "node:fs" + } + ] + }), + ) + .unwrap(); + let diagnostics = read_diagnostics(&mut client); + let cache_diagnostics = diagnostics + .with_file_and_source("file:///a/file.ts", "deno") + .diagnostics + .into_iter() + .filter(|d| { + d.code == Some(lsp::NumberOrString::String("no-cache-npm".to_string())) + }) + .collect::>(); + + assert_eq!( + json!(cache_diagnostics), + json!([ + { + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 24 + } + }, + "data": { + "specifier": "npm:@types/node", + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: \"@types/node\"." + } + ]) + ); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [ + { + "uri": "npm:@types/node", + } + ] + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + + 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": "fs." + } + ] + }), + ) + .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": 3 + }, + "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 == "writeFile")); + assert!(list.items.iter().any(|i| i.label == "writeFileSync")); + } else { + panic!("unexpected response"); + } + + shutdown(&mut client); +} + #[test] fn lsp_completions_registry() { let _g = http_server(); diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 564b7355fcc614..f12282b9fc89c1 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -3743,3 +3743,23 @@ fn stdio_streams_are_locked_in_permission_prompt() { assert_eq!(output, expected_output); }); } + +itest!(run_node_builtin_modules_ts { + args: "run --quiet run/node_builtin_modules/mod.ts", + output: "run/node_builtin_modules/mod.ts.out", + envs: vec![( + "DENO_NODE_COMPAT_URL".to_string(), + test_util::std_file_url() + )], + exit_code: 0, +}); + +itest!(run_node_builtin_modules_js { + args: "run --quiet run/node_builtin_modules/mod.js", + output: "run/node_builtin_modules/mod.js.out", + envs: vec![( + "DENO_NODE_COMPAT_URL".to_string(), + test_util::std_file_url() + )], + exit_code: 0, +}); diff --git a/cli/tests/testdata/check/node_builtin_modules/mod.js b/cli/tests/testdata/check/node_builtin_modules/mod.js new file mode 100644 index 00000000000000..196fb9be98c008 --- /dev/null +++ b/cli/tests/testdata/check/node_builtin_modules/mod.js @@ -0,0 +1,3 @@ +// @ts-check +import fs from "node:fs"; +const _data = fs.readFileSync("./node_builtin.js", 123); diff --git a/cli/tests/testdata/check/node_builtin_modules/mod.js.out b/cli/tests/testdata/check/node_builtin_modules/mod.js.out new file mode 100644 index 00000000000000..97786ebaeb2980 --- /dev/null +++ b/cli/tests/testdata/check/node_builtin_modules/mod.js.out @@ -0,0 +1,5 @@ +error: TS2769 [ERROR]: No overload matches this call. + [WILDCARD] +const _data = fs.readFileSync("./node_builtin.js", 123); + ~~~ + at file:///[WILDCARD]/node_builtin_modules/mod.js:3:52 diff --git a/cli/tests/testdata/check/node_builtin_modules/mod.ts b/cli/tests/testdata/check/node_builtin_modules/mod.ts new file mode 100644 index 00000000000000..0e62353fec5237 --- /dev/null +++ b/cli/tests/testdata/check/node_builtin_modules/mod.ts @@ -0,0 +1,9 @@ +import fs from "node:fs"; +const _data = fs.readFileSync("./node_builtin.js", 123); + +// check node:module specifically because for deno check it should +// resolve to the @types/node package, but at runtime it uses a different +// builtin object than deno_std +import { builtinModules } from "node:module"; +// should error about being string[] +const _testString: number[] = builtinModules; diff --git a/cli/tests/testdata/check/node_builtin_modules/mod.ts.out b/cli/tests/testdata/check/node_builtin_modules/mod.ts.out new file mode 100644 index 00000000000000..49b762cff88a4d --- /dev/null +++ b/cli/tests/testdata/check/node_builtin_modules/mod.ts.out @@ -0,0 +1,13 @@ +error: TS2769 [ERROR]: No overload matches this call. + [WILDCARD] +const _data = fs.readFileSync("./node_builtin.js", 123); + ~~~ + at file:///[WILDCARD]/node_builtin_modules/mod.ts:2:52 + +TS2322 [ERROR]: Type 'string[]' is not assignable to type 'number[]'. + Type 'string' is not assignable to type 'number'. +const _testString: number[] = builtinModules; + ~~~~~~~~~~~ + at file:///[WILDCARD]/node_builtin_modules/mod.ts:9:7 + +Found 2 errors. diff --git a/cli/tests/testdata/run/node_builtin_modules/mod.js b/cli/tests/testdata/run/node_builtin_modules/mod.js new file mode 100644 index 00000000000000..70e39be5689ae1 --- /dev/null +++ b/cli/tests/testdata/run/node_builtin_modules/mod.js @@ -0,0 +1,2 @@ +import process from "node:process"; +console.log(process.version); diff --git a/cli/tests/testdata/run/node_builtin_modules/mod.js.out b/cli/tests/testdata/run/node_builtin_modules/mod.js.out new file mode 100644 index 00000000000000..9dc2247f4d4339 --- /dev/null +++ b/cli/tests/testdata/run/node_builtin_modules/mod.js.out @@ -0,0 +1 @@ +v[WILDCARD].[WILDCARD].[WILDCARD] diff --git a/cli/tests/testdata/run/node_builtin_modules/mod.ts b/cli/tests/testdata/run/node_builtin_modules/mod.ts new file mode 100644 index 00000000000000..70e39be5689ae1 --- /dev/null +++ b/cli/tests/testdata/run/node_builtin_modules/mod.ts @@ -0,0 +1,2 @@ +import process from "node:process"; +console.log(process.version); diff --git a/cli/tests/testdata/run/node_builtin_modules/mod.ts.out b/cli/tests/testdata/run/node_builtin_modules/mod.ts.out new file mode 100644 index 00000000000000..9dc2247f4d4339 --- /dev/null +++ b/cli/tests/testdata/run/node_builtin_modules/mod.ts.out @@ -0,0 +1 @@ +v[WILDCARD].[WILDCARD].[WILDCARD] diff --git a/cli/tools/check.rs b/cli/tools/check.rs index 2ab4fe498b1a49..d669a736f34b9f 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -232,10 +232,16 @@ fn get_tsc_roots( graph_data: &GraphData, check_js: bool, ) -> Vec<(ModuleSpecifier, MediaType)> { - graph_data - .entries() - .into_iter() - .filter_map(|(specifier, module_entry)| match module_entry { + let mut result = Vec::new(); + if graph_data.has_node_builtin_specifier() { + // inject a specifier that will resolve node types + result.push(( + ModuleSpecifier::parse("asset:///node_types.d.ts").unwrap(), + MediaType::Dts, + )); + } + result.extend(graph_data.entries().into_iter().filter_map( + |(specifier, module_entry)| match module_entry { ModuleEntry::Module { media_type, code, .. } => match media_type { @@ -252,8 +258,9 @@ fn get_tsc_roots( _ => None, }, _ => None, - }) - .collect() + }, + )); + result } /// Matches the `@ts-check` pragma. diff --git a/cli/tsc/00_typescript.js b/cli/tsc/00_typescript.js index 789de3a66af134..3fbf4624a2b952 100644 --- a/cli/tsc/00_typescript.js +++ b/cli/tsc/00_typescript.js @@ -91389,10 +91389,15 @@ var ts; var deno; (function (deno) { var isNodeSourceFile = function () { return false; }; + var nodeBuiltInModuleNames = new ts.Set(); function setIsNodeSourceFileCallback(callback) { isNodeSourceFile = callback; } deno.setIsNodeSourceFileCallback = setIsNodeSourceFileCallback; + function setNodeBuiltInModuleNames(names) { + nodeBuiltInModuleNames = new ts.Set(names); + } + deno.setNodeBuiltInModuleNames = setNodeBuiltInModuleNames; // When upgrading: // 1. Inspect all usages of "globals" and "globalThisSymbol" in checker.ts // - Beware that `globalThisType` might refer to the global `this` type @@ -91452,8 +91457,16 @@ var ts; function getGlobalsForName(id) { // Node ambient modules are only accessible in the node code, // so put them on the node globals - if (ambientModuleSymbolRegex.test(id)) + if (ambientModuleSymbolRegex.test(id)) { + if (id.startsWith('"node:')) { + // check if it's a node specifier that we support + var name = id.slice(6, -1); + if (nodeBuiltInModuleNames.has(name)) { + return globals; + } + } return nodeGlobals; + } return nodeOnlyGlobalNames.has(id) ? nodeGlobals : globals; } function mergeGlobalSymbolTable(node, source, unidirectional) { diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index a0219fe13bce47..138b24ba0d8982 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -855,25 +855,7 @@ delete Object.prototype.__proto__; ...program.getOptionsDiagnostics(), ...program.getGlobalDiagnostics(), ...program.getSemanticDiagnostics(), - ].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; - } - }); + ].filter((diagnostic) => !IGNORED_DIAGNOSTICS.includes(diagnostic.code)); // emit the tsbuildinfo file // @ts-ignore: emitBuildInfo is not exposed (https://github.com/microsoft/TypeScript/issues/49871) @@ -1273,9 +1255,11 @@ delete Object.prototype.__proto__; // A build time only op that provides some setup information that is used to // ensure the snapshot is setup properly. - /** @type {{ buildSpecifier: string; libs: string[] }} */ + /** @type {{ buildSpecifier: string; libs: string[]; nodeBuiltInModuleNames: string[] }} */ + const { buildSpecifier, libs, nodeBuiltInModuleNames } = ops.op_build_info(); + + ts.deno.setNodeBuiltInModuleNames(nodeBuiltInModuleNames); - const { buildSpecifier, libs } = ops.op_build_info(); for (const lib of libs) { const specifier = `lib.${lib}.d.ts`; // we are using internal APIs here to "inject" our custom libraries into diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index ab6f95bd359553..03784ae84b3ea9 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -30,6 +30,7 @@ declare global { function setIsNodeSourceFileCallback( callback: (sourceFile: SourceFile) => boolean, ); + function setNodeBuiltInModuleNames(names: string[]); } } diff --git a/cli/tsc/diagnostics.rs b/cli/tsc/diagnostics.rs index fb7304c9bedae7..b3026d934793ab 100644 --- a/cli/tsc/diagnostics.rs +++ b/cli/tsc/diagnostics.rs @@ -299,9 +299,11 @@ impl Diagnostic { fn fmt_related_information(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(related_information) = self.related_information.as_ref() { - write!(f, "\n\n")?; - for info in related_information { - info.fmt_stack(f, 4)?; + if !related_information.is_empty() { + write!(f, "\n\n")?; + for info in related_information { + info.fmt_stack(f, 4)?; + } } } diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index 3da2f5f2520643..439a0de20d4ce0 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -165,6 +165,12 @@ pub static LAZILY_LOADED_STATIC_ASSETS: Lazy< "lib.webworker.iterable.d.ts", inc!("lib.webworker.iterable.d.ts"), ), + ( + // Special file that can be used to inject the @types/node package. + // This is used for `node:` specifiers. + "node_types.d.ts", + "/// \n", + ), ]) .iter() .cloned() @@ -599,117 +605,133 @@ fn op_resolve( "Error converting a string module specifier for \"op_resolve\".", )? }; - for specifier in &args.specifiers { + for specifier in args.specifiers { + if let Some(module_name) = specifier.strip_prefix("node:") { + if crate::node::resolve_builtin_node_module(module_name).is_ok() { + // return itself for node: specifiers because during type checking + // we resolve to the ambient modules in the @types/node package + // rather than deno_std/node + resolved.push((specifier, MediaType::Dts.to_string())); + continue; + } + } + if specifier.starts_with("asset:///") { - resolved.push(( - specifier.clone(), - MediaType::from(specifier).as_ts_extension().to_string(), - )); - } else { - let graph_data = state.graph_data.read(); - let resolved_dep = match graph_data.get_dependencies(&referrer) { - Some(dependencies) => dependencies.get(specifier).map(|d| { - if matches!(d.maybe_type, Resolved::Ok { .. }) { - &d.maybe_type - } else { - &d.maybe_code - } - }), - None => None, - }; - let maybe_result = match resolved_dep { - Some(Resolved::Ok { specifier, .. }) => { - let specifier = graph_data.follow_redirect(specifier); - match graph_data.get(&specifier) { - Some(ModuleEntry::Module { - media_type, - maybe_types, - .. - }) => match maybe_types { - Some(Resolved::Ok { specifier, .. }) => { - let types = graph_data.follow_redirect(specifier); - match graph_data.get(&types) { - Some(ModuleEntry::Module { media_type, .. }) => { - Some((types, *media_type)) - } - _ => None, + let media_type = + MediaType::from(&specifier).as_ts_extension().to_string(); + resolved.push((specifier, media_type)); + continue; + } + + let graph_data = state.graph_data.read(); + let resolved_dep = match graph_data.get_dependencies(&referrer) { + Some(dependencies) => dependencies.get(&specifier).map(|d| { + if matches!(d.maybe_type, Resolved::Ok { .. }) { + &d.maybe_type + } else { + &d.maybe_code + } + }), + None => None, + }; + let maybe_result = match resolved_dep { + Some(Resolved::Ok { specifier, .. }) => { + let specifier = graph_data.follow_redirect(specifier); + match graph_data.get(&specifier) { + Some(ModuleEntry::Module { + media_type, + maybe_types, + .. + }) => match maybe_types { + Some(Resolved::Ok { specifier, .. }) => { + let types = graph_data.follow_redirect(specifier); + match graph_data.get(&types) { + Some(ModuleEntry::Module { media_type, .. }) => { + Some((types, *media_type)) } + _ => None, } - _ => Some((specifier, *media_type)), - }, - _ => { - // handle npm: 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 - } + } + _ => Some((specifier, *media_type)), + }, + _ => { + // handle npm: 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 } - } - } - } - _ => { - 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, - NodeResolutionMode::Types, - npm_resolver, - &mut PermissionsContainer::allow_all(), - ) - .ok() - .flatten(), - )) } else { None } - }) + } } - }; - let result = match maybe_result { - Some((specifier, media_type)) => { - let specifier_str = match specifier.scheme() { - "data" | "blob" => { - let specifier_str = hash_url(&specifier, media_type); + } + _ => { + if let Some(npm_resolver) = state.maybe_npm_resolver.as_ref() { + 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, + NodeResolutionMode::Types, + npm_resolver, + &mut PermissionsContainer::allow_all(), + ) + .ok() + .flatten(), + )) + } else if let Ok(npm_ref) = NpmPackageReference::from_str(&specifier) + { + // this could occur when resolving npm:@types/node when it is + // injected and not part of the graph + Some(resolve_npm_package_reference_types(&npm_ref, npm_resolver)?) + } else { + None + } + } else { + None + } + } + }; + let result = match maybe_result { + Some((specifier, media_type)) => { + let specifier_str = match specifier.scheme() { + "data" | "blob" => { + let specifier_str = hash_url(&specifier, media_type); + state + .remapped_specifiers + .insert(specifier_str.clone(), specifier); + specifier_str + } + _ => { + if let Some(specifier_str) = + maybe_remap_specifier(&specifier, media_type) + { state .remapped_specifiers .insert(specifier_str.clone(), specifier); specifier_str + } else { + specifier.to_string() } - _ => { - if let Some(specifier_str) = - maybe_remap_specifier(&specifier, media_type) - { - state - .remapped_specifiers - .insert(specifier_str.clone(), specifier); - specifier_str - } else { - specifier.to_string() - } - } - }; - (specifier_str, media_type.as_ts_extension().into()) - } - None => ( - "deno:///missing_dependency.d.ts".to_string(), - ".d.ts".to_string(), - ), - }; - log::debug!("Resolved {} to {:?}", specifier, result); - resolved.push(result); - } + } + }; + (specifier_str, media_type.as_ts_extension().into()) + } + None => ( + "deno:///missing_dependency.d.ts".to_string(), + ".d.ts".to_string(), + ), + }; + log::debug!("Resolved {} to {:?}", specifier, result); + resolved.push(result); } Ok(resolved) diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 8a36e95fa27fde..3eda1895852cc6 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -675,3 +675,197 @@ fn op_require_break_on_next_statement(state: &mut OpState) { .borrow_mut() .wait_for_session_and_break_on_next_statement() } + +pub struct NodeModulePolyfill { + /// Name of the module like "assert" or "timers/promises" + pub name: &'static str, + + /// Specifier relative to the root of `deno_std` repo, like "node/assert.ts" + pub specifier: &'static str, +} + +pub static SUPPORTED_BUILTIN_NODE_MODULES: &[NodeModulePolyfill] = &[ + NodeModulePolyfill { + name: "assert", + specifier: "node/assert.ts", + }, + NodeModulePolyfill { + name: "assert/strict", + specifier: "node/assert/strict.ts", + }, + NodeModulePolyfill { + name: "async_hooks", + specifier: "node/async_hooks.ts", + }, + NodeModulePolyfill { + name: "buffer", + specifier: "node/buffer.ts", + }, + NodeModulePolyfill { + name: "child_process", + specifier: "node/child_process.ts", + }, + NodeModulePolyfill { + name: "cluster", + specifier: "node/cluster.ts", + }, + NodeModulePolyfill { + name: "console", + specifier: "node/console.ts", + }, + NodeModulePolyfill { + name: "constants", + specifier: "node/constants.ts", + }, + NodeModulePolyfill { + name: "crypto", + specifier: "node/crypto.ts", + }, + NodeModulePolyfill { + name: "dgram", + specifier: "node/dgram.ts", + }, + NodeModulePolyfill { + name: "dns", + specifier: "node/dns.ts", + }, + NodeModulePolyfill { + name: "dns/promises", + specifier: "node/dns/promises.ts", + }, + NodeModulePolyfill { + name: "domain", + specifier: "node/domain.ts", + }, + NodeModulePolyfill { + name: "events", + specifier: "node/events.ts", + }, + NodeModulePolyfill { + name: "fs", + specifier: "node/fs.ts", + }, + NodeModulePolyfill { + name: "fs/promises", + specifier: "node/fs/promises.ts", + }, + NodeModulePolyfill { + name: "http", + specifier: "node/http.ts", + }, + NodeModulePolyfill { + name: "https", + specifier: "node/https.ts", + }, + NodeModulePolyfill { + name: "module", + // NOTE(bartlomieju): `module` is special, because we don't want to use + // `deno_std/node/module.ts`, but instead use a special shim that we + // provide in `ext/node`. + specifier: "[USE `deno_node::MODULE_ES_SHIM` to get this module]", + }, + NodeModulePolyfill { + name: "net", + specifier: "node/net.ts", + }, + NodeModulePolyfill { + name: "os", + specifier: "node/os.ts", + }, + NodeModulePolyfill { + name: "path", + specifier: "node/path.ts", + }, + NodeModulePolyfill { + name: "path/posix", + specifier: "node/path/posix.ts", + }, + NodeModulePolyfill { + name: "path/win32", + specifier: "node/path/win32.ts", + }, + NodeModulePolyfill { + name: "perf_hooks", + specifier: "node/perf_hooks.ts", + }, + NodeModulePolyfill { + name: "process", + specifier: "node/process.ts", + }, + NodeModulePolyfill { + name: "querystring", + specifier: "node/querystring.ts", + }, + NodeModulePolyfill { + name: "readline", + specifier: "node/readline.ts", + }, + NodeModulePolyfill { + name: "stream", + specifier: "node/stream.ts", + }, + NodeModulePolyfill { + name: "stream/consumers", + specifier: "node/stream/consumers.mjs", + }, + NodeModulePolyfill { + name: "stream/promises", + specifier: "node/stream/promises.mjs", + }, + NodeModulePolyfill { + name: "stream/web", + specifier: "node/stream/web.ts", + }, + NodeModulePolyfill { + name: "string_decoder", + specifier: "node/string_decoder.ts", + }, + NodeModulePolyfill { + name: "sys", + specifier: "node/sys.ts", + }, + NodeModulePolyfill { + name: "timers", + specifier: "node/timers.ts", + }, + NodeModulePolyfill { + name: "timers/promises", + specifier: "node/timers/promises.ts", + }, + NodeModulePolyfill { + name: "tls", + specifier: "node/tls.ts", + }, + NodeModulePolyfill { + name: "tty", + specifier: "node/tty.ts", + }, + NodeModulePolyfill { + name: "url", + specifier: "node/url.ts", + }, + NodeModulePolyfill { + name: "util", + specifier: "node/util.ts", + }, + NodeModulePolyfill { + name: "util/types", + specifier: "node/util/types.ts", + }, + NodeModulePolyfill { + name: "v8", + specifier: "node/v8.ts", + }, + NodeModulePolyfill { + name: "vm", + specifier: "node/vm.ts", + }, + NodeModulePolyfill { + name: "worker_threads", + specifier: "node/worker_threads.ts", + }, + NodeModulePolyfill { + name: "zlib", + specifier: "node/zlib.ts", + }, +];