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(&params));
     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());