summaryrefslogtreecommitdiff
path: root/cli/lsp/config.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2024-07-18 18:16:35 -0400
committerGitHub <noreply@github.com>2024-07-18 18:16:35 -0400
commit3bda8eb4fe059fd79a522c9277a5a872f75dc270 (patch)
tree172d04d91223694c494b754d39c44ac5851575ac /cli/lsp/config.rs
parent1722e0aebfd830b7cbc0824ace5de0517072d0dc (diff)
fix(lsp): support npm workspaces and fix some resolution issues (#24627)
Makes the lsp use the same code as the rest of the cli.
Diffstat (limited to 'cli/lsp/config.rs')
-rw-r--r--cli/lsp/config.rs1168
1 files changed, 634 insertions, 534 deletions
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index 861a63d0c..5b549fc39 100644
--- a/cli/lsp/config.rs
+++ b/cli/lsp/config.rs
@@ -1,22 +1,27 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-use super::logging::lsp_log;
-use crate::args::discover_npmrc;
-use crate::args::CliLockfile;
-use crate::args::ConfigFile;
-use crate::args::FmtOptions;
-use crate::args::LintOptions;
-use crate::args::DENO_FUTURE;
-use crate::cache::FastInsecureHasher;
-use crate::file_fetcher::FileFetcher;
-use crate::lsp::logging::lsp_warn;
-use crate::tools::lint::get_configured_rules;
-use crate::tools::lint::ConfiguredRules;
-use crate::util::fs::canonicalize_path_maybe_not_exists;
use deno_ast::MediaType;
+use deno_config::fs::DenoConfigFs;
+use deno_config::fs::RealDenoConfigFs;
+use deno_config::glob::FilePatterns;
+use deno_config::glob::PathOrPatternSet;
+use deno_config::package_json::PackageJsonCache;
+use deno_config::workspace::CreateResolverOptions;
+use deno_config::workspace::PackageJsonDepResolution;
+use deno_config::workspace::SpecifiedImportMap;
+use deno_config::workspace::VendorEnablement;
+use deno_config::workspace::Workspace;
+use deno_config::workspace::WorkspaceDiscoverOptions;
+use deno_config::workspace::WorkspaceEmptyOptions;
+use deno_config::workspace::WorkspaceMemberContext;
+use deno_config::workspace::WorkspaceResolver;
+use deno_config::DenoJsonCache;
+use deno_config::FmtConfig;
use deno_config::FmtOptionsConfig;
use deno_config::TsConfig;
-use deno_core::normalize_path;
+use deno_core::anyhow::anyhow;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::Mutex;
use deno_core::serde::de::DeserializeOwned;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
@@ -24,25 +29,33 @@ use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::ModuleSpecifier;
-use deno_lint::linter::LintConfig;
use deno_npm::npm_rc::ResolvedNpmRc;
-use deno_runtime::deno_fs::DenoConfigFsAdapter;
-use deno_runtime::deno_fs::RealFs;
use deno_runtime::deno_node::PackageJson;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::fs_util::specifier_to_file_path;
-use deno_semver::package::PackageNv;
-use deno_semver::Version;
-use import_map::ImportMap;
use indexmap::IndexSet;
use lsp::Url;
use lsp_types::ClientCapabilities;
use std::collections::BTreeMap;
use std::collections::HashMap;
+use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tower_lsp::lsp_types as lsp;
+use super::logging::lsp_log;
+use crate::args::discover_npmrc_from_workspace;
+use crate::args::has_flag_env_var;
+use crate::args::CliLockfile;
+use crate::args::ConfigFile;
+use crate::args::DENO_FUTURE;
+use crate::cache::FastInsecureHasher;
+use crate::file_fetcher::FileFetcher;
+use crate::lsp::logging::lsp_warn;
+use crate::tools::lint::get_configured_rules;
+use crate::tools::lint::ConfiguredRules;
+use crate::util::fs::canonicalize_path_maybe_not_exists;
+
pub const SETTINGS_SECTION: &str = "deno";
fn is_true() -> bool {
@@ -934,10 +947,10 @@ impl Config {
}
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool {
- let config_file = self.tree.config_file_for_specifier(specifier);
- if let Some(cf) = config_file {
- if let Ok(files) = cf.to_exclude_files_config() {
- if !files.matches_specifier(specifier) {
+ let data = self.tree.data_for_specifier(specifier);
+ if let Some(data) = &data {
+ if let Ok(path) = specifier.to_file_path() {
+ if data.exclude_files.matches_path(&path) {
return false;
}
}
@@ -945,18 +958,16 @@ impl Config {
self
.settings
.specifier_enabled(specifier)
- .unwrap_or_else(|| config_file.is_some())
+ .unwrap_or_else(|| data.and_then(|d| d.maybe_deno_json()).is_some())
}
pub fn specifier_enabled_for_test(
&self,
specifier: &ModuleSpecifier,
) -> bool {
- if let Some(cf) = self.tree.config_file_for_specifier(specifier) {
- if let Ok(options) = cf.to_test_config() {
- if !options.files.matches_specifier(specifier) {
- return false;
- }
+ if let Some(data) = self.tree.data_for_specifier(specifier) {
+ if !data.test_config.files.matches_specifier(specifier) {
+ return false;
}
}
self.specifier_enabled(specifier)
@@ -1083,16 +1094,11 @@ impl LspTsConfig {
}
}
-#[derive(Debug, Clone)]
-pub struct LspPackageConfig {
- pub nv: PackageNv,
- pub exports: Value,
-}
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigWatchedFileType {
DenoJson,
Lockfile,
+ NpmRc,
PackageJson,
ImportMap,
}
@@ -1100,495 +1106,455 @@ pub enum ConfigWatchedFileType {
/// Contains the config file and dependent information.
#[derive(Debug, Clone)]
pub struct ConfigData {
- pub scope: ModuleSpecifier,
- pub config_file: Option<Arc<ConfigFile>>,
- pub fmt_options: Arc<FmtOptions>,
- pub lint_options: Arc<LintOptions>,
- pub lint_config: LintConfig,
+ pub scope: Arc<ModuleSpecifier>,
+ pub workspace: Arc<Workspace>,
+ pub member_ctx: Arc<WorkspaceMemberContext>,
+ pub fmt_config: Arc<deno_config::FmtConfig>,
+ pub lint_config: Arc<deno_config::LintConfig>,
+ pub test_config: Arc<deno_config::TestConfig>,
+ pub exclude_files: Arc<PathOrPatternSet>,
+ pub deno_lint_config: deno_lint::linter::LintConfig,
pub lint_rules: Arc<ConfiguredRules>,
pub ts_config: Arc<LspTsConfig>,
pub byonm: bool,
pub node_modules_dir: Option<PathBuf>,
pub vendor_dir: Option<PathBuf>,
pub lockfile: Option<Arc<CliLockfile>>,
- pub package_json: Option<Arc<PackageJson>>,
pub npmrc: Option<Arc<ResolvedNpmRc>>,
- pub import_map: Option<Arc<ImportMap>>,
- pub import_map_from_settings: bool,
- pub package_config: Option<Arc<LspPackageConfig>>,
- pub is_workspace_root: bool,
- pub workspace_root_dir: ModuleSpecifier,
- /// Workspace member directories. For a workspace root this will be a list of
- /// members. For a member this will be the same list, representing self and
- /// siblings. For a solitary package this will be `vec![self.scope]`. These
- /// are the list of packages to override with local resolutions for this
- /// config scope.
- pub workspace_members: Arc<Vec<ModuleSpecifier>>,
+ pub resolver: Arc<WorkspaceResolver>,
+ pub import_map_from_settings: Option<ModuleSpecifier>,
watched_files: HashMap<ModuleSpecifier, ConfigWatchedFileType>,
}
impl ConfigData {
async fn load(
- config_file_specifier: Option<&ModuleSpecifier>,
+ specified_config: Option<&Path>,
scope: &ModuleSpecifier,
- workspace_root: Option<&ConfigData>,
settings: &Settings,
- file_fetcher: Option<&Arc<FileFetcher>>,
+ file_fetcher: &Arc<FileFetcher>,
+ // sync requirement is because the lsp requires sync
+ cached_deno_config_fs: &(dyn DenoConfigFs + Sync),
+ deno_json_cache: &(dyn DenoJsonCache + Sync),
+ pkg_json_cache: &(dyn PackageJsonCache + Sync),
) -> Self {
- if let Some(specifier) = config_file_specifier {
- match ConfigFile::from_specifier(
- &DenoConfigFsAdapter::new(&RealFs),
- specifier.clone(),
- &deno_config::ConfigParseOptions::default(),
- ) {
- Ok(config_file) => {
- lsp_log!(
- " Resolved Deno configuration file: \"{}\"",
- config_file.specifier.as_str()
- );
- Self::load_inner(
- Some(config_file),
- scope,
- workspace_root,
- settings,
- file_fetcher,
- )
- .await
- }
- Err(err) => {
- lsp_warn!(
- " Couldn't read Deno configuration file \"{}\": {}",
- specifier.as_str(),
- err
- );
- let mut data = Self::load_inner(
- None,
- scope,
- workspace_root,
- settings,
- file_fetcher,
- )
- .await;
- data
- .watched_files
- .insert(specifier.clone(), ConfigWatchedFileType::DenoJson);
- let canonicalized_specifier = specifier
- .to_file_path()
- .ok()
- .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok())
- .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
- if let Some(specifier) = canonicalized_specifier {
- data
- .watched_files
- .insert(specifier, ConfigWatchedFileType::DenoJson);
+ let scope = Arc::new(scope.clone());
+ let discover_result = match scope.to_file_path() {
+ Ok(scope_dir_path) => {
+ let paths = [scope_dir_path];
+ Workspace::discover(
+ match specified_config {
+ Some(config_path) => {
+ deno_config::workspace::WorkspaceDiscoverStart::ConfigFile(
+ config_path,
+ )
+ }
+ None => {
+ deno_config::workspace::WorkspaceDiscoverStart::Paths(&paths)
+ }
+ },
+ &WorkspaceDiscoverOptions {
+ fs: cached_deno_config_fs,
+ additional_config_file_names: &[],
+ deno_json_cache: Some(deno_json_cache),
+ pkg_json_cache: Some(pkg_json_cache),
+ discover_pkg_json: !has_flag_env_var("DENO_NO_PACKAGE_JSON"),
+ config_parse_options: Default::default(),
+ maybe_vendor_override: None,
+ },
+ )
+ .map(Arc::new)
+ .map_err(AnyError::from)
+ }
+ Err(()) => Err(anyhow!("Scope '{}' was not a directory path.", scope)),
+ };
+ match discover_result {
+ Ok(workspace) => {
+ Self::load_inner(workspace, scope, settings, Some(file_fetcher)).await
+ }
+ Err(err) => {
+ lsp_warn!(" Couldn't open workspace \"{}\": {}", scope.as_str(), err);
+ let workspace = Arc::new(Workspace::empty(WorkspaceEmptyOptions {
+ root_dir: scope.clone(),
+ use_vendor_dir: VendorEnablement::Disable,
+ }));
+ let mut data = Self::load_inner(
+ workspace,
+ scope.clone(),
+ settings,
+ Some(file_fetcher),
+ )
+ .await;
+ // check if any of these need to be added to the workspace
+ let files = [
+ (
+ scope.join("deno.json").unwrap(),
+ ConfigWatchedFileType::DenoJson,
+ ),
+ (
+ scope.join("deno.jsonc").unwrap(),
+ ConfigWatchedFileType::DenoJson,
+ ),
+ (
+ scope.join("package.json").unwrap(),
+ ConfigWatchedFileType::PackageJson,
+ ),
+ ];
+ for (url, file_type) in files {
+ let Some(file_path) = url.to_file_path().ok() else {
+ continue;
+ };
+ if file_path.exists() {
+ data.watched_files.insert(url.clone(), file_type);
+ let canonicalized_specifier =
+ canonicalize_path_maybe_not_exists(&file_path)
+ .ok()
+ .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
+ if let Some(specifier) = canonicalized_specifier {
+ data.watched_files.insert(specifier, file_type);
+ }
}
- data
}
+ data
}
- } else {
- Self::load_inner(None, scope, workspace_root, settings, file_fetcher)
- .await
}
}
async fn load_inner(
- config_file: Option<ConfigFile>,
- scope: &ModuleSpecifier,
- workspace_root: Option<&ConfigData>,
+ workspace: Arc<Workspace>,
+ scope: Arc<ModuleSpecifier>,
settings: &Settings,
file_fetcher: Option<&Arc<FileFetcher>>,
) -> Self {
- let (settings, workspace_folder) = settings.get_for_specifier(scope);
- let mut watched_files = HashMap::with_capacity(6);
- if let Some(config_file) = &config_file {
- watched_files
- .entry(config_file.specifier.clone())
- .or_insert(ConfigWatchedFileType::DenoJson);
- }
- let config_file_path = config_file
- .as_ref()
- .and_then(|c| specifier_to_file_path(&c.specifier).ok());
- let config_file_canonicalized_specifier = config_file_path
- .as_ref()
- .and_then(|p| canonicalize_path_maybe_not_exists(p).ok())
- .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
- if let Some(specifier) = config_file_canonicalized_specifier {
- watched_files
- .entry(specifier)
- .or_insert(ConfigWatchedFileType::DenoJson);
- }
+ let (settings, workspace_folder) = settings.get_for_specifier(&scope);
+ let mut watched_files = HashMap::with_capacity(10);
+ let mut add_watched_file =
+ |specifier: ModuleSpecifier, file_type: ConfigWatchedFileType| {
+ let maybe_canonicalized = specifier
+ .to_file_path()
+ .ok()
+ .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok())
+ .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
+ if let Some(canonicalized) = maybe_canonicalized {
+ if canonicalized != specifier {
+ watched_files.entry(canonicalized).or_insert(file_type);
+ }
+ }
+ watched_files.entry(specifier).or_insert(file_type);
+ };
- let mut fmt_options = None;
- if let Some(workspace_data) = workspace_root {
- let has_own_fmt_options = config_file
- .as_ref()
- .is_some_and(|config_file| config_file.json.fmt.is_some());
- if !has_own_fmt_options {
- fmt_options = Some(workspace_data.fmt_options.clone())
- }
- }
- let fmt_options = fmt_options.unwrap_or_else(|| {
- config_file
- .as_ref()
- .and_then(|config_file| {
- config_file
- .to_fmt_config()
- .map(|o| FmtOptions::resolve(o, &Default::default()))
- .inspect_err(|err| {
- lsp_warn!(" Couldn't read formatter configuration: {}", err)
- })
- .ok()
- })
- .map(Arc::new)
- .unwrap_or_default()
- });
+ let member_ctx = workspace.resolve_start_ctx();
- let mut lint_options_rules = None;
- if let Some(workspace_data) = workspace_root {
- let has_own_lint_options = config_file
- .as_ref()
- .is_some_and(|config_file| config_file.json.lint.is_some());
- if !has_own_lint_options {
- lint_options_rules = Some((
- workspace_data.lint_options.clone(),
- workspace_data.lint_rules.clone(),
- ))
- }
+ if let Some(deno_json) = member_ctx.maybe_deno_json() {
+ lsp_log!(
+ " Resolved Deno configuration file: \"{}\"",
+ deno_json.specifier
+ );
+
+ add_watched_file(
+ deno_json.specifier.clone(),
+ ConfigWatchedFileType::DenoJson,
+ );
}
- let (lint_options, lint_rules) = lint_options_rules.unwrap_or_else(|| {
- let lint_options = config_file
- .as_ref()
- .and_then(|config_file| {
- config_file
- .to_lint_config()
- .map(|o| LintOptions::resolve(o, &Default::default()))
- .inspect_err(|err| {
- lsp_warn!(" Couldn't read lint configuration: {}", err)
- })
- .ok()
- })
- .map(Arc::new)
- .unwrap_or_default();
- let lint_rules = Arc::new(get_configured_rules(
- lint_options.rules.clone(),
- config_file.as_ref(),
- ));
- (lint_options, lint_rules)
- });
- let ts_config = LspTsConfig::new(config_file.as_ref());
+ if let Some(pkg_json) = member_ctx.maybe_pkg_json() {
+ lsp_log!(" Resolved package.json: \"{}\"", pkg_json.specifier());
- let lint_config = if ts_config.inner.0.get("jsx").and_then(|v| v.as_str())
- == Some("react")
- {
- let default_jsx_factory =
- ts_config.inner.0.get("jsxFactory").and_then(|v| v.as_str());
- let default_jsx_fragment_factory = ts_config
- .inner
- .0
- .get("jsxFragmentFactory")
- .and_then(|v| v.as_str());
- deno_lint::linter::LintConfig {
- default_jsx_factory: default_jsx_factory.map(String::from),
- default_jsx_fragment_factory: default_jsx_fragment_factory
- .map(String::from),
- }
- } else {
- deno_lint::linter::LintConfig {
- default_jsx_factory: None,
- default_jsx_fragment_factory: None,
- }
- };
+ add_watched_file(
+ pkg_json.specifier(),
+ ConfigWatchedFileType::PackageJson,
+ );
+ }
- let vendor_dir = if let Some(workspace_root) = workspace_root {
- workspace_root.vendor_dir.clone()
- } else {
- config_file.as_ref().and_then(|c| {
- if c.vendor() == Some(true) {
- Some(c.specifier.to_file_path().ok()?.parent()?.join("vendor"))
- } else {
- None
+ // todo(dsherret): cache this so we don't load this so many times
+ let npmrc = discover_npmrc_from_workspace(&workspace)
+ .inspect(|(_, path)| {
+ if let Some(path) = path {
+ lsp_log!(" Resolved .npmrc: \"{}\"", path.display());
+
+ if let Ok(specifier) = ModuleSpecifier::from_file_path(path) {
+ add_watched_file(specifier, ConfigWatchedFileType::NpmRc);
+ }
}
})
- };
+ .inspect_err(|err| {
+ lsp_warn!(" Couldn't read .npmrc for \"{scope}\": {err}");
+ })
+ .map(|(r, _)| r)
+ .ok();
+ let default_file_patterns = scope
+ .to_file_path()
+ .map(FilePatterns::new_with_base)
+ .unwrap_or_else(|_| FilePatterns::new_with_base(PathBuf::from("/")));
+ let fmt_config = Arc::new(
+ member_ctx
+ .to_fmt_config(FilePatterns::new_with_base(member_ctx.dir_path()))
+ .inspect_err(|err| {
+ lsp_warn!(" Couldn't read formatter configuration: {}", err)
+ })
+ .ok()
+ .unwrap_or_else(|| deno_config::FmtConfig {
+ options: Default::default(),
+ files: default_file_patterns.clone(),
+ }),
+ );
+ let lint_config = Arc::new(
+ member_ctx
+ .to_lint_config(FilePatterns::new_with_base(member_ctx.dir_path()))
+ .inspect_err(|err| {
+ lsp_warn!(" Couldn't read lint configuration: {}", err)
+ })
+ .ok()
+ .unwrap_or_else(|| deno_config::LintConfig {
+ options: Default::default(),
+ files: default_file_patterns.clone(),
+ }),
+ );
+ let lint_rules = Arc::new(get_configured_rules(
+ lint_config.options.rules.clone(),
+ member_ctx.maybe_deno_json().map(|c| c.as_ref()),
+ ));
+ let test_config = Arc::new(
+ member_ctx
+ .to_test_config(FilePatterns::new_with_base(member_ctx.dir_path()))
+ .inspect_err(|err| {
+ lsp_warn!(" Couldn't read test configuration: {}", err)
+ })
+ .ok()
+ .unwrap_or_else(|| deno_config::TestConfig {
+ files: default_file_patterns.clone(),
+ }),
+ );
+ let exclude_files = Arc::new(
+ workspace
+ .resolve_config_excludes()
+ .inspect_err(|err| {
+ lsp_warn!(" Couldn't read config excludes: {}", err)
+ })
+ .ok()
+ .unwrap_or_default(),
+ );
- // Load lockfile
- let lockfile = if let Some(workspace_root) = workspace_root {
- workspace_root.lockfile.clone()
- } else {
- config_file
- .as_ref()
- .and_then(resolve_lockfile_from_config)
- .map(Arc::new)
- };
+ let ts_config =
+ LspTsConfig::new(workspace.root_deno_json().map(|c| c.as_ref()));
+
+ let deno_lint_config =
+ if ts_config.inner.0.get("jsx").and_then(|v| v.as_str()) == Some("react")
+ {
+ let default_jsx_factory =
+ ts_config.inner.0.get("jsxFactory").and_then(|v| v.as_str());
+ let default_jsx_fragment_factory = ts_config
+ .inner
+ .0
+ .get("jsxFragmentFactory")
+ .and_then(|v| v.as_str());
+ deno_lint::linter::LintConfig {
+ default_jsx_factory: default_jsx_factory.map(String::from),
+ default_jsx_fragment_factory: default_jsx_fragment_factory
+ .map(String::from),
+ }
+ } else {
+ deno_lint::linter::LintConfig {
+ default_jsx_factory: None,
+ default_jsx_fragment_factory: None,
+ }
+ };
+
+ let vendor_dir = workspace.vendor_dir_path().cloned();
+ // todo(dsherret): add caching so we don't load this so many times
+ let lockfile = resolve_lockfile_from_workspace(&workspace).map(Arc::new);
if let Some(lockfile) = &lockfile {
if let Ok(specifier) = ModuleSpecifier::from_file_path(&lockfile.filename)
{
- watched_files
- .entry(specifier)
- .or_insert(ConfigWatchedFileType::Lockfile);
+ add_watched_file(specifier, ConfigWatchedFileType::Lockfile);
}
}
- let lockfile_canonicalized_specifier = lockfile
- .as_ref()
- .and_then(|lockfile| {
- canonicalize_path_maybe_not_exists(&lockfile.filename).ok()
- })
- .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
- if let Some(specifier) = lockfile_canonicalized_specifier {
- watched_files
- .entry(specifier)
- .or_insert(ConfigWatchedFileType::Lockfile);
+
+ let byonm = std::env::var("DENO_UNSTABLE_BYONM").is_ok()
+ || workspace.has_unstable("byonm")
+ || (*DENO_FUTURE
+ && workspace.package_jsons().next().is_some()
+ && workspace.node_modules_dir().is_none());
+ if byonm {
+ lsp_log!(" Enabled 'bring your own node_modules'.");
}
+ let node_modules_dir = resolve_node_modules_dir(&workspace, byonm);
- // Load package.json
- let mut package_json = None;
- let package_json_path = specifier_to_file_path(scope)
- .ok()
- .map(|p| p.join("package.json"));
- if let Some(path) = &package_json_path {
- if let Ok(specifier) = ModuleSpecifier::from_file_path(path) {
- watched_files
- .entry(specifier)
- .or_insert(ConfigWatchedFileType::PackageJson);
+ // Mark the import map as a watched file
+ if let Some(import_map_specifier) =
+ workspace.to_import_map_specifier().ok().flatten()
+ {
+ add_watched_file(
+ import_map_specifier.clone(),
+ ConfigWatchedFileType::ImportMap,
+ );
+ }
+ // attempt to create a resolver for the workspace
+ let pkg_json_dep_resolution = if byonm {
+ PackageJsonDepResolution::Disabled
+ } else {
+ // todo(dsherret): this should be false for nodeModulesDir: true
+ PackageJsonDepResolution::Enabled
+ };
+ let mut import_map_from_settings = {
+ let is_config_import_map = member_ctx
+ .maybe_deno_json()
+ .map(|c| c.is_an_import_map() || c.json.import_map.is_some())
+ .or_else(|| {
+ workspace
+ .root_deno_json()
+ .map(|c| c.is_an_import_map() || c.json.import_map.is_some())
+ })
+ .unwrap_or(false);
+ if is_config_import_map {
+ None
+ } else {
+ settings.import_map.as_ref().and_then(|import_map_str| {
+ Url::parse(import_map_str)
+ .ok()
+ .or_else(|| workspace_folder?.join(import_map_str).ok())
+ })
}
- let package_json_canonicalized_specifier =
- canonicalize_path_maybe_not_exists(path)
- .ok()
- .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
- if let Some(specifier) = package_json_canonicalized_specifier {
- watched_files
- .entry(specifier)
- .or_insert(ConfigWatchedFileType::PackageJson);
+ };
+
+ let specified_import_map = {
+ let is_config_import_map = member_ctx
+ .maybe_deno_json()
+ .map(|c| c.is_an_import_map() || c.json.import_map.is_some())
+ .or_else(|| {
+ workspace
+ .root_deno_json()
+ .map(|c| c.is_an_import_map() || c.json.import_map.is_some())
+ })
+ .unwrap_or(false);
+ if is_config_import_map {
+ import_map_from_settings = None;
}
- if let Ok(source) = std::fs::read_to_string(path) {
- match PackageJson::load_from_string(path.clone(), source) {
- Ok(result) => {
- lsp_log!(" Resolved package.json: \"{}\"", path.display());
- package_json = Some(result);
+ if let Some(import_map_url) = &import_map_from_settings {
+ add_watched_file(
+ import_map_url.clone(),
+ ConfigWatchedFileType::ImportMap,
+ );
+ // spawn due to the lsp's `Send` requirement
+ let fetch_result = deno_core::unsync::spawn({
+ let file_fetcher = file_fetcher.cloned().unwrap();
+ let import_map_url = import_map_url.clone();
+ async move {
+ file_fetcher
+ .fetch(&import_map_url, &PermissionsContainer::allow_all())
+ .await
}
+ })
+ .await
+ .unwrap();
+
+ let value_result = fetch_result.and_then(|f| {
+ serde_json::from_slice::<Value>(&f.source).map_err(|e| e.into())
+ });
+ match value_result {
+ Ok(value) => Some(SpecifiedImportMap {
+ base_url: import_map_url.clone(),
+ value,
+ }),
Err(err) => {
lsp_warn!(
- " Couldn't read package.json \"{}\": {}",
- path.display(),
+ " Couldn't read import map \"{}\": {}",
+ import_map_url.as_str(),
err
);
+ import_map_from_settings = None;
+ None
}
}
+ } else {
+ None
}
- }
- let npmrc = discover_npmrc(package_json_path, config_file_path)
- .inspect(|(_, path)| {
- if let Some(path) = path {
- lsp_log!(" Resolved .npmrc: \"{}\"", path.display());
- }
- })
- .inspect_err(|err| {
- lsp_warn!(" Couldn't read .npmrc for \"{scope}\": {err}");
- })
- .map(|(r, _)| r)
- .ok();
- let byonm = if let Some(workspace_root) = workspace_root {
- workspace_root.byonm
- } else {
- std::env::var("DENO_UNSTABLE_BYONM").is_ok()
- || config_file
- .as_ref()
- .map(|c| c.has_unstable("byonm"))
- .unwrap_or(false)
- || (*DENO_FUTURE
- && package_json.is_some()
- && config_file
- .as_ref()
- .map(|c| c.json.node_modules_dir.is_none())
- .unwrap_or(true))
};
- if byonm {
- lsp_log!(" Enabled 'bring your own node_modules'.");
- }
- let node_modules_dir = if let Some(workspace_root) = workspace_root {
- workspace_root.node_modules_dir.clone()
- } else {
- config_file
- .as_ref()
- .and_then(|c| resolve_node_modules_dir(c, byonm))
- };
-
- // Load import map
- let mut import_map = None;
- let mut import_map_value = None;
- let mut import_map_specifier = None;
- let mut import_map_from_settings = false;
- if let Some(workspace_data) = workspace_root {
- import_map.clone_from(&workspace_data.import_map);
- import_map_from_settings = workspace_data.import_map_from_settings;
- } else {
- if let Some(config_file) = &config_file {
- if config_file.is_an_import_map() {
- import_map_value =
- Some(config_file.to_import_map_value_from_imports());
- import_map_specifier = Some(config_file.specifier.clone());
- } else if let Ok(Some(specifier)) =
- config_file.to_import_map_specifier()
- {
- import_map_specifier = Some(specifier);
- }
- }
- import_map_specifier = import_map_specifier.or_else(|| {
- let import_map_str = settings.import_map.as_ref()?;
- let specifier = Url::parse(import_map_str)
- .ok()
- .or_else(|| workspace_folder?.join(import_map_str).ok())?;
- import_map_from_settings = true;
- Some(specifier)
- });
- if let Some(specifier) = &import_map_specifier {
- if let Ok(path) = specifier_to_file_path(specifier) {
- watched_files
- .entry(specifier.clone())
- .or_insert(ConfigWatchedFileType::ImportMap);
- let import_map_canonicalized_specifier =
- canonicalize_path_maybe_not_exists(&path)
- .ok()
- .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
- if let Some(specifier) = import_map_canonicalized_specifier {
- watched_files
- .entry(specifier)
- .or_insert(ConfigWatchedFileType::ImportMap);
- }
- }
- if import_map_value.is_none() {
- if let Some(file_fetcher) = file_fetcher {
- // spawn due to the lsp's `Send` requirement
- let fetch_result = deno_core::unsync::spawn({
- let file_fetcher = file_fetcher.clone();
+ let resolver = deno_core::unsync::spawn({
+ let workspace = workspace.clone();
+ let file_fetcher = file_fetcher.cloned();
+ async move {
+ workspace
+ .create_resolver(
+ CreateResolverOptions {
+ pkg_json_dep_resolution,
+ specified_import_map,
+ },
+ move |specifier| {
let specifier = specifier.clone();
+ let file_fetcher = file_fetcher.clone().unwrap();
async move {
- file_fetcher
+ let file = file_fetcher
.fetch(&specifier, &PermissionsContainer::allow_all())
- .await
- }
- })
- .await
- .unwrap();
- let value_result = fetch_result.and_then(|f| {
- serde_json::from_slice::<Value>(&f.source).map_err(|e| e.into())
- });
- match value_result {
- Ok(value) => {
- import_map_value = Some(value);
+ .await?
+ .into_text_decoded()?;
+ Ok(file.source.to_string())
}
- Err(err) => {
- lsp_warn!(
- " Couldn't read import map \"{}\": {}",
- specifier.as_str(),
- err
- );
- }
- }
- }
- }
- }
- if let (Some(value), Some(specifier)) =
- (import_map_value, import_map_specifier)
- {
- match import_map::parse_from_value(specifier.clone(), value) {
- Ok(result) => {
- if config_file.as_ref().map(|c| &c.specifier) == Some(&specifier) {
- lsp_log!(" Resolved import map from configuration file");
- } else {
- lsp_log!(" Resolved import map: \"{}\"", specifier.as_str());
- }
- if !result.diagnostics.is_empty() {
- lsp_warn!(
- " Import map diagnostics:\n{}",
- result
- .diagnostics
- .iter()
- .map(|d| format!(" - {d}"))
- .collect::<Vec<_>>()
- .join("\n")
- );
- }
- import_map = Some(Arc::new(result.import_map));
- }
- Err(err) => {
+ },
+ )
+ .await
+ .inspect_err(|err| {
lsp_warn!(
- "Couldn't read import map \"{}\": {}",
- specifier.as_str(),
- err
+ " Failed to load resolver: {}",
+ err // will contain the specifier
);
- }
- }
+ })
+ .ok()
}
- }
-
- let package_config = config_file.as_ref().and_then(|c| {
- Some(LspPackageConfig {
- nv: PackageNv {
- name: c.json.name.clone()?,
- version: Version::parse_standard(c.json.version.as_ref()?).ok()?,
- },
- exports: c.json.exports.clone()?,
- })
+ })
+ .await
+ .unwrap()
+ .unwrap_or_else(|| {
+ // create a dummy resolver
+ WorkspaceResolver::new_raw(
+ scope.clone(),
+ None,
+ workspace.package_jsons().cloned().collect(),
+ pkg_json_dep_resolution,
+ )
});
-
- let workspace_config = config_file
- .as_ref()
- .and_then(|c| c.to_workspace_config().ok().flatten().map(|w| (c, w)));
- let is_workspace_root = workspace_config.is_some();
- let workspace_members =
- if let Some((config, workspace_config)) = workspace_config {
- Arc::new(
- workspace_config
- .members
- .iter()
- .flat_map(|p| {
- let dir_specifier = config.specifier.join(p).ok()?;
- let dir_path = specifier_to_file_path(&dir_specifier).ok()?;
- Url::from_directory_path(normalize_path(dir_path)).ok()
- })
- .collect(),
- )
- } else if let Some(workspace_data) = workspace_root {
- workspace_data.workspace_members.clone()
- } else if config_file.as_ref().is_some_and(|c| c.json.name.is_some()) {
- Arc::new(vec![scope.clone()])
- } else {
- Arc::new(vec![])
- };
- let workspace_root_dir = if is_workspace_root {
- scope.clone()
- } else {
- workspace_root
- .as_ref()
- .map(|r| r.scope.clone())
- .unwrap_or_else(|| scope.clone())
- };
+ if !resolver.diagnostics().is_empty() {
+ lsp_warn!(
+ " Import map diagnostics:\n{}",
+ resolver
+ .diagnostics()
+ .iter()
+ .map(|d| format!(" - {d}"))
+ .collect::<Vec<_>>()
+ .join("\n")
+ );
+ }
ConfigData {
- scope: scope.clone(),
- config_file: config_file.map(Arc::new),
- fmt_options,
- lint_options,
+ scope,
+ workspace,
+ member_ctx: Arc::new(member_ctx),
+ resolver: Arc::new(resolver),
+ fmt_config,
lint_config,
+ test_config,
+ deno_lint_config,
lint_rules,
+ exclude_files,
ts_config: Arc::new(ts_config),
byonm,
node_modules_dir,
vendor_dir,
lockfile,
- package_json: package_json.map(Arc::new),
npmrc,
- import_map,
import_map_from_settings,
- package_config: package_config.map(Arc::new),
- is_workspace_root,
- workspace_root_dir,
- workspace_members,
watched_files,
}
}
+
+ pub fn maybe_deno_json(&self) -> Option<&Arc<deno_config::ConfigFile>> {
+ self.member_ctx.maybe_deno_json()
+ }
+
+ pub fn maybe_pkg_json(
+ &self,
+ ) -> Option<&Arc<deno_config::package_json::PackageJson>> {
+ self.member_ctx.maybe_pkg_json()
+ }
}
#[derive(Clone, Debug, Default)]
@@ -1622,20 +1588,18 @@ impl ConfigTree {
&self.scopes
}
- pub fn config_file_for_specifier(
+ pub fn workspace_member_ctx_for_specifier(
&self,
specifier: &ModuleSpecifier,
- ) -> Option<&Arc<ConfigFile>> {
- self
- .data_for_specifier(specifier)
- .and_then(|d| d.config_file.as_ref())
+ ) -> Option<&Arc<WorkspaceMemberContext>> {
+ self.data_for_specifier(specifier).map(|d| &d.member_ctx)
}
pub fn config_files(&self) -> Vec<&Arc<ConfigFile>> {
self
.scopes
.iter()
- .filter_map(|(_, d)| d.config_file.as_ref())
+ .filter_map(|(_, d)| d.maybe_deno_json())
.collect()
}
@@ -1643,18 +1607,23 @@ impl ConfigTree {
self
.scopes
.iter()
- .filter_map(|(_, d)| d.package_json.as_ref())
+ .filter_map(|(_, d)| d.maybe_pkg_json())
.collect()
}
- pub fn fmt_options_for_specifier(
+ pub fn fmt_config_for_specifier(
&self,
specifier: &ModuleSpecifier,
- ) -> Arc<FmtOptions> {
+ ) -> Arc<FmtConfig> {
self
.data_for_specifier(specifier)
- .map(|d| d.fmt_options.clone())
- .unwrap_or_default()
+ .map(|d| d.fmt_config.clone())
+ .unwrap_or_else(|| {
+ Arc::new(FmtConfig {
+ options: Default::default(),
+ files: FilePatterns::new_with_base(PathBuf::from("/")),
+ })
+ })
}
/// Returns (scope_uri, type).
@@ -1690,6 +1659,12 @@ impl ConfigTree {
file_fetcher: &Arc<FileFetcher>,
) {
lsp_log!("Refreshing configuration tree...");
+ // since we're resolving a workspace multiple times in different
+ // folders, we want to cache all the lookups and config files across
+ // ConfigData::load calls
+ let cached_fs = CachedDenoConfigFs::default();
+ let deno_json_cache = DenoJsonMemCache::default();
+ let pkg_json_cache = PackageJsonMemCache::default();
let mut scopes = BTreeMap::new();
for (folder_uri, ws_settings) in &settings.by_workspace_folder {
let mut ws_settings = ws_settings.as_ref();
@@ -1699,19 +1674,23 @@ impl ConfigTree {
if let Some(ws_settings) = ws_settings {
if let Some(config_path) = &ws_settings.config {
if let Ok(config_uri) = folder_uri.join(config_path) {
- scopes.insert(
- folder_uri.clone(),
- Arc::new(
- ConfigData::load(
- Some(&config_uri),
- folder_uri,
- None,
- settings,
- Some(file_fetcher),
- )
- .await,
- ),
- );
+ if let Ok(config_file_path) = config_uri.to_file_path() {
+ scopes.insert(
+ folder_uri.clone(),
+ Arc::new(
+ ConfigData::load(
+ Some(&config_file_path),
+ folder_uri,
+ settings,
+ file_fetcher,
+ &cached_fs,
+ &deno_json_cache,
+ &pkg_json_cache,
+ )
+ .await,
+ ),
+ );
+ }
}
}
}
@@ -1719,7 +1698,8 @@ impl ConfigTree {
for specifier in workspace_files {
if !(specifier.path().ends_with("/deno.json")
- || specifier.path().ends_with("/deno.jsonc"))
+ || specifier.path().ends_with("/deno.jsonc")
+ || specifier.path().ends_with("/package.json"))
{
continue;
}
@@ -1729,46 +1709,35 @@ impl ConfigTree {
if scopes.contains_key(&scope) {
continue;
}
- let data = ConfigData::load(
- Some(specifier),
- &scope,
- None,
- settings,
- Some(file_fetcher),
- )
- .await;
- if data.is_workspace_root {
- for member_scope in data.workspace_members.iter() {
- if scopes.contains_key(member_scope) {
- continue;
- }
- let Ok(member_path) = specifier_to_file_path(member_scope) else {
- continue;
- };
- let Some(config_file_path) = Some(member_path.join("deno.json"))
- .filter(|p| p.exists())
- .or_else(|| {
- Some(member_path.join("deno.jsonc")).filter(|p| p.exists())
- })
- else {
- continue;
- };
- let Ok(config_file_specifier) = Url::from_file_path(config_file_path)
- else {
- continue;
- };
- let member_data = ConfigData::load(
- Some(&config_file_specifier),
- member_scope,
- Some(&data),
- settings,
- Some(file_fetcher),
- )
- .await;
- scopes.insert(member_scope.clone(), Arc::new(member_data));
+ let data = Arc::new(
+ ConfigData::load(
+ None,
+ &scope,
+ settings,
+ file_fetcher,
+ &cached_fs,
+ &deno_json_cache,
+ &pkg_json_cache,
+ )
+ .await,
+ );
+ scopes.insert(scope, data.clone());
+ for (member_scope, _) in data.workspace.config_folders() {
+ if scopes.contains_key(member_scope) {
+ continue;
}
+ let member_data = ConfigData::load(
+ None,
+ member_scope,
+ settings,
+ file_fetcher,
+ &cached_fs,
+ &deno_json_cache,
+ &pkg_json_cache,
+ )
+ .await;
+ scopes.insert(member_scope.as_ref().clone(), Arc::new(member_data));
}
- scopes.insert(scope, Arc::new(data));
}
for folder_uri in settings.by_workspace_folder.keys() {
@@ -1782,9 +1751,11 @@ impl ConfigTree {
ConfigData::load(
None,
folder_uri,
- None,
settings,
- Some(file_fetcher),
+ file_fetcher,
+ &cached_fs,
+ &deno_json_cache,
+ &pkg_json_cache,
)
.await,
),
@@ -1797,24 +1768,43 @@ impl ConfigTree {
#[cfg(test)]
pub async fn inject_config_file(&mut self, config_file: ConfigFile) {
let scope = config_file.specifier.join(".").unwrap();
+ let json_text = serde_json::to_string(&config_file.json).unwrap();
+ let test_fs = deno_runtime::deno_fs::InMemoryFs::default();
+ let config_path = specifier_to_file_path(&config_file.specifier).unwrap();
+ test_fs.setup_text_files(vec![(
+ config_path.to_string_lossy().to_string(),
+ json_text,
+ )]);
+ let workspace = Arc::new(
+ Workspace::discover(
+ deno_config::workspace::WorkspaceDiscoverStart::ConfigFile(
+ &config_path,
+ ),
+ &deno_config::workspace::WorkspaceDiscoverOptions {
+ fs: &deno_runtime::deno_fs::DenoConfigFsAdapter::new(&test_fs),
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ );
let data = Arc::new(
ConfigData::load_inner(
- Some(config_file),
- &scope,
- None,
+ workspace,
+ Arc::new(scope.clone()),
&Default::default(),
None,
)
.await,
);
+ assert!(data.maybe_deno_json().is_some());
self.scopes = Arc::new([(scope, data)].into_iter().collect());
}
}
-fn resolve_lockfile_from_config(
- config_file: &ConfigFile,
+fn resolve_lockfile_from_workspace(
+ workspace: &Workspace,
) -> Option<CliLockfile> {
- let lockfile_path = match config_file.resolve_lockfile_path() {
+ let lockfile_path = match workspace.resolve_lockfile_path() {
Ok(Some(value)) => value,
Ok(None) => return None,
Err(err) => {
@@ -1826,28 +1816,29 @@ fn resolve_lockfile_from_config(
}
fn resolve_node_modules_dir(
- config_file: &ConfigFile,
+ workspace: &Workspace,
byonm: bool,
) -> Option<PathBuf> {
// For the language server, require an explicit opt-in via the
// `nodeModulesDir: true` setting in the deno.json file. This is to
// reduce the chance of modifying someone's node_modules directory
// without them having asked us to do so.
- let explicitly_disabled = config_file.json.node_modules_dir == Some(false);
+ let explicitly_disabled = workspace.node_modules_dir() == Some(false);
if explicitly_disabled {
return None;
}
let enabled = byonm
- || config_file.json.node_modules_dir == Some(true)
- || config_file.json.vendor == Some(true);
+ || workspace.node_modules_dir() == Some(true)
+ || workspace.vendor_dir_path().is_some();
if !enabled {
return None;
}
- if config_file.specifier.scheme() != "file" {
- return None;
- }
- let file_path = config_file.specifier.to_file_path().ok()?;
- let node_modules_dir = file_path.parent()?.join("node_modules");
+ let node_modules_dir = workspace
+ .root_folder()
+ .0
+ .to_file_path()
+ .ok()?
+ .join("node_modules");
canonicalize_path_maybe_not_exists(&node_modules_dir).ok()
}
@@ -1869,6 +1860,107 @@ fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option<CliLockfile> {
}
}
+// todo(dsherret): switch to RefCell once the lsp no longer requires Sync
+#[derive(Default)]
+struct DenoJsonMemCache(Mutex<HashMap<PathBuf, Arc<ConfigFile>>>);
+
+impl deno_config::DenoJsonCache for DenoJsonMemCache {
+ fn get(&self, path: &Path) -> Option<Arc<ConfigFile>> {
+ self.0.lock().get(path).cloned()
+ }
+
+ fn set(&self, path: PathBuf, data: Arc<ConfigFile>) {
+ self.0.lock().insert(path, data);
+ }
+}
+
+#[derive(Default)]
+struct PackageJsonMemCache(Mutex<HashMap<PathBuf, Arc<PackageJson>>>);
+
+impl deno_config::package_json::PackageJsonCache for PackageJsonMemCache {
+ fn get(&self, path: &Path) -> Option<Arc<PackageJson>> {
+ self.0.lock().get(path).cloned()
+ }
+
+ fn set(&self, path: PathBuf, data: Arc<PackageJson>) {
+ self.0.lock().insert(path, data);
+ }
+}
+
+#[derive(Default)]
+struct CachedFsItems<T: Clone> {
+ items: HashMap<PathBuf, Result<T, std::io::Error>>,
+}
+
+impl<T: Clone> CachedFsItems<T> {
+ pub fn get(
+ &mut self,
+ path: &Path,
+ action: impl FnOnce(&Path) -> Result<T, std::io::Error>,
+ ) -> Result<T, std::io::Error> {
+ let value = if let Some(value) = self.items.get(path) {
+ value
+ } else {
+ let value = action(path);
+ // just in case this gets really large for some reason
+ if self.items.len() == 16_384 {
+ return value;
+ }
+ self.items.insert(path.to_owned(), value);
+ self.items.get(path).unwrap()
+ };
+ value
+ .as_ref()
+ .map(|v| (*v).clone())
+ .map_err(|e| std::io::Error::new(e.kind(), e.to_string()))
+ }
+}
+
+#[derive(Default)]
+struct InnerData {
+ stat_calls: CachedFsItems<deno_config::fs::FsMetadata>,
+ read_to_string_calls: CachedFsItems<String>,
+ read_dir_calls: CachedFsItems<Vec<deno_config::fs::FsDirEntry>>,
+}
+
+#[derive(Default)]
+struct CachedDenoConfigFs(Mutex<InnerData>);
+
+impl DenoConfigFs for CachedDenoConfigFs {
+ fn stat_sync(
+ &self,
+ path: &Path,
+ ) -> Result<deno_config::fs::FsMetadata, std::io::Error> {
+ self
+ .0
+ .lock()
+ .stat_calls
+ .get(path, |path| RealDenoConfigFs.stat_sync(path))
+ }
+
+ fn read_to_string_lossy(
+ &self,
+ path: &Path,
+ ) -> Result<String, std::io::Error> {
+ self
+ .0
+ .lock()
+ .read_to_string_calls
+ .get(path, |path| RealDenoConfigFs.read_to_string_lossy(path))
+ }
+
+ fn read_dir(
+ &self,
+ path: &Path,
+ ) -> Result<Vec<deno_config::fs::FsDirEntry>, std::io::Error> {
+ self
+ .0
+ .lock()
+ .read_dir_calls
+ .get(path, |path| RealDenoConfigFs.read_dir(path))
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -2132,7 +2224,7 @@ mod tests {
#[tokio::test]
async fn config_enable_via_config_file_detection() {
- let root_uri = resolve_url("file:///root/").unwrap();
+ let root_uri = root_dir();
let mut config = Config::new_with_roots(vec![root_uri.clone()]);
assert!(!config.specifier_enabled(&root_uri));
@@ -2153,7 +2245,7 @@ mod tests {
// Regression test for https://github.com/denoland/vscode_deno/issues/917.
#[test]
fn config_specifier_enabled_matches_by_path_component() {
- let root_uri = resolve_url("file:///root/").unwrap();
+ let root_uri = root_dir();
let mut config = Config::new_with_roots(vec![root_uri.clone()]);
config.set_workspace_settings(
WorkspaceSettings {
@@ -2167,7 +2259,7 @@ mod tests {
#[tokio::test]
async fn config_specifier_enabled_for_test() {
- let root_uri = resolve_url("file:///root/").unwrap();
+ let root_uri = root_dir();
let mut config = Config::new_with_roots(vec![root_uri.clone()]);
let mut settings = WorkspaceSettings {
enable: Some(true),
@@ -2256,4 +2348,12 @@ mod tests {
!config.specifier_enabled_for_test(&root_uri.join("mod2.ts").unwrap())
);
}
+
+ fn root_dir() -> Url {
+ if cfg!(windows) {
+ Url::parse("file://C:/root/").unwrap()
+ } else {
+ Url::parse("file:///root/").unwrap()
+ }
+ }
}